mirror of https://github.com/encode/starlette.git
214 lines
5.8 KiB
Markdown
214 lines
5.8 KiB
Markdown
Starlette encourages a strict separation of configuration from code,
|
|
following [the twelve-factor pattern][twelve-factor].
|
|
|
|
Configuration should be stored in environment variables, or in a ".env" file
|
|
that is not committed to source control.
|
|
|
|
**app.py**:
|
|
|
|
```python
|
|
from starlette.applications import Starlette
|
|
from starlette.config import Config
|
|
from starlette.datastructures import DatabaseURL, Secret
|
|
|
|
# Config will be read from environment variables and/or ".env" files.
|
|
config = Config(".env")
|
|
|
|
DEBUG = config('DEBUG', cast=bool, default=False)
|
|
DATABASE_URL = config('DATABASE_URL', cast=DatabaseURL)
|
|
SECRET_KEY = config('SECRET_KEY', cast=Secret)
|
|
|
|
app = Starlette()
|
|
app.debug = DEBUG
|
|
...
|
|
```
|
|
|
|
**.env**:
|
|
|
|
```shell
|
|
# Don't commit this to source control.
|
|
# Eg. Include ".env" in your `.gitignore` file.
|
|
DEBUG=True
|
|
DATABASE_URL=postgresql://localhost/myproject
|
|
SECRET_KEY=43n080musdfjt54t-09sdgr
|
|
```
|
|
|
|
## Configuration precedence
|
|
|
|
The order in which configuration values are read is:
|
|
|
|
* From an environment variable.
|
|
* From the ".env" file.
|
|
* The default value given in `config`.
|
|
|
|
If none of those match, then `config(...)` will raise an error.
|
|
|
|
## Secrets
|
|
|
|
For sensitive keys, the `Secret` class is useful, since it prevents the value from
|
|
leaking out into tracebacks or logging.
|
|
|
|
To get the value of a `Secret` instance, you must explicitly cast it to a string.
|
|
You should only do this at the point at which the value is used.
|
|
|
|
```python
|
|
>>> from myproject import settings
|
|
>>> print(settings.SECRET_KEY)
|
|
Secret('**********')
|
|
>>> str(settings.SECRET_KEY)
|
|
'98n349$%8b8-7yjn0n8y93T$23r'
|
|
```
|
|
|
|
Similarly, the `URL` and `DatabaseURL` class will hide any password component
|
|
in their representations.
|
|
|
|
```python
|
|
>>> from myproject import settings
|
|
>>> print(settings.DATABASE_URL)
|
|
DatabaseURL('postgresql://admin:**********@192.168.0.8/my-application')
|
|
>>> str(settings.SECRET_KEY)
|
|
'postgresql://admin:Fkjh348htGee4t3@192.168.0.8/my-application'
|
|
```
|
|
|
|
## Reading or modifying the environment
|
|
|
|
In some cases you might want to read or modify the environment variables programmatically.
|
|
This is particularly useful in testing, where you may want to override particular
|
|
keys in the environment.
|
|
|
|
Rather than reading or writing from `os.environ`, you should use Starlette's
|
|
`environ` instance. This instance is a mapping onto the standard `os.environ`
|
|
that additionally protects you by raising an error if any environment variable
|
|
is set *after* the point that it has already been read by the configuration.
|
|
|
|
If you're using `pytest`, then you can setup any initial environment in
|
|
`tests/conftest.py`.
|
|
|
|
**tests/conftest.py**:
|
|
|
|
```python
|
|
from starlette.config import environ
|
|
|
|
environ['TESTING'] = 'TRUE'
|
|
```
|
|
|
|
## A full example
|
|
|
|
Structuring large applications can be complex. You need proper separation of
|
|
configuration and code, database isolation during tests, separate test and
|
|
production databases, etc...
|
|
|
|
Here we'll take a look at a complete example, that demonstrates how
|
|
we can start to structure an application.
|
|
|
|
First, let's keep our settings, our database table definitions, and our
|
|
application logic separated:
|
|
|
|
**myproject/settings.py**:
|
|
|
|
```python
|
|
from starlette.config import Config
|
|
from starlette.datastructures import DatabaseURL, Secret
|
|
|
|
config = Config(".env")
|
|
|
|
DEBUG = config('DEBUG', cast=bool, default=False)
|
|
TESTING = config('TESTING', cast=bool, default=False)
|
|
SECRET_KEY = config('SECRET_KEY', cast=Secret)
|
|
|
|
DATABASE_URL = config('DATABASE_URL', cast=DatabaseURL)
|
|
if TESTING:
|
|
DATABASE_URL = DATABASE_URL.replace(database='test_' + DATABASE_URL.database)
|
|
```
|
|
|
|
**myproject/tables.py**:
|
|
|
|
```python
|
|
import sqlalchemy
|
|
|
|
# Database table definitions.
|
|
metadata = sqlalchemy.MetaData()
|
|
|
|
organisations = sqlalchemy.Table(
|
|
...
|
|
)
|
|
```
|
|
|
|
**myproject/app.py**
|
|
|
|
```python
|
|
from starlette.applications import Starlette
|
|
from starlette.middleware.database import DatabaseMiddleware
|
|
from starlette.middleware.session import SessionMiddleware
|
|
from myproject import settings
|
|
|
|
|
|
app = Starlette()
|
|
|
|
app.debug = settings.DEBUG
|
|
|
|
app.add_middleware(
|
|
SessionMiddleware,
|
|
secret_key=settings.SECRET_KEY,
|
|
)
|
|
app.add_middleware(
|
|
DatabaseMiddleware,
|
|
database_url=settings.DATABASE_URL,
|
|
rollback_on_shutdown=settings.TESTING
|
|
)
|
|
|
|
@app.route('/', methods=['GET'])
|
|
async def homepage(request):
|
|
...
|
|
```
|
|
|
|
Now let's deal with our test configuration.
|
|
We'd like to create a new test database every time the test suite runs,
|
|
and drop it once the tests complete. We'd also like to ensure
|
|
|
|
**tests/conftest.py**:
|
|
|
|
```python
|
|
from starlette.config import environ
|
|
from starlette.testclient import TestClient
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy_utils import database_exists, create_database
|
|
|
|
# This line would raise an error if we use it after 'settings' has been imported.
|
|
environ['TESTING'] = 'TRUE'
|
|
|
|
from myproject import settings
|
|
from myproject.app import app
|
|
from myproject.tables import metadata
|
|
|
|
|
|
@pytest.fixture(autouse=True, scope="session")
|
|
def setup_test_database():
|
|
"""
|
|
Create a clean test database every time the tests are run.
|
|
"""
|
|
url = str(settings.DATABASE_URL)
|
|
engine = create_engine(url)
|
|
assert not database_exists(url), 'Test database already exists. Aborting tests.'
|
|
create_database(url) # Create the test database.
|
|
metadata.create_all(engine) # Create the tables.
|
|
yield # Run the tests.
|
|
drop_database(url) # Drop the test database.
|
|
|
|
|
|
@pytest.fixture()
|
|
def client():
|
|
"""
|
|
Make a 'client' fixture available to test cases.
|
|
"""
|
|
# Our fixture is created within a context manager. This ensures that
|
|
# application startup and shutdown run for every test case.
|
|
#
|
|
# Because we've configured the DatabaseMiddleware with `rollback_on_shutdown`
|
|
# we'll get a complete rollback to the initial state after each test case runs.
|
|
with TestClient(app) as test_client:
|
|
yield test_client
|
|
```
|
|
|
|
[twelve-factor]: https://12factor.net/config
|