Fastapi: [QUESTION] How do I pass environment variables to my route files?

Created on 22 Jul 2019  ยท  8Comments  ยท  Source: tiangolo/fastapi

From the example given here - https://fastapi.tiangolo.com/tutorial/bigger-applications/

I have this kind of structure.

.
โ”œโ”€โ”€ app
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”œโ”€โ”€ main.py
โ”‚   โ””โ”€โ”€ routers
โ”‚       โ”œโ”€โ”€ __init__.py
โ”‚       โ”œโ”€โ”€ items.py
โ”‚       โ””โ”€โ”€ users.py

I would like to read environment variables, when running/deploying my code, and then pass those variables to items.py and users.py

How do I achieve this?

Do I have to use tags or is that for some other purpose?

question

Most helpful comment

@unography I would recommend the use of the BaseSettings class in pydantic. Here's a basic example:

from functools import lru_cache
import os
from typing import Optional

from pydantic import BaseSettings


class AppSettings(BaseSettings):
    project_name: str = "My API"
    debug: bool = False

    # Server
    server_name: Optional[str]
    server_host: Optional[str]
    sentry_dsn: Optional[str]
    secret_key: bytes = os.urandom(32)

    ...

    class Config:
        env_prefix = ""  # defaults to 'APP_'; see description in pydantic docs
        allow_mutation = False


@lru_cache()
def get_settings() -> AppSettings:
    return AppSettings()

This way you can basically treat the settings like a singleton by only accessing them via get_settings(), with the benefit that you can modify the environment after module imports but prior to the first call to get_settings, and the settings will reflect the environment on its first call. (This may be useful for testing purposes.)

All 8 comments

@euri10 @tiangolo any help?

tags is for giving a name and group endpoints in swagger so no it won't help

you can use simply os.environ to read env variables
or you can use starlette Configuration that reads env var then env vars from files etc
I'm sure there are many other ways,

I make some use of it here, maybe that can give you some ideas

@unography I would recommend the use of the BaseSettings class in pydantic. Here's a basic example:

from functools import lru_cache
import os
from typing import Optional

from pydantic import BaseSettings


class AppSettings(BaseSettings):
    project_name: str = "My API"
    debug: bool = False

    # Server
    server_name: Optional[str]
    server_host: Optional[str]
    sentry_dsn: Optional[str]
    secret_key: bytes = os.urandom(32)

    ...

    class Config:
        env_prefix = ""  # defaults to 'APP_'; see description in pydantic docs
        allow_mutation = False


@lru_cache()
def get_settings() -> AppSettings:
    return AppSettings()

This way you can basically treat the settings like a singleton by only accessing them via get_settings(), with the benefit that you can modify the environment after module imports but prior to the first call to get_settings, and the settings will reflect the environment on its first call. (This may be useful for testing purposes.)

I just wanted to add a further approach that you may wish to take. My approach is more similar to Flask and Django; it depends on environment variables mainly for knowing the path of your settings file.

I've chosen to use TOML, but you can use YAML or JSON too.

My settings.py file looks like this:

import os
from functools import lru_cache
from typing import Dict, List

import toml
from pydantic import BaseModel


class DatabaseSettings(BaseModel):
    host: str
    port: int
    username: str
    password: str
    db_name: str
    max_connections: int


class JWTSettings(BaseModel):
    key: str
    session_duration: int


class AuthenticationSettings(BaseModel):
    cert_path: str
    hosts: List[str]
    username: str
    password: str
    group_access: Dict[str, List[str]]


class Settings(BaseModel):
    database: DatabaseSettings
    jwt: JWTSettings
    authentication: AuthenticationSettings


@lru_cache()
def get_settings() -> Settings:
    settings_path = os.environ.get("MYAPP_SETTINGS", "/etc/myapp/myapp-app.toml")

    with open(settings_path) as fp:
        settings = Settings.parse_obj(toml.load(fp))

    return settings

It is also easy to point your unit tests to your testing config as long as you don't attempt to use get_settings too early.

os.environ["MYAPP_SETTINGS"] = "settings/testing/myapp-app.toml"


@pytest.fixture
def client():
    return TestClient(app)

If you are in a situation where this causes you trouble, you may wrap your app in a get_app function which is called similarly to that below in main.py:

if os.path.basename(sys.argv[0]) in ["gunicorn", "uvicorn"]:
    app = get_app()

This way, importing the main module in your unit tests won't create the app and you are free to set the appropriate environment variable before calling get_app yourself ๐Ÿ˜„

Note: In case anyone is unaware, it seems impossible to call a function when running gunicorn or uvicorn like you can with sync workers. As such, the main module must define an app variable itself which is why we need that litle trick above.

The best way to avoid this problem is to use the startup event handler for anything that must happen upon startup that needs to access settings. The startup event won't run immediately when the app is imported in your unit tests; only once the TestClient starts using it;.

Really hope this helps and provides some more ideas which we can develop further together.

Cheers
Fotis

Thanks everyone for the discussion and help here!

Yeah, for the simplest case, you can just use os.getenv().

For a more sophisticated case, I would go for BaseSettings, as @dmontagu suggests.

There are new docs for handling settings here: https://fastapi.tiangolo.com/advanced/settings/ , started by @alexmitelman in #1118 .

Now it documents more or less the same example from @dmontagu above :rocket:

Thanks @tiangolo. The updated documentation looks cool!

Is this issue considered as an open question yet? Can we close it? @unography

Yes, it's solved!
And the new documentation looks great!

Was this page helpful?
0 / 5 - 0 ratings