uplink icon indicating copy to clipboard operation
uplink copied to clipboard

Make it simple to group requests by resource under a single namespace/consumer

Open prkumar opened this issue 6 years ago • 4 comments

Is your feature request related to a problem? Please describe. This feature requests is spun out from a discussion on gitter:

Hi. Is there a best practice for making namespaces? For example, my API has different things in it like User (list, get, create, delete), Pizza (list, get, order) and so on. So I'd like to call each method inside of a namespace, e.g.

api = PizzaClient(...)
user = api.user.get(...)
pizza_order = api.pizza.order(...)
print(api.orders.status(user, pizza_order))

You get the idea.

Currently, we can achieve this by defining separate consumers for each resource (e.g., User and Pizza), then composing those consumers under a single class (e.g., PizzaClient) that acts as the root of the namespace. So, something like:

class Users(Consumer):
    @get("/users/{id}")
    def get(self, id) -> User:
        """Gets a user."""

class Pizzas(Consumer):
    @post("/pizzas", args={"pizza": Body})
    def order(self, pizza: Pizza) -> Order:
        """Orders a pizza."""
 
 class Orders(Consumer):
    @get("/orders/{id}")
    def status(self, id) -> OrderStatus:
        """Returns the order status."""
         
class PizzaClient(object):
    def __init__(self, ...):
        self.user = Users(...)
        self.pizza = Pizzas(...)
        self.orders = Orders(...)

However this can get kinda tedious to manage:

Composing consumers this way is OK, but since they have to share the same internal stuff (session, auth, base_url etc) - it can be tedious.

Describe the solution you'd like Here's a variant of what @Senpos' proposed on gitter:

adding a resource could be as simple as defining a class attribute of type Resource on a Consumer:

class Users(Resource):
    @get("/users/{id}")
    def get(self, id) -> User:
        """Gets a user."""
...
class PizzaClient(Consumer):
    users = Users()

Additional context This should work great since we can have the ConsumerMeta (an internal metaclass for the Consumer object) discover these Resource types on instantiation and class creation.

prkumar avatar Mar 02 '19 21:03 prkumar

Yes! Making this easier would be nice.

Here is how I approached this in https://github.com/OpenAPITools/openapi-generator/pull/2270:

This is a global client that has attributes for each of the individual consumers: https://github.com/OpenAPITools/openapi-generator/blob/51ffdb3d1a0f3b7b6c7cdafe7c286c3a894f0bf9/samples/client/petstore/python-uplink/openapi_client_python_uplink/client.py#L82-L86

The consumers are lazy loaded thanks to ApiDescriptor, saving the session so that it is shared between the consumers.

cognifloyd avatar Mar 03 '19 03:03 cognifloyd

Here is a code snippet that summarizes some thoughts I had regarding the requirements for this feature:

from uplink import Resource, Consumer, get, timeout, retry

# On its own, a Resource is meaningless. More specifically, a Resource 
# instance can’t make a request on its own, rather it needs to be bound to 
# a Consumer instance.
class Users(Resource):

    # Resource subclasses should not override the constructor (i.e., __init__)

    @get("/users/{id}")
    def get(self, id):
        pass

    def set_token(self, access_token):
        # The resource also has a session object. It inherits values
        # from the Consumer session, but should be able to overwrite entries
        # without affecting other resources bound to the consumer
        self.session.params["Authorization"] = access_token


# Class decorators on the parent Consumer propagate to any Resource attributes,
# recursively, similar to how class decorators work for consumer methods.
@timeout(5)
class GitHub(Consumer):
    # The resource becomes useful when it's bound to a Consumer.
    users = Users()  # or Users.bind()

    def __init__(self, users_token):
        # The token should only appear on Users calls made from this
        # instance of the GitHub client.
        self.users.set_token(users_token)

        # ...

# Decorators on a subclass should propagate to Resources without affecting
# calls made through the superclass, similar to how it works with consumer
# methods.
@retry(on_exception=retry.CONNECTION_TIMEOUT)
class RetryingGitHub(GitHub):
    pass

prkumar avatar Mar 19 '19 03:03 prkumar

Another helpful concept could be the Entity, which removes the reliance on the consumer for making calls against a specific instance of a resource:


class Order(Entity):
    
     def __init__(self, response):
          ...
     
     @get("/orders/{self.order}")   # You can reference an attribute on the object.
     def is_ready(self):
         pass

class PizzaClient(uplink.Consumer):
    @json
    @post("/orders")
    def order(self, **options: Body) -> Order:
        pass

Another option is to just pass the consumer/resource down to the entity object through its constructor. This would remove a lot of complexity in the implementation. The problem here is that you'll probably need to add a method to the consumer for each method of the entity class. This can lead to a lot of duplicate method signatures.

class Order(Entity):
    
     def __init__(self, consumer, response):
          self.consumer = consumer
          ...
     
     def is_ready(self):
         return self.consumer.check_order_status(self.id)

prkumar avatar Jun 14 '19 02:06 prkumar

Has there ever been any attempt to implement this into the package?

wabiloo avatar Mar 26 '23 10:03 wabiloo