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?
@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!
Most helpful comment
@unography I would recommend the use of the
BaseSettingsclass in pydantic. Here's a basic example: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 toget_settings, and the settings will reflect the environment on its first call. (This may be useful for testing purposes.)