Skip to content

Commit b542879

Browse files
committed
update settings management from starlette to pydantic
1 parent 495b3ea commit b542879

File tree

8 files changed

+164
-78
lines changed

8 files changed

+164
-78
lines changed

.env.example

+14-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# DB connection
12
POSTGRES_USER=username
23
POSTGRES_PASS=password
34
POSTGRES_DBNAME=postgis
@@ -7,6 +8,16 @@ POSTGRES_PORT=5432
78
# You can also define the DATABASE_URL directly
89
# DATABASE_URL=postgresql://username:[email protected]:5432/postgis
910

10-
TILE_RESOLUTION=4096
11-
TILE_BUFFER=256
12-
MAX_FEATURES_PER_TILE=10000
11+
# Tile settings
12+
TIMVT_TILE_RESOLUTION=4096
13+
TIMVT_TILE_BUFFER=256
14+
TIMVT_MAX_FEATURES_PER_TILE=10000
15+
16+
# Default Table/Function min/max zoom
17+
TIMVT_DEFAULT_MINZOOM=8
18+
TIMVT_DEFAULT_MAXZOOM=19
19+
20+
# TiMVT settings
21+
TIMVT_NAME = "Fast MVT Server"
22+
TIMVT_CORS_ORIGINS="*"
23+
TIMVT_DEBUG=TRUE

CHANGES.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Release Notes
22

3+
## 0.3.0 (2022-02-09)
4+
5+
* update settings management from starlette to pydantic and use `TIMVT_` prefix (https://github.com/developmentseed/timvt/pull/76)
6+
37
## 0.2.1 (2022-01-25)
48

59
* update FastAPI version requirement to allow `>=0.73`

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ If you want more info about `ST_AsMVT` function or on the subject of creating Ve
6868

6969
### Configuration
7070

71-
To be able to create Vector Tile, the application will need access to the PostGIS database. `TiMVT` uses [starlette](https://www.starlette.io/config/)'s configuration pattern which make use of environment variable and/or `.env` file to pass variable to the application.
71+
To be able to create Vector Tile, the application will need access to the PostGIS database. `TiMVT` uses [pydantic](https://pydantic-docs.helpmanual.io/usage/settings/)'s configuration pattern which make use of environment variable and/or `.env` file to pass variable to the application.
7272

7373
Example of `.env` file can be found in [.env.example](https://github.com/developmentseed/timvt/blob/master/.env.example)
7474
```

tests/conftest.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ def database_url(test_db):
3232
def app(database_url, monkeypatch):
3333
"""Create app with connection to the pytest database."""
3434
monkeypatch.setenv("DATABASE_URL", str(database_url))
35-
monkeypatch.setenv("DEFAULT_MINZOOM", str(5))
36-
monkeypatch.setenv("DEFAULT_MAXZOOM", str(12))
35+
monkeypatch.setenv("TIMVT_MINZOOM", str(5))
36+
monkeypatch.setenv("TIMVT_MAXZOOM", str(12))
3737

3838
from timvt.functions import registry as FunctionRegistry
3939
from timvt.layer import Function

timvt/db.py

+8-12
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,12 @@
55

66
from buildpg import asyncpg
77

8-
from timvt.settings import (
9-
DATABASE_URL,
10-
DB_MAX_CONN_SIZE,
11-
DB_MAX_INACTIVE_CONN_LIFETIME,
12-
DB_MAX_QUERIES,
13-
DB_MIN_CONN_SIZE,
14-
)
8+
from timvt.settings import PostgresSettings
159

1610
from fastapi import FastAPI
1711

12+
pg_settings = PostgresSettings()
13+
1814

1915
async def table_index(db_pool: asyncpg.BuildPgPool) -> Sequence:
2016
"""Fetch Table index."""
@@ -95,11 +91,11 @@ async def table_index(db_pool: asyncpg.BuildPgPool) -> Sequence:
9591
async def connect_to_db(app: FastAPI) -> None:
9692
"""Connect."""
9793
app.state.pool = await asyncpg.create_pool_b(
98-
DATABASE_URL,
99-
min_size=DB_MIN_CONN_SIZE,
100-
max_size=DB_MAX_CONN_SIZE,
101-
max_queries=DB_MAX_QUERIES,
102-
max_inactive_connection_lifetime=DB_MAX_INACTIVE_CONN_LIFETIME,
94+
pg_settings.database_url,
95+
min_size=pg_settings.db_min_conn_size,
96+
max_size=pg_settings.db_max_conn_size,
97+
max_queries=pg_settings.db_max_queries,
98+
max_inactive_connection_lifetime=pg_settings.db_max_inactive_conn_lifetime,
10399
)
104100
app.state.table_catalog = await table_index(app.state.pool)
105101

timvt/layer.py

+12-14
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,9 @@
1111
from pydantic import BaseModel, Field, root_validator
1212

1313
from timvt.errors import MissingEPSGCode
14-
from timvt.settings import (
15-
DEFAULT_MAXZOOM,
16-
DEFAULT_MINZOOM,
17-
MAX_FEATURES_PER_TILE,
18-
TILE_BUFFER,
19-
TILE_RESOLUTION,
20-
)
14+
from timvt.settings import TileSettings
15+
16+
tile_settings = TileSettings()
2117

2218

2319
class Layer(BaseModel, metaclass=abc.ABCMeta):
@@ -34,8 +30,8 @@ class Layer(BaseModel, metaclass=abc.ABCMeta):
3430

3531
id: str
3632
bounds: List[float] = [-180, -90, 180, 90]
37-
minzoom: int = DEFAULT_MINZOOM
38-
maxzoom: int = DEFAULT_MAXZOOM
33+
minzoom: int = tile_settings.default_minzoom
34+
maxzoom: int = tile_settings.default_maxzoom
3935
tileurl: Optional[str]
4036

4137
@abc.abstractmethod
@@ -98,18 +94,20 @@ async def get_tile(
9894
bbox = tms.xy_bounds(tile)
9995

10096
limit = kwargs.get(
101-
"limit", str(MAX_FEATURES_PER_TILE)
97+
"limit", str(tile_settings.max_features_per_tile)
10298
) # Number of features to write to a tile.
103-
limit = min(int(limit), MAX_FEATURES_PER_TILE)
99+
limit = min(int(limit), tile_settings.max_features_per_tile)
104100
if limit == -1:
105-
limit = MAX_FEATURES_PER_TILE
101+
limit = tile_settings.max_features_per_tile
106102

107103
columns = kwargs.get(
108104
"columns"
109105
) # Comma-seprated list of properties (column's name) to include in the tile
110-
resolution = kwargs.get("resolution", str(TILE_RESOLUTION)) # Tile's resolution
106+
resolution = kwargs.get(
107+
"resolution", str(tile_settings.tile_resolution)
108+
) # Tile's resolution
111109
buffer = kwargs.get(
112-
"buffer", str(TILE_BUFFER)
110+
"buffer", str(tile_settings.tile_buffer)
113111
) # Size of extra data to add for a tile.
114112

115113
# create list of columns to return

timvt/main.py

+6-7
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""TiVTiler app."""
22

3-
from timvt import settings
43
from timvt.db import close_db_connection, connect_to_db
54
from timvt.factory import TMSFactory, VectorTilerFactory
5+
from timvt.settings import ApiSettings
66
from timvt.version import __version__ as timvt_version
77

88
from fastapi import FastAPI, Request
@@ -19,22 +19,21 @@
1919

2020

2121
templates = Jinja2Templates(directory=str(resources_files(__package__) / "templates")) # type: ignore
22-
22+
settings = ApiSettings()
2323

2424
# Create TiVTiler Application.
2525
app = FastAPI(
26-
title=settings.APP_NAME,
26+
title=settings.name,
2727
description="A lightweight PostGIS vector tile server.",
2828
version=timvt_version,
29-
debug=settings.DEBUG,
29+
debug=settings.debug,
3030
)
3131

3232
# Setup CORS.
33-
if settings.CORS_ORIGINS:
34-
origins = [origin.strip() for origin in settings.CORS_ORIGINS.split(",")]
33+
if settings.cors_origins:
3534
app.add_middleware(
3635
CORSMiddleware,
37-
allow_origins=origins,
36+
allow_origins=settings.cors_origins,
3837
allow_credentials=True,
3938
allow_methods=["GET"],
4039
allow_headers=["*"],

timvt/settings.py

+117-39
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,123 @@
11
"""
22
TiMVT config.
33
4-
TiVTiler uses starlette.config to either get settings from `.env` or environment variables
5-
see: https://www.starlette.io/config/
4+
TiVTiler uses pydantic.BaseSettings to either get settings from `.env` or environment variables
5+
see: https://pydantic-docs.helpmanual.io/usage/settings/
66
77
"""
8+
from functools import lru_cache
9+
from typing import Any, Dict, Optional
810

9-
from starlette.config import Config
10-
11-
config = Config(".env")
12-
13-
# API config
14-
APP_NAME = config("APP_NAME", cast=str, default="TiMVT")
15-
DEBUG = config("DEBUG", cast=bool, default=False)
16-
CORS_ORIGINS = config("CORS_ORIGINS", cast=str, default="*")
17-
18-
# Database config
19-
DATABASE_URL = config("DATABASE_URL", cast=str, default="")
20-
if not DATABASE_URL:
21-
POSTGRES_USER = config("POSTGRES_USER", cast=str)
22-
POSTGRES_PASS = config("POSTGRES_PASS", cast=str)
23-
POSTGRES_DBNAME = config("POSTGRES_DBNAME", cast=str)
24-
POSTGRES_PORT = config("POSTGRES_PORT", cast=str)
25-
POSTGRES_HOST = config("POSTGRES_HOST", cast=str)
26-
27-
DATABASE_URL = (
28-
f"postgresql://{POSTGRES_USER}:"
29-
f"{POSTGRES_PASS}@{POSTGRES_HOST}:"
30-
f"{POSTGRES_PORT}/{POSTGRES_DBNAME}"
31-
)
32-
33-
DB_MIN_CONN_SIZE = config("DB_MIN_CONN_SIZE", cast=int, default=1)
34-
DB_MAX_CONN_SIZE = config("DB_MAX_CONN_SIZE", cast=int, default=10)
35-
DB_MAX_QUERIES = config("DB_MAX_QUERIES", cast=int, default=50000)
36-
DB_MAX_INACTIVE_CONN_LIFETIME = config(
37-
"DB_MAX_INACTIVE_CONN_LIFETIME", cast=float, default=300.0
38-
)
39-
40-
# Tile / Table config
41-
TILE_RESOLUTION = config("TILE_RESOLUTION", cast=int, default=4096)
42-
TILE_BUFFER = config("TILE_BUFFER", cast=int, default=256)
43-
MAX_FEATURES_PER_TILE = config("MAX_FEATURES_PER_TILE", cast=int, default=10000)
44-
DEFAULT_MINZOOM = config("DEFAULT_MINZOOM", cast=int, default=0)
45-
DEFAULT_MAXZOOM = config("DEFAULT_MAXZOOM", cast=int, default=22)
11+
import pydantic
12+
13+
14+
class _ApiSettings(pydantic.BaseSettings):
15+
"""API settings"""
16+
17+
name: str = "TiMVT"
18+
cors_origins: str = "*"
19+
debug: bool = False
20+
21+
@pydantic.validator("cors_origins")
22+
def parse_cors_origin(cls, v):
23+
"""Parse CORS origins."""
24+
return [origin.strip() for origin in v.split(",")]
25+
26+
class Config:
27+
"""model config"""
28+
29+
env_prefix = "TIMVT_"
30+
env_file = ".env"
31+
32+
33+
@lru_cache()
34+
def ApiSettings() -> _ApiSettings:
35+
"""
36+
This function returns a cached instance of the APISettings object.
37+
Caching is used to prevent re-reading the environment every time the API settings are used in an endpoint.
38+
If you want to change an environment variable and reset the cache (e.g., during testing), this can be done
39+
using the `lru_cache` instance method `get_api_settings.cache_clear()`.
40+
41+
From https://github.com/dmontagu/fastapi-utils/blob/af95ff4a8195caaa9edaa3dbd5b6eeb09691d9c7/fastapi_utils/api_settings.py#L60-L69
42+
"""
43+
return _ApiSettings()
44+
45+
46+
class _TileSettings(pydantic.BaseSettings):
47+
"""MVT settings"""
48+
49+
tile_resolution: int = 4096
50+
tile_buffer: int = 256
51+
max_features_per_tile: int = 10000
52+
default_minzoom: int = 0
53+
default_maxzoom: int = 22
54+
55+
class Config:
56+
"""model config"""
57+
58+
env_prefix = "TIMVT_"
59+
env_file = ".env"
60+
61+
62+
@lru_cache()
63+
def TileSettings() -> _TileSettings:
64+
"""Cache settings."""
65+
return _TileSettings()
66+
67+
68+
class _PostgresSettings(pydantic.BaseSettings):
69+
"""Postgres-specific API settings.
70+
71+
Attributes:
72+
postgres_user: postgres username.
73+
postgres_pass: postgres password.
74+
postgres_host: hostname for the connection.
75+
postgres_port: database port.
76+
postgres_dbname: database name.
77+
"""
78+
79+
postgres_user: Optional[str]
80+
postgres_pass: Optional[str]
81+
postgres_host: Optional[str]
82+
postgres_port: Optional[str]
83+
postgres_dbname: Optional[str]
84+
85+
database_url: Optional[pydantic.PostgresDsn] = None
86+
87+
db_min_conn_size: int = 1
88+
db_max_conn_size: int = 10
89+
db_max_queries: int = 50000
90+
db_max_inactive_conn_lifetime: float = 300
91+
92+
class Config:
93+
"""model config"""
94+
95+
env_file = ".env"
96+
97+
# https://github.com/tiangolo/full-stack-fastapi-postgresql/blob/master/%7B%7Bcookiecutter.project_slug%7D%7D/backend/app/app/core/config.py#L42
98+
@pydantic.validator("database_url", pre=True)
99+
def assemble_db_connection(cls, v: Optional[str], values: Dict[str, Any]) -> Any:
100+
if isinstance(v, str):
101+
return v
102+
103+
return pydantic.PostgresDsn.build(
104+
scheme="postgresql",
105+
user=values.get("postgres_user"),
106+
password=values.get("postgres_pass"),
107+
host=values.get("postgres_host", ""),
108+
port=values.get("postgres_port", 5432),
109+
path=f"/{values.get('postgres_dbname') or ''}",
110+
)
111+
112+
113+
@lru_cache()
114+
def PostgresSettings() -> _PostgresSettings:
115+
"""
116+
This function returns a cached instance of the APISettings object.
117+
Caching is used to prevent re-reading the environment every time the API settings are used in an endpoint.
118+
If you want to change an environment variable and reset the cache (e.g., during testing), this can be done
119+
using the `lru_cache` instance method `get_api_settings.cache_clear()`.
120+
121+
From https://github.com/dmontagu/fastapi-utils/blob/af95ff4a8195caaa9edaa3dbd5b6eeb09691d9c7/fastapi_utils/api_settings.py#L60-L69
122+
"""
123+
return _PostgresSettings()

0 commit comments

Comments
 (0)