Config: cleanup, use '__' as ENV nested delimiter; adjust CI test

This commit is contained in:
phil 2024-12-15 01:50:23 +01:00
parent ed3812b0f0
commit 898197209a
2 changed files with 55 additions and 67 deletions

View file

@ -31,4 +31,4 @@ jobs:
run: uv pip install --python=$UV_PROJECT_ENVIRONMENT --no-deps . run: uv pip install --python=$UV_PROJECT_ENVIRONMENT --no-deps .
- name: Run tests (API call) - name: Run tests (API call)
run: GISAF_DB_HOST=gisaf-database pytest -s tests/basic.py run: GISAF__DB__HOST=gisaf-database pytest -s tests/basic.py

View file

@ -3,27 +3,29 @@ import logging
from pathlib import Path from pathlib import Path
from typing import Any, Type, Tuple from typing import Any, Type, Tuple
from yaml import safe_load from yaml import safe_load
from importlib.metadata import version
from xdg import BaseDirectory from xdg import BaseDirectory
from pydantic import BaseModel
from pydantic_settings import ( from pydantic_settings import (
BaseSettings, BaseSettings,
PydanticBaseSettingsSource, PydanticBaseSettingsSource,
SettingsConfigDict, SettingsConfigDict,
YamlConfigSettingsSource,
) )
from pydantic.v1.utils import deep_update
from importlib.metadata import version
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
ENV = environ.get("env", "prod") ENV = environ.get("env", "prod")
config_files = [ config_files = [
Path(Path.cwd().root) / "etc" / "gisaf" / ENV, Path(BaseDirectory.xdg_config_home) / "gisaf" / f"{ENV}.yaml",
Path(BaseDirectory.xdg_config_home) / "gisaf" / ENV, Path(BaseDirectory.xdg_config_home) / "gisaf" / f"{ENV}.yml",
Path(Path.cwd().root) / "etc" / "gisaf" / f"{ENV}.yaml",
Path(Path.cwd().root) / "etc" / "gisaf" / f"{ENV}.yml",
] ]
class DashboardHome(BaseSettings): class DashboardHome(BaseModel):
title: str = "Gisaf - home/dashboards" title: str = "Gisaf - home/dashboards"
content_file: Path = ( content_file: Path = (
Path(Path.cwd().root) / "etc" / "gisaf" / "dashboard_home_content.html" Path(Path.cwd().root) / "etc" / "gisaf" / "dashboard_home_content.html"
@ -33,7 +35,7 @@ class DashboardHome(BaseSettings):
) )
class GisafConfig(BaseSettings): class GisafConfig(BaseModel):
title: str = "Gisaf" title: str = "Gisaf"
windowTitle: str = "Gisaf" windowTitle: str = "Gisaf"
debugLevel: str = "INFO" debugLevel: str = "INFO"
@ -42,7 +44,7 @@ class GisafConfig(BaseSettings):
use_pretty_errors: bool = False use_pretty_errors: bool = False
class SpatialSysRef(BaseSettings): class SpatialSysRef(BaseModel):
author: str = "AVSM" author: str = "AVSM"
ellps: str = "WGS84" ellps: str = "WGS84"
k: int = 1 k: int = 1
@ -56,12 +58,12 @@ class SpatialSysRef(BaseSettings):
y_0: float = 1328608.994 y_0: float = 1328608.994
class RawSurvey(BaseSettings): class RawSurvey(BaseModel):
spatial_sys_ref: SpatialSysRef = SpatialSysRef() spatial_sys_ref: SpatialSysRef = SpatialSysRef()
srid: int = 910001 srid: int = 910001
class Geo(BaseSettings): class Geo(BaseModel):
raw_survey: RawSurvey = RawSurvey() raw_survey: RawSurvey = RawSurvey()
simplify_geom_factor: int = 10000000 simplify_geom_factor: int = 10000000
simplify_preserve_topology: bool = False simplify_preserve_topology: bool = False
@ -69,17 +71,17 @@ class Geo(BaseSettings):
srid_for_proj: int = 32644 srid_for_proj: int = 32644
# class Flask(BaseSettings): # class Flask(BaseModel):
# secret_key: str # secret_key: str
# debug: int # debug: int
class MQTT(BaseSettings): class MQTT(BaseModel):
broker: str = "localhost" broker: str = "localhost"
port: int = 1883 port: int = 1883
class GisafLive(BaseSettings): class GisafLive(BaseModel):
hostname: str = "localhost" hostname: str = "localhost"
port: int = 80 port: int = 80
scheme: str = "http" scheme: str = "http"
@ -87,24 +89,24 @@ class GisafLive(BaseSettings):
mqtt: MQTT = MQTT() mqtt: MQTT = MQTT()
class DefaultSurvey(BaseSettings): class DefaultSurvey(BaseModel):
surveyor_id: int = 1 surveyor_id: int = 1
equipment_id: int = 1 equipment_id: int = 1
class Survey(BaseSettings): class Survey(BaseModel):
db_schema_raw: str = "raw_survey" db_schema_raw: str = "raw_survey"
db_schema: str = "survey" db_schema: str = "survey"
default: DefaultSurvey = DefaultSurvey() default: DefaultSurvey = DefaultSurvey()
class Crypto(BaseSettings): class Crypto(BaseModel):
secret: str = "Gisaf big secret" secret: str = "Gisaf big secret"
algorithm: str = "HS256" algorithm: str = "HS256"
expire: float = 21600 expire: float = 21600
class DB(BaseSettings): class DB(BaseModel):
# uri: str # uri: str
host: str = "localhost" host: str = "localhost"
port: int = 5432 port: int = 5432
@ -124,21 +126,21 @@ class DB(BaseSettings):
return f"postgresql://{self.user}:{self.password}@{self.host}:{self.port}/{self.db}" return f"postgresql://{self.user}:{self.password}@{self.host}:{self.port}/{self.db}"
class Log(BaseSettings): class Log(BaseModel):
level: str = "WARNING" level: str = "WARNING"
class OGCAPILicense(BaseSettings): class OGCAPILicense(BaseModel):
name: str = "CC-BY 4.0 license" name: str = "CC-BY 4.0 license"
url: str = "https://creativecommons.org/licenses/by/4.0/" url: str = "https://creativecommons.org/licenses/by/4.0/"
class OGCAPIProvider(BaseSettings): class OGCAPIProvider(BaseModel):
name: str = "Organization Name" name: str = "Organization Name"
url: str = "https://pygeoapi.io" url: str = "https://pygeoapi.io"
class OGCAPIServerContact(BaseSettings): class OGCAPIServerContact(BaseModel):
name: str = "Lastname, Firstname" name: str = "Lastname, Firstname"
position: str = "Position Title" position: str = "Position Title"
address: str = "Mailing Address" address: str = "Mailing Address"
@ -150,7 +152,7 @@ class OGCAPIServerContact(BaseSettings):
url: str | None = None url: str | None = None
class OGCAPIIdentification(BaseSettings): class OGCAPIIdentification(BaseModel):
title: str = "pygeoapi default instance" title: str = "pygeoapi default instance"
description: str = "pygeoapi provides an API to geospatial data" description: str = "pygeoapi provides an API to geospatial data"
keywords: list[str] = ["geospatial", "data", "api"] keywords: list[str] = ["geospatial", "data", "api"]
@ -159,26 +161,26 @@ class OGCAPIIdentification(BaseSettings):
url: str = "http://example.org" url: str = "http://example.org"
class OGCAPIMetadata(BaseSettings): class OGCAPIMetadata(BaseModel):
identification: OGCAPIIdentification = OGCAPIIdentification() identification: OGCAPIIdentification = OGCAPIIdentification()
license: OGCAPILicense = OGCAPILicense() license: OGCAPILicense = OGCAPILicense()
provider: OGCAPIProvider = OGCAPIProvider() provider: OGCAPIProvider = OGCAPIProvider()
contact: OGCAPIServerContact = OGCAPIServerContact() contact: OGCAPIServerContact = OGCAPIServerContact()
class ServerBind(BaseSettings): class ServerBind(BaseModel):
host: str = "0.0.0.0" host: str = "0.0.0.0"
port: int = 5000 port: int = 5000
class OGCAPIServerMap(BaseSettings): class OGCAPIServerMap(BaseModel):
url: str = "https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png" url: str = "https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png"
attribution: str = ( attribution: str = (
"""<a href="https://wikimediafoundation.org/wiki/Maps_Terms_of_Use">Wikimedia maps</a> | Map data &copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a>""" """<a href="https://wikimediafoundation.org/wiki/Maps_Terms_of_Use">Wikimedia maps</a> | Map data &copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a>"""
) )
class OGCAPIServer(BaseSettings): class OGCAPIServer(BaseModel):
bind: ServerBind = ServerBind() bind: ServerBind = ServerBind()
url: str = "https://example.org/ogcapi" url: str = "https://example.org/ogcapi"
mimetype: str = "application/json; charset=UTF-8" mimetype: str = "application/json; charset=UTF-8"
@ -189,7 +191,7 @@ class OGCAPIServer(BaseSettings):
map: OGCAPIServerMap = OGCAPIServerMap() map: OGCAPIServerMap = OGCAPIServerMap()
class OGCAPI(BaseSettings): class OGCAPI(BaseModel):
base_url: str = "http://example.org/ogcapi" base_url: str = "http://example.org/ogcapi"
bbox: list[float] = [-180, -90, 180, 90] bbox: list[float] = [-180, -90, 180, 90]
log: Log = Log() log: Log = Log()
@ -197,7 +199,7 @@ class OGCAPI(BaseSettings):
server: OGCAPIServer = OGCAPIServer() server: OGCAPIServer = OGCAPIServer()
class TileServer(BaseSettings): class TileServer(BaseModel):
baseDir: Path = Path(BaseDirectory.xdg_data_home) / "gisaf" / "mbtiles_files_dir" baseDir: Path = Path(BaseDirectory.xdg_data_home) / "gisaf" / "mbtiles_files_dir"
useRequestUrl: bool = False useRequestUrl: bool = False
spriteBaseDir: Path = ( spriteBaseDir: Path = (
@ -213,7 +215,7 @@ class TileServer(BaseSettings):
self.spriteBaseDir.mkdir(parents=True, exist_ok=True) self.spriteBaseDir.mkdir(parents=True, exist_ok=True)
class Map(BaseSettings): class Map(BaseModel):
tileServer: TileServer = TileServer() tileServer: TileServer = TileServer()
zoom: int = 14 zoom: int = 14
pitch: int = 45 pitch: int = 45
@ -228,11 +230,11 @@ class Map(BaseSettings):
tagKeys: list[str] = ["source"] tagKeys: list[str] = ["source"]
class Measures(BaseSettings): class Measures(BaseModel):
defaultStore: str | None = None defaultStore: str | None = None
class BasketDefault(BaseSettings): class BasketDefault(BaseModel):
surveyor: str = "Default surveyor" surveyor: str = "Default surveyor"
equipment: str = "Default equipment" equipment: str = "Default equipment"
project: str = "Default project" project: str = "Default project"
@ -240,40 +242,40 @@ class BasketDefault(BaseSettings):
store: str | None = None store: str | None = None
# class BasketOldDef(BaseSettings): # class BasketOldDef(BaseModel):
# base_dir: str # base_dir: str
class Basket(BaseSettings): class Basket(BaseModel):
base_dir: str = "/var/local/gisaf/baskets" base_dir: str = "/var/local/gisaf/baskets"
default: BasketDefault = BasketDefault() default: BasketDefault = BasketDefault()
class Plot(BaseSettings): class Plot(BaseModel):
maxDataSize: int = 10000 maxDataSize: int = 10000
class Dashboard(BaseSettings): class Dashboard(BaseModel):
base_source_url: str = "http://url.to.jupyter/lab/tree/" base_source_url: str = "http://url.to.jupyter/lab/tree/"
base_storage_dir: str = "/var/lib/share/gisaf/dashboard" base_storage_dir: str = "/var/lib/share/gisaf/dashboard"
base_storage_url: str = "/dashboard-attachment/" base_storage_url: str = "/dashboard-attachment/"
class Widgets(BaseSettings): class Widgets(BaseModel):
footer: str = ( footer: str = (
"""Generated by <span class='link' onclick="window.open('https://redmine.auroville.org.in/projects/gisaf/')">Gisaf</span>""" """Generated by <span class='link' onclick="window.open('https://redmine.auroville.org.in/projects/gisaf/')">Gisaf</span>"""
) )
class Admin(BaseSettings): class Admin(BaseModel):
basket: Basket = Basket() basket: Basket = Basket()
class Attachments(BaseSettings): class Attachments(BaseModel):
base_dir: str = "/var/local/gisaf/attachments" base_dir: str = "/var/local/gisaf/attachments"
class Job(BaseSettings): class Job(BaseModel):
id: str id: str
func: str func: str
trigger: str trigger: str
@ -281,7 +283,7 @@ class Job(BaseSettings):
seconds: int | None = 0 seconds: int | None = 0
class Crs(BaseSettings): class Crs(BaseModel):
""" """
Handy definitions for crs-es Handy definitions for crs-es
""" """
@ -295,7 +297,8 @@ class Crs(BaseSettings):
class Config(BaseSettings): class Config(BaseSettings):
model_config = SettingsConfigDict( model_config = SettingsConfigDict(
# env_prefix='gisaf_', env_prefix="GISAF__",
nested_model_default_partial_update=True,
env_nested_delimiter="__", env_nested_delimiter="__",
) )
@ -308,7 +311,15 @@ class Config(BaseSettings):
dotenv_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource,
) -> Tuple[PydanticBaseSettingsSource, ...]: ) -> Tuple[PydanticBaseSettingsSource, ...]:
return env_settings, init_settings, file_secret_settings, config_file_settings # type: ignore configs = [
YamlConfigSettingsSource(settings_cls, yaml_file=cf) for cf in config_files
]
return (
env_settings,
init_settings,
file_secret_settings,
*configs,
)
# def __init__(self, **kwargs): # def __init__(self, **kwargs):
# super().__init__(**kwargs) # super().__init__(**kwargs)
@ -353,31 +364,8 @@ class Config(BaseSettings):
) )
def config_file_settings() -> dict[str, Any]:
config: dict[str, Any] = {}
for p in config_files:
for suffix in {".yaml", ".yml"}:
path = p.with_suffix(suffix)
if not path.is_file():
logger.info(f"No file found at `{path.resolve()}`")
continue
logger.debug(f"Reading config file `{path.resolve()}`")
if path.suffix in {".yaml", ".yml"}:
config = deep_update(config, load_yaml(path))
else:
logger.info(f"Unknown config file extension `{path.suffix}`")
return config
def load_yaml(path: Path) -> dict[str, Any]:
with Path(path).open("r") as f:
config = safe_load(f)
if not isinstance(config, dict):
raise TypeError(f"Config file has no top-level mapping: {path}")
return config
conf = Config() conf = Config()
breakpoint()
# def set_app_config(app) -> None: # def set_app_config(app) -> None:
# raw_configs = [] # raw_configs = []