uplink
uplink copied to clipboard
Make it simple to group requests by resource under a single namespace/consumer
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
Resourceon aConsumer: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.
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.
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
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)
Has there ever been any attempt to implement this into the package?