Fastapi: [QUESTION] Storing object instances in the app context

Created on 15 Mar 2019  Â·  12Comments  Â·  Source: tiangolo/fastapi

Description

In Flask, I've found this to be a common pattern to do something like this:

run_app.py

from api.factory import create_app

app = create_app()

api/factory.py

from flask import Flask, current_app
import redis
import config
from api.resources.basic import basic

def create_app():
    """Create the app."""
    app = Flask(__name__)
    app.config.from_object(config.ProductionConfig)
    app.redis = redis.Redis(host=app.config["REDIS_HOST"], port=app.config["REDIS_PORT"])
    app.register_blueprint(basic, url_prefix="/api/v1")

api/resources/basic.py

from flask import current_app
from flask import Blueprint
from webargs import fields
from webargs.flaskparser import use_args

mod = Blueprint('basic', __name__)
write_args = {"value": fields.Str(required=True)}

@mod.route('/<str:key>', methods=['POST'])
@use_args(write_args, key)
def create_resource(args, key):
   """POST resource to redis."""
   current_app.redis.set(key, args["value"])
   current_app.logger.info("inserted value into redis")
   return {key: args["value"]}, 201

@mod.route('/<str:key>', methods=['GET'])
def retrieve_resource(key):
   """GET value from redis."""
   return {key: current_app.redis.get(key)}, 200

Is it possible to have a 'global' app context where you can put objects shared across request contexts, where you would access objects that you don't want to keep reinstantiating because it might be expensive and because of the convenience and how it makes it easier to reason about where 'objects' live. Like current_app.redis, or current_app.logger, or current_app.kafka_producer. I read about Depends, and saw the examples provided for how to deal with dependencies / resources but I didn't see something like the above.

Might be a bad practice, but it's worked well for me so far.

question

Most helpful comment

@tiangolo, I'm working with a colleague on a FastAPI application and we're having a discussion about the merits of global module variables vs. passed dependencies. I've been advocating dependency passing as a way to modularize an application, govern access, facilitate testing, etc. For example, I've been advocating that a database repository object should be passed as a dependency of some kind and should not be a globally accessible module variable.

We noticed this issue and your comment about globals on 2019/3/17. I'm hoping you'll weigh in again since we're looking to the broader Python community for guidance on this question. Are global module variables considered idiomatic Python? Are the pitfalls of globals less applicable here? Can you think of FastAPI reference applications that heavily use module globals as a way to distribute dependencies like caches, database repositories, etc?

All 12 comments

You should look at: https://fastapi.tiangolo.com/tutorial/sql-databases/ I
think the db dependancy is what you describe:

On Fri, Mar 15, 2019 at 2:40 PM Lasse Vang Gravesen <
[email protected]> wrote:

Description

In Flask, I've found this to be a common pattern to do something like this:

run_app.py

from api.factory import create_app

app = create_app()

api/factory.py

from flask import Flask, current_app
import redis
import config
from webargs.flaskparser import use_args
from api.resources.basic import basic

def create_app():
"""Create the app."""
app = Flask(__name__)
app.config.from_object(config.ProductionConfig)
app.redis = redis.Redis(host=app.config["REDIS_HOST"], port=app.config["REDIS_PORT"])
app.register_blueprint(basic, url_prefix="/api/v1")

api/resources/basic.py

from flask import current_app
from flask import Blueprint
from webargs import fields
from webargs.flaskparser import use_args

mod = Blueprint('basic', __name__)
write_args = {"value": fields.Str(required=True)}

@mod.route('/write/', methods=['POST'])
@use_args(write_args )
def create_resource(args, key):
"""POST resource to redis."""
current_app.redis.set(key, args["value"])
current_app.logger.info("inserted value into redis")
return {key: args["value"]}, 201

@mod.route('/write/', methods=['GET'])
def retrieve_resource(key):
"""GET value from redis."""
return {key: current_app.redis.get(key)}, 200

Is it possible to have a 'global' app context where you can put objects
shared across request contexts, where you would access objects that you
don't want to keep reinstantiating because it might be expensive and
because of the convenience and how it makes it easier to reason about where
'objects' live. Like current_app.redis, or current_app.logger, or
current_app.kafka_producer. I read about Depends, and saw the examples
provided for how to deal with dependencies / resources but I didn't see
something like the above.

Might be a bad practice, but it's worked well for me so far.

—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/tiangolo/fastapi/issues/81, or mute the thread
https://github.com/notifications/unsubscribe-auth/ABDZPt4hbxMUXWPraZi2pTCysiKhBnXOks5vW6LUgaJpZM4b2fPB
.

--
benoit barthelet
http://pgp.mit.edu/pks/lookup?op=get&search=0xF150E01A72F6D2EE

@euri10
I read about that but I didn't think it was totally right and I didn't know how to adapt it to other resources while retaining the nice properties of the aforementioned approach.
Like for instance, if I were to create a Kafka producer for every single session that would be really inefficient, and it looks like the docs create a local sqlalchemy session at each request and then closes it.
So specifically, my issue with dependencies like suggested in the tutorial is that I didn't see a way to create an app-level instance of dependencies - where I want to avoid recreating the same instances over and over again on every request, and I also want to avoid having ephemereal resources that are initialized by a function call without really associating it with the app context.

The docs say:

request.state is a property of each Starlette Request object, it is there to store arbitrary objects attached to the request itself, like the database session in this case.

Where I would instead want an app object where I can store arbitrary objects that can be accessed throughout the application, like you can choose to store objects in the app object in Flask and then refer to those objects using the current_app context.

If I can find out a nice way to do this using this or figure out a way to adapt dependencies such that they are immediately accessible from a common location.
I'm not sure if I like the thought-model that has you call a function to initialize something somewhere. I prefer initializing everything in a factory and then associate the created instances with the app itself as to make that globally available as needed by simply using app.state.db or app.state.kafka_producer or app.state.elasticsearch.

sorry I misunderstood you

I may be wrong but I think for that you can use

@app.on_event('startup') @app.on_event('shutdown')

Le sam. 16 mars 2019 à 12:14 PM, Lasse Vang Gravesen <
[email protected]> a écrit :

@euri10 https://github.com/euri10
I read about that but I didn't think it was totally right and I didn't
know how to adapt it to other resources while retaining the nice properties
of the aforementioned approach.
Like for instance, if I were to create a Kafka producer for every single
session that would be really inefficient, and it looks like the docs create
a local sqlalchemy session at each request and then closes it.
So specifically, my issue with dependencies like suggested in the tutorial
is that I didn't see a way to create an app-level instance of dependencies

  • where I want to avoid recreating the same instances over and over again
    on every request, and I also want to avoid having ephemereal resources that
    are initialized by a function call without really associating it with the
    app context.

The docs say:

request.state is a property of each Starlette Request object, it is there
to store arbitrary objects attached to the request itself, like the
database session in this case.

Where I would instead want an app object where I can store arbitrary
objects that can be accessed throughout the application, like you can
choose to store objects in the app object in Flask and then refer to
those objects using the current_app context.

If I can find out a nice way to do this using this or figure out a way to
adapt dependencies such that they are immediately accessible from a common
location.
I'm not sure if I like the thought-model that has you call a function to
initialize something somewhere. I prefer initializing everything in a
factory and then associate the created instances with the app itself as to
make that globally available as needed by simply using app.state.db or
app.state.kafka_producer or app.state.elasticsearch.

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/tiangolo/fastapi/issues/81#issuecomment-473521359,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABDZPv5B-_KSJrFvCaJApV4Ez9KWgM8Gks5vXNIlgaJpZM4b2fPB
.

@euri10
I think I figured out how to approximately do something like this. The request object provides access to the app object. If you assign variables to the app objects you can later access it. See for instance this line in an example API that I quickly made.

https://github.com/Atheuz/Test-API/blob/master/api/routers/basic.py#L11

Hmm, in your case, why not a global object that you can import and use directly everywhere?

As you are not using something that requires transactions or something similar, just more or less atomic operations (Kafka, Redis), you can use the same object everywhere. You don't have to attach it to the app, just import it and use it.

Dependencies are for cases that need to somehow read data from the current request or similar things.

Adding things to the request state is for situations where you need to have something through all the request, like a SQLAlchemy session.

But in your case, if it's something that can be at the same application level, you don't really need to put it as a property in the application, you can just have the global object/variable in a file (module) and import it anywhere you need it. If you import it from different Python files, Python will take care of re-using the same object that was imported before.

Would that work for you?

@tiangolo

I've tried to keep global objects that I can import and use anywhere in other projects, but I've just found the experience of instantiating objects and attaching them as properties on the app context to be simpler to reason about and less confusing.

I think, my problem with the other approach, is a feeling that I don't know "when"/"if" global objects are instantiated, with the app context I handle instantiation through a factory where objects that I want to use are clearly instantiated and attached to the app context. If the app throws up an exception like: AttributeError: 'app' object has no attribute 'kafka_producer' - I know where to look for the problem, in the factory.py file where that instance SHOULD have been created but for some reason was not.
This also makes it simpler to test: Create an app in the conftest.py file, and then every functional object that the app requires is available through the app context without having to import 9 different functional objects. Also makes it simple to mock these properties out in cases I'm not interested in their behaviour, but rather if they get called as intended.

Examples of this in use:
Creating a logger for every single file in the application feels messy, whereas with this approach you would instantiate a logger once in the factory and assign it to the app as a property. Then the logger would be available through the app, like this: app.logger.info("I am a log entry", extra={"meaning-of-life": 42}).
This is what Flask does as well, where there is a logger object assigned to the app context that can be used anywhere in the application through current_app or app.

Similarly with config, I like to have that be part of the application itself, so I can always refer to it anywhere through the app without having to think about if I should import it. For instance, if I define an hostname for an API that needs to be used multiple places in the application, I would like it to be available through the app context without having to import anything, like so: app.config["FIREBASE_API_URL"].
This is also what Flask does, also reachable through app or current_app.

See here for an example of instantiation of a logger object:
https://github.com/Atheuz/Test-API/blob/master/api/factory.py

Then it's reused here, using the request.app that's available:
https://github.com/Atheuz/Test-API/blob/master/api/routers/basic.py#L11

This almost works for me, except not everything in an API is necessarily an endpoint, for instance you can have actions or background tasks that you call from endpoints to perform some work, and in that case the request instance is not available unless you pass it directly through from the callee. In Flask they resolve this by having it be possible to import current_app, as a proxy for app.

This is a pattern I've been using a lot over the last year and I don't know if I would want to change it without a good reason to.

I extended the pattern in my example API a little bit:

https://github.com/Atheuz/Test-API/blob/master/api/actions/storage.py#L14

Seems to be alright like this.
I'll also look into putting the objects into modules and then importing them at some point.

Cool! I'm glad you found a way to solve your problem that matches well your workflow.

@tiangolo still not totally sure about this approach. I'd like a standard way to do it. In Flask, apparently the current_app object is magic and handles locking per session that's using it so you don't end up in bad situations. The above mentioned approach would work in instances where the object to be used is atomic and/or thread safe. So I think for Kafka and Redis it might be OK. I doubt that it's OK with SQLAlchemy and I need to do something like the dependency injection to establish a local session.

For SQLAlchemy you probably want to attach the session to the request (that's what SQLAlchemy recommends), as in the docs: https://fastapi.tiangolo.com/tutorial/sql-databases/

@tiangolo, I'm working with a colleague on a FastAPI application and we're having a discussion about the merits of global module variables vs. passed dependencies. I've been advocating dependency passing as a way to modularize an application, govern access, facilitate testing, etc. For example, I've been advocating that a database repository object should be passed as a dependency of some kind and should not be a globally accessible module variable.

We noticed this issue and your comment about globals on 2019/3/17. I'm hoping you'll weigh in again since we're looking to the broader Python community for guidance on this question. Are global module variables considered idiomatic Python? Are the pitfalls of globals less applicable here? Can you think of FastAPI reference applications that heavily use module globals as a way to distribute dependencies like caches, database repositories, etc?

@ebarlas you're 100% right. The way FastAPI documentation proposing app configuration assuming you have global variables for use with (angular-style inspired)? dependency injection, and in big projects it will be a huge mess, but MORE important - it's complex to write reusable modules for different apps.
Other frameworks trying to solve this in different way, happily FastAPI is based on starlette - and there is request.state andrequest.app.state variables. Pseudo-code for proper application configuration will look something like this, use this factory in tests to override settings without ugly hacks like documentation suggests.

create_app(**config):
  config.set_default('debug', true)
  config.set_default('db_url', 'sqlite://:memory:')
  app = FastAPI(app)
  app.state.config = config
  setup_database(app)  # reads app.state.config, register middleware for request.state.db_session
  setup_redis(app)  # something similar
  return app
Was this page helpful?
0 / 5 - 0 ratings

Related issues

laith43d picture laith43d  Â·  3Comments

rlonka picture rlonka  Â·  3Comments

vnwarrior picture vnwarrior  Â·  3Comments

updatatoday picture updatatoday  Â·  3Comments

zero0nee picture zero0nee  Â·  3Comments