2023-11-06 17:04:17 +05:30
|
|
|
from os import environ
|
|
|
|
import logging
|
|
|
|
from pathlib import Path
|
2023-11-17 11:35:09 +05:30
|
|
|
from typing import Any, Type, Tuple
|
2023-11-06 17:04:17 +05:30
|
|
|
|
2023-12-13 01:25:00 +05:30
|
|
|
from pydantic_settings import (BaseSettings,
|
|
|
|
PydanticBaseSettingsSource,
|
|
|
|
SettingsConfigDict)
|
|
|
|
from pydantic import ConfigDict
|
2023-11-17 11:35:09 +05:30
|
|
|
from pydantic.v1.utils import deep_update
|
|
|
|
from yaml import safe_load
|
|
|
|
|
2023-12-25 15:50:45 +05:30
|
|
|
from gisaf._version import __version__
|
2023-11-17 11:35:09 +05:30
|
|
|
#from sqlalchemy.ext.asyncio.engine import AsyncEngine
|
|
|
|
#from sqlalchemy.orm.session import sessionmaker
|
2023-11-06 17:04:17 +05:30
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
ENV = environ.get('env', 'prod')
|
|
|
|
|
2023-11-17 11:35:09 +05:30
|
|
|
config_files = [
|
|
|
|
Path(Path.cwd().root) / 'etc' / 'gisaf' / ENV,
|
|
|
|
Path.home() / '.local' / 'gisaf' / ENV
|
|
|
|
]
|
|
|
|
|
|
|
|
class DashboardHome(BaseSettings):
|
|
|
|
title: str
|
|
|
|
content_file: str
|
|
|
|
footer_file: str
|
|
|
|
|
|
|
|
class GisafConfig(BaseSettings):
|
|
|
|
title: str
|
|
|
|
windowTitle: str
|
|
|
|
debugLevel: str
|
|
|
|
dashboard_home: DashboardHome
|
|
|
|
redirect: str = ''
|
2023-12-13 01:25:00 +05:30
|
|
|
use_pretty_errors: bool = False
|
2023-11-17 11:35:09 +05:30
|
|
|
|
|
|
|
class SpatialSysRef(BaseSettings):
|
|
|
|
author: str
|
|
|
|
ellps: str
|
|
|
|
k: int
|
|
|
|
lat_0: float
|
|
|
|
lon_0: float
|
|
|
|
no_defs: bool
|
|
|
|
proj: str
|
|
|
|
towgs84: str
|
|
|
|
units: str
|
|
|
|
x_0: float
|
|
|
|
y_0: float
|
|
|
|
|
|
|
|
class RawSurvey(BaseSettings):
|
|
|
|
spatial_sys_ref: SpatialSysRef
|
|
|
|
srid: int
|
|
|
|
|
|
|
|
class Geo(BaseSettings):
|
|
|
|
raw_survey: RawSurvey
|
|
|
|
simplify_geom_factor: int
|
2024-01-07 18:30:25 +05:30
|
|
|
simplify_preserve_topology: bool = False
|
2023-11-17 11:35:09 +05:30
|
|
|
srid: int
|
|
|
|
srid_for_proj: int
|
|
|
|
|
|
|
|
class Flask(BaseSettings):
|
|
|
|
secret_key: str
|
|
|
|
debug: int
|
|
|
|
|
|
|
|
class MQTT(BaseSettings):
|
2023-12-13 01:25:00 +05:30
|
|
|
broker: str = 'localhost'
|
|
|
|
port: int = 1883
|
2023-11-17 11:35:09 +05:30
|
|
|
|
|
|
|
class GisafLive(BaseSettings):
|
|
|
|
hostname: str
|
|
|
|
port: int
|
|
|
|
scheme: str
|
|
|
|
redis: str
|
|
|
|
mqtt: MQTT
|
|
|
|
|
|
|
|
class DefaultSurvey(BaseSettings):
|
|
|
|
surveyor_id: int
|
|
|
|
equipment_id: int
|
|
|
|
|
|
|
|
class Survey(BaseSettings):
|
2023-12-13 01:25:00 +05:30
|
|
|
model_config = ConfigDict(extra='ignore')
|
|
|
|
db_schema_raw: str
|
|
|
|
db_schema: str
|
2023-11-17 11:35:09 +05:30
|
|
|
default: DefaultSurvey
|
|
|
|
|
|
|
|
class Crypto(BaseSettings):
|
|
|
|
secret: str
|
|
|
|
algorithm: str
|
|
|
|
expire: float
|
|
|
|
|
|
|
|
class DB(BaseSettings):
|
|
|
|
uri: str
|
|
|
|
host: str
|
2023-12-23 15:08:42 +05:30
|
|
|
port: int = 5432
|
2023-11-17 11:35:09 +05:30
|
|
|
user: str
|
|
|
|
db: str
|
|
|
|
password: str
|
|
|
|
debug: bool
|
|
|
|
info: bool
|
|
|
|
pool_size: int = 10
|
|
|
|
max_overflow: int = 10
|
2023-12-23 15:08:42 +05:30
|
|
|
echo: bool = False
|
|
|
|
|
|
|
|
def get_sqla_url(self):
|
|
|
|
return f'postgresql+asyncpg://{self.user}:{self.password}@{self.host}:{self.port}/{self.db}'
|
|
|
|
|
|
|
|
def get_pg_url(self):
|
|
|
|
return f'postgresql://{self.user}:{self.password}@{self.host}:{self.port}/{self.db}'
|
|
|
|
|
2023-11-17 11:35:09 +05:30
|
|
|
|
|
|
|
class Log(BaseSettings):
|
|
|
|
level: str
|
|
|
|
|
|
|
|
class OGCAPILicense(BaseSettings):
|
|
|
|
name: str
|
|
|
|
url: str
|
|
|
|
|
|
|
|
class OGCAPIProvider(BaseSettings):
|
|
|
|
name: str
|
|
|
|
url: str
|
|
|
|
|
|
|
|
class OGCAPIServerContact(BaseSettings):
|
|
|
|
name: str
|
|
|
|
address: str
|
|
|
|
city: str
|
|
|
|
stateorprovince: str
|
|
|
|
postalcode: int
|
|
|
|
country: str
|
|
|
|
email: str
|
|
|
|
|
|
|
|
class OGCAPIIdentification(BaseSettings):
|
|
|
|
title: str
|
|
|
|
description: str
|
|
|
|
keywords: list[str]
|
|
|
|
keywords_type: str
|
|
|
|
terms_of_service: str
|
|
|
|
url: str
|
|
|
|
|
|
|
|
class OGCAPIMetadata(BaseSettings):
|
|
|
|
identification: OGCAPIIdentification
|
|
|
|
license: OGCAPILicense
|
|
|
|
provider: OGCAPIProvider
|
|
|
|
contact: OGCAPIServerContact
|
2023-11-06 17:04:17 +05:30
|
|
|
|
2023-11-17 11:35:09 +05:30
|
|
|
class ServerBind(BaseSettings):
|
|
|
|
host: str
|
|
|
|
port: int
|
|
|
|
|
|
|
|
class OGCAPIServerMap(BaseSettings):
|
|
|
|
url: str
|
|
|
|
attribution: str
|
|
|
|
|
|
|
|
class OGCAPIServer(BaseSettings):
|
|
|
|
bind: ServerBind
|
|
|
|
url: str
|
|
|
|
mimetype: str
|
|
|
|
encoding: str
|
|
|
|
language: str
|
|
|
|
pretty_print: bool
|
|
|
|
limit: int
|
|
|
|
map: OGCAPIServerMap
|
|
|
|
|
|
|
|
class OGCAPI(BaseSettings):
|
|
|
|
base_url: str
|
|
|
|
bbox: list[float]
|
|
|
|
log: Log
|
|
|
|
metadata: OGCAPIMetadata
|
|
|
|
server: OGCAPIServer
|
|
|
|
|
2023-11-19 12:13:39 +05:30
|
|
|
class TileServer(BaseSettings):
|
2023-12-13 01:25:00 +05:30
|
|
|
baseDir: str
|
|
|
|
useRequestUrl: bool = False
|
|
|
|
spriteBaseDir: str
|
|
|
|
spriteUrl: str
|
|
|
|
spriteBaseUrl: str
|
2023-11-19 12:13:39 +05:30
|
|
|
openMapTilesKey: str | None = None
|
|
|
|
|
2023-11-17 11:35:09 +05:30
|
|
|
class Map(BaseSettings):
|
2023-11-19 12:13:39 +05:30
|
|
|
tileServer: TileServer | None = None
|
2023-11-17 11:35:09 +05:30
|
|
|
zoom: int
|
|
|
|
pitch: int
|
|
|
|
lat: float
|
|
|
|
lng: float
|
|
|
|
bearing: float
|
|
|
|
style: str
|
|
|
|
opacity: float
|
|
|
|
attribution: str
|
|
|
|
status: list[str]
|
|
|
|
defaultStatus: list[str] # FIXME: should be str
|
|
|
|
tagKeys: list[str]
|
|
|
|
|
|
|
|
class Measures(BaseSettings):
|
|
|
|
defaultStore: str
|
|
|
|
|
|
|
|
class BasketDefault(BaseSettings):
|
|
|
|
surveyor: str
|
|
|
|
equipment: str
|
|
|
|
project: str | None
|
|
|
|
status: str
|
|
|
|
store: str | None
|
|
|
|
|
|
|
|
class BasketOldDef(BaseSettings):
|
|
|
|
base_dir: str
|
|
|
|
|
|
|
|
class Basket(BaseSettings):
|
|
|
|
base_dir: str
|
|
|
|
default: BasketDefault
|
|
|
|
|
|
|
|
class Plot(BaseSettings):
|
|
|
|
maxDataSize: int
|
|
|
|
|
|
|
|
class Dashboard(BaseSettings):
|
|
|
|
base_source_url: str
|
|
|
|
base_storage_dir: str
|
2024-01-09 17:46:18 +05:30
|
|
|
base_storage_url: str = '/dashboard-attachment/'
|
2023-11-17 11:35:09 +05:30
|
|
|
|
|
|
|
class Widgets(BaseSettings):
|
|
|
|
footer: str
|
|
|
|
|
|
|
|
class Admin(BaseSettings):
|
|
|
|
basket: Basket
|
|
|
|
|
|
|
|
class Attachments(BaseSettings):
|
|
|
|
base_dir: str
|
|
|
|
|
|
|
|
class Job(BaseSettings):
|
|
|
|
id: str
|
|
|
|
func: str
|
|
|
|
trigger: str
|
|
|
|
minutes: int | None = 0
|
|
|
|
seconds: int | None = 0
|
|
|
|
|
2023-12-21 10:51:31 +05:30
|
|
|
class Crs(BaseSettings):
|
|
|
|
'''
|
|
|
|
Handy definitions for crs-es
|
|
|
|
'''
|
|
|
|
db: str
|
|
|
|
geojson: str
|
|
|
|
for_proj: str
|
|
|
|
survey: str
|
|
|
|
web_mercator: str
|
|
|
|
|
2023-11-17 11:35:09 +05:30
|
|
|
class Config(BaseSettings):
|
2023-12-13 01:25:00 +05:30
|
|
|
model_config = SettingsConfigDict(
|
|
|
|
#env_prefix='gisaf_',
|
|
|
|
env_nested_delimiter='__',
|
|
|
|
)
|
|
|
|
|
2023-11-17 11:35:09 +05:30
|
|
|
@classmethod
|
|
|
|
def settings_customise_sources(
|
|
|
|
cls,
|
|
|
|
settings_cls: Type[BaseSettings],
|
|
|
|
init_settings: PydanticBaseSettingsSource,
|
|
|
|
env_settings: PydanticBaseSettingsSource,
|
|
|
|
dotenv_settings: PydanticBaseSettingsSource,
|
|
|
|
file_secret_settings: PydanticBaseSettingsSource,
|
|
|
|
) -> Tuple[PydanticBaseSettingsSource, ...]:
|
|
|
|
return env_settings, init_settings, file_secret_settings, config_file_settings
|
|
|
|
|
2023-12-21 10:51:31 +05:30
|
|
|
# def __init__(self, **kwargs):
|
|
|
|
# super().__init__(**kwargs)
|
|
|
|
# self.crs = {
|
|
|
|
# 'db': f'epsg:{conf.srid}',
|
|
|
|
# 'geojson': f'epsg:{conf.geojson_srid}',
|
|
|
|
# 'for_proj': f'epsg:{conf.srid_for_proj}',
|
|
|
|
# 'survey': f'epsg:{conf.raw_survey_srid}',
|
|
|
|
# 'web_mercator': 'epsg:3857',
|
|
|
|
# }
|
|
|
|
|
2023-11-17 11:35:09 +05:30
|
|
|
admin: Admin
|
|
|
|
attachments: Attachments
|
|
|
|
basket: BasketOldDef
|
2023-12-21 10:51:31 +05:30
|
|
|
# crs: Crs
|
2023-11-17 11:35:09 +05:30
|
|
|
crypto: Crypto
|
|
|
|
dashboard: Dashboard
|
|
|
|
db: DB
|
|
|
|
flask: Flask
|
|
|
|
geo: Geo
|
|
|
|
gisaf: GisafConfig
|
|
|
|
gisaf_live: GisafLive
|
|
|
|
jobs: list[Job]
|
|
|
|
map: Map
|
|
|
|
measures: Measures
|
|
|
|
ogcapi: OGCAPI
|
|
|
|
plot: Plot
|
|
|
|
plugins: dict[str, dict[str, Any]]
|
|
|
|
survey: Survey
|
2023-11-06 17:04:17 +05:30
|
|
|
version: str
|
2023-11-17 11:35:09 +05:30
|
|
|
weather_station: dict[str, dict[str, Any]]
|
|
|
|
widgets: Widgets
|
|
|
|
#engine: AsyncEngine
|
|
|
|
#session_maker: sessionmaker
|
|
|
|
|
2023-12-21 10:51:31 +05:30
|
|
|
@property
|
|
|
|
def crs(self) -> Crs:
|
|
|
|
return Crs(
|
|
|
|
db=f'epsg:{self.geo.srid}',
|
|
|
|
geojson=f'epsg:{self.geo.srid}',
|
|
|
|
for_proj=f'epsg:{self.geo.srid_for_proj}',
|
|
|
|
survey=f'epsg:{self.geo.raw_survey.srid}',
|
|
|
|
web_mercator='epsg:3857',
|
|
|
|
)
|
2023-11-17 11:35:09 +05:30
|
|
|
|
|
|
|
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
|
2023-11-06 17:04:17 +05:30
|
|
|
|
|
|
|
|
2023-11-17 11:35:09 +05:30
|
|
|
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
|
2023-11-06 17:04:17 +05:30
|
|
|
|
|
|
|
|
2023-11-17 11:35:09 +05:30
|
|
|
conf = Config(version=__version__)
|
2023-11-06 17:04:17 +05:30
|
|
|
|
2023-11-17 11:35:09 +05:30
|
|
|
# def set_app_config(app) -> None:
|
|
|
|
# raw_configs = []
|
|
|
|
# with open(Path(__file__).parent / 'defaults.yml') as cf:
|
|
|
|
# raw_configs.append(cf.read())
|
|
|
|
# for cf_path in (
|
|
|
|
# Path(Path.cwd().root) / 'etc' / 'gisaf' / ENV,
|
|
|
|
# Path.home() / '.local' / 'gisaf' / ENV
|
|
|
|
# ):
|
|
|
|
# try:
|
|
|
|
# with open(cf_path.with_suffix('.yml')) as cf:
|
|
|
|
# raw_configs.append(cf.read())
|
|
|
|
# except FileNotFoundError:
|
|
|
|
# pass
|
2023-11-06 17:04:17 +05:30
|
|
|
|
2023-11-17 11:35:09 +05:30
|
|
|
# yaml_config = safe_load('\n'.join(raw_configs))
|
2023-11-06 17:04:17 +05:30
|
|
|
|
2023-11-17 11:35:09 +05:30
|
|
|
# conf.app = yaml_config['app']
|
|
|
|
# conf.postgres = yaml_config['postgres']
|
|
|
|
# conf.storage = yaml_config['storage']
|
|
|
|
# conf.map = yaml_config['map']
|
|
|
|
# conf.security = yaml_config['security']
|
|
|
|
# # create_dirs()
|
2023-11-06 17:04:17 +05:30
|
|
|
|
|
|
|
|
|
|
|
# def create_dirs():
|
|
|
|
# """
|
|
|
|
# Create the directories needed for a proper functioning of the app
|
|
|
|
# """
|
|
|
|
# ## Avoid circular imports
|
|
|
|
# from treetrail.api_v1 import attachment_types
|
|
|
|
# for type in attachment_types:
|
|
|
|
# base_dir = Path(conf.storage['root_attachment_path']) / type
|
|
|
|
# base_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# logger.info(f'Cache dir: {get_cache_dir()}')
|
|
|
|
# get_cache_dir().mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
|
2023-11-17 11:35:09 +05:30
|
|
|
# def get_cache_dir() -> Path:
|
|
|
|
# return Path(conf.storage['root_cache_path'])
|