Access current request without passing it through as context

Hello everyone,

I’m quite new to Python and even newer to asyncio. But I like the thinking of Sanic, it reminds me a lot of Silex (from the PHP world). Anyways, this small intro is here to show that my reasoning about web app development is a bit biased towards the PHP / Java world. So what is in my head how to solve the following problem, is probably not really Pythonic, but let’s see.

I created a UserLibrary that has several methods, such as async def get_tenant(self, tenant_id) and async def create_ticket(self, subject, content). I instantiated this library under app.ctx.user_library, just after where I create my Sanic app instance. Is this the right way to create such a variable?

This user library is used by some inner platform, where the user writes their own Python. We massage that Python code into a Playbook, that looks like this:

class Playbook:
  async def run(self, u):
    # Some user provided Python
    result = {}
    # ...
    tenant = await u.get_tenant(123)
    # ...

    return result

‘We’ call run(app.ctx.user_library). The users know that they are able to call tenant = await u.get_tenant(123) to retrieve a tenant.

Now the user library does a HTTP call using httpx when get_tenant is issued. What I now want to do, is to include some of the original HTTP request headers (request ID, tracing headers, etc. etc.) in this outgoing HTTP request. But I do not want to bother the users by providing this context, or them passing along all these variables.

So I was wondering whether you can somehow retrieve Sanic’s current request from inside the UserLibrary?

:heart:

Sounds like it. Happy to see some code if you want to share. Typically this is added in something like this:

@app.before_server_start
async def setup(app, _):
    app.ctx.user_library = UserLibrary()

On a completely unrelated topic, there is a really nice new feature coming in v21.12 that sort of simplifies a lot of that.

Does the UserLibrary instance have to persist for the lifetime of the server? Can it be created once per request, with the request object passed in?

@app.on_request
async def setup(request):
    request.ctx.user_library = UserLibrary(request)

If not, an answer might be contextvars. That will surely do what you want. LMK if you need help setting it up.

Thanks for the quick reply :slight_smile: !

This is indeed how I instantiate the UserLibrary. (On a side-note, would it also be possible to do instantiate it like this, instead of through the before_server_start decorator?)

app = Sanic()
app.ctx.user_library = UserLibrary()
app.run()

Nice!

It would be nice to persist the UserLibrary for the lifetime of the server. Quite a few httpx clients are created by it, and to reuse some of those clients for the duration of the server lifetime seems like a good thing to do. (Although I don’t know if httpx keeps a connection pool itself or something like that.)

So how would I do this, if I want to instantiate the UserLibrary once per server?

This would instantiate it for each and every request, right? So also for routes where the user library is not used, therefore not my favorite option :slight_smile:.

I’m still a bit confused by the ‘memory model’ of Sanic (or asyncio in general)? (That’s also the reason of the before_server_start or in global scope instantiation question from above.) It looks like the before_server_start is called once per Sanic process / worker. Is that the reason you have to hold the instance in app.ctx, because the app context is created for every worker?

And how does this relate to Python’s contextvars?

You can. I generally would not advise that (instantiating objects in the global scope) however. This is particularly true when running in a development server or with multiple workers since you end up with those objects in the management processes as well.

That is what putting it on the app.ctx will do.

The before_server_start pattern.

Yes. You could alternatively do it in a decorator–which would then give you control over where it is created. Or, you could add a check in the middleware for the route to only do it on certain routes and not all of them.

The last alternative is using Sanic Extensions injection API. I am particularly thinking about the last example on that page. It would look something like this:

ext = Extend(app)

async def setup_user_library(request: Request):
    return UserLibrary()

ext.injection(UserLibrary, setup_user_library)

@app.route(...)
async def handler(request: Request, user_library: UserLibrary):
    ...

This would only instantiate it on requests where there is a UserLibrary annotation in the function signature. This is what I was talking about that will be easier in the next version. It is taking this concept and cleaning it up a little and making it easier. See the PR for more info.