.snipenv configuration
All checks were successful
continuous-integration/drone/push Build is passing

Read configuration from:
- environment
- .snipenv
- .snipenv.[SNIP_STAGE]
This commit is contained in:
Armin Friedl 2020-11-08 14:20:41 +01:00
parent 95683625da
commit 9d10f4f5b9
8 changed files with 184 additions and 59 deletions

1
.snipenv Normal file
View file

@ -0,0 +1 @@
SNIP_STAGE=local

9
.snipenv.local Normal file
View file

@ -0,0 +1,9 @@
# Database
SNIP_DATABASE=sqlite
SNIP_DATABASE_URI=sqlite:////tmp/test.db
SNIP_DATABASE_TRACK_MODIFICATIONS=False
# Flask
SNIP_FLASK_SECRET=secret
SNIP_FLASK_ENVIRONMENT=development
SNIP_FLASK_DEBUG=True

View file

@ -14,6 +14,7 @@ flask = "*"
flask-sqlalchemy = "*"
base58 = "*"
flask-wtf = "*"
pydantic = "*"
[requires]
python_version = "3.8"

36
Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "5dc5b4443b620da8c4ee4052dea107005a6edec09fe78eae470daecc8d2fc570"
"sha256": "85510df90332ce819c847463e8d8ef3410d9d43774844df83e1a82d677498586"
},
"pipfile-spec": 6,
"requires": {
@ -107,6 +107,34 @@
],
"version": "==1.1.1"
},
"pydantic": {
"hashes": [
"sha256:01f0291f4951580f320f7ae3f2ecaf0044cdebcc9b45c5f882a7e84453362420",
"sha256:0fe8b45d31ae53d74a6aa0bf801587bd49970070eac6a6326f9fa2a302703b8a",
"sha256:2182ba2a9290964b278bcc07a8d24207de709125d520efec9ad6fa6f92ee058d",
"sha256:2c1673633ad1eea78b1c5c420a47cd48717d2ef214c8230d96ca2591e9e00958",
"sha256:388c0c26c574ff49bad7d0fd6ed82fbccd86a0473fa3900397d3354c533d6ebb",
"sha256:4ba6b903e1b7bd3eb5df0e78d7364b7e831ed8b4cd781ebc3c4f1077fbcb72a4",
"sha256:6665f7ab7fbbf4d3c1040925ff4d42d7549a8c15fe041164adfe4fc2134d4cce",
"sha256:95d4410c4e429480c736bba0db6cce5aaa311304aea685ebcf9ee47571bfd7c8",
"sha256:a2fc7bf77ed4a7a961d7684afe177ff59971828141e608f142e4af858e07dddc",
"sha256:a3c274c49930dc047a75ecc865e435f3df89715c775db75ddb0186804d9b04d0",
"sha256:ab1d5e4d8de00575957e1c982b951bffaedd3204ddd24694e3baca3332e53a23",
"sha256:b11fc9530bf0698c8014b2bdb3bbc50243e82a7fa2577c8cfba660bcc819e768",
"sha256:b9572c0db13c8658b4a4cb705dcaae6983aeb9842248b36761b3fbc9010b740f",
"sha256:c68b5edf4da53c98bb1ccb556ae8f655575cb2e676aef066c12b08c724a3f1a1",
"sha256:c8200aecbd1fb914e1bd061d71a4d1d79ecb553165296af0c14989b89e90d09b",
"sha256:c9760d1556ec59ff745f88269a8f357e2b7afc75c556b3a87b8dda5bc62da8ba",
"sha256:ce2d452961352ba229fe1e0b925b41c0c37128f08dddb788d0fd73fd87ea0f66",
"sha256:dfaa6ed1d509b5aef4142084206584280bb6e9014f01df931ec6febdad5b200a",
"sha256:e5fece30e80087d9b7986104e2ac150647ec1658c4789c89893b03b100ca3164",
"sha256:f045cf7afb3352a03bc6cb993578a34560ac24c5d004fa33c76efec6ada1361a",
"sha256:f83f679e727742b0c465e7ef992d6da4a7e5268b8edd8fdaf5303276374bef52",
"sha256:fc21a37ff3f545de80b166e1735c4172b41b017948a3fb2d5e2f03c219eac50a"
],
"index": "pypi",
"version": "==1.7.2"
},
"sqlalchemy": {
"hashes": [
"sha256:009e8388d4d551a2107632921320886650b46332f61dc935e70c8bcf37d8e0d6",
@ -204,11 +232,11 @@
},
"ipython": {
"hashes": [
"sha256:2e22c1f74477b5106a6fb301c342ab8c64bb75d702e350f05a649e8cb40a0fb8",
"sha256:a331e78086001931de9424940699691ad49dfb457cea31f5471eae7b78222d5e"
"sha256:c987e8178ced651532b3b1ff9965925bfd445c279239697052561a9ab806d28f",
"sha256:cbb2ef3d5961d44e6a963b9817d4ea4e1fa2eb589c371a470fed14d8d40cbd6a"
],
"index": "pypi",
"version": "==7.18.1"
"version": "==7.19.0"
},
"ipython-genutils": {
"hashes": [

View file

@ -1,13 +1,45 @@
import os
from flask import Flask
###########################
# Read snip configuration #
###########################
from . import config
snip_config = config.configure()
###############
# Setup Flask #
###############
# ENV and DEBUG are special in flask. They must be set to the environment before app init.
# https://flask.palletsprojects.com/en/1.1.x/config/#environment-and-debug-features
os.environ['FLASK_ENV'] = snip_config.SNIP_FLASK_ENVIRONMENT
os.environ['FLASK_DEBUG'] = str(snip_config.SNIP_FLASK_DEBUG)
# FLASK_SKIP_DOTENV currently only read from environment
os.environ['FLASK_SKIP_DOTENV'] = str(snip_config.SNIP_FLASK_SKIP_DOTENV)
app = Flask(__name__)
from . import config
config.configure("local")
app.config.update(
SECRET_KEY = snip_config.SNIP_FLASK_SECRET.get_secret_value(),
SQLALCHEMY_DATABASE_URI = snip_config.SNIP_DATABASE_URI,
SQLALCHEMY_TRACK_MODIFICATIONS = snip_config.SNIP_DATABASE_TRACK_MODIFICATION)
###################
# Setup SQAlchemy #
###################
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy(app)
from . import models
db.create_all()
##############
# Setup snip #
##############
from . import api
from . import views

4
snip/__main__.py Normal file
View file

@ -0,0 +1,4 @@
from . import app
if __name__ == "__main__":
app.run()

View file

@ -1,17 +0,0 @@
# FLASK
FLASK_DEBUG = True
FLASK_ENV = "development"
# elisp:
# (->> (let (res) (dotimes (n 64 res) (setq res (cons (random (math-pow 2 8)) res))))
# (--map (format "\\x%s" it))
# (--reduce (concat acc it))
# (kill-new))
SECRET_KEY = b'\x90\xd6\x07\xa9\x84\x41\x15\x7c\x7b\x96\xd5\xb7\xa2\x52\xc3'\
b'\x33\x3e\x40\xdf\x0a\x64\xdc\x27\x2b\x4a\x48\x7a\x88\xf6\x4c\xce\x9a'\
b'\x5c\x5f\xc5\xc8\xa8\xa3\xbc\x28\xc0\x10\x6d\x54\xbb\x10\x58\x86\x9c'\
b'\x68\x1b\xcc\xad\x68\xd0\xf3\x66\x65\xb5\xf4\x70\x9d\x0e\x3d'
# SQL ALCHEMY
SQLALCHEMY_DATABASE_URI = "sqlite:////tmp/test.db"
SQLALCHEMY_TRACK_MODIFICATIONS = False

View file

@ -1,53 +1,120 @@
"""
Snip Configuration
==================
Configure snip based on settings found in the environment or environment
specific config files. The variables are set in the following order (later
settings override earlier ones):
- .snipenv
- .snipenv.[SNIP_STAGE]
- environment
- config.py (defaults)
SNIP_STAGE is the only special value. It is taken from definitions in the
following order (later settings override earlier ones):
- .snipenv
- environment
- parameter in `config.configure`
If no SNIP_STAGE is found in any of them, no stage specific configuration
(.snipenv.[SNIP_STAGE]) file is taken into account.
All known configuration variables can be found in `SnipConfig`.
"""
import os
import logging
from typing import Literal, Optional
from pathlib import Path
from . import app
from pydantic import BaseModel
from pydantic.types import SecretStr
logger = logging.getLogger(__name__)
log = logging.getLogger(__name__)
default_env = "local"
class SnipConfig(BaseModel):
# Database settings
SNIP_DATABASE: Literal['sqlite', 'postgres', 'mysql']
SNIP_DATABASE_URI: str
SNIP_DATABASE_TRACK_MODIFICATION: bool = False
config_vars = [
"SQLALCHEMY_DATABASE_URI",
"SQLALCHEMY_TRACK_MODIFICATIONS",
"SECRET_KEY",
"DEBUG",
"ENV"
]
# Flask settings
SNIP_FLASK_SECRET: SecretStr
SNIP_FLASK_ENVIRONMENT: Literal['development', 'production'] = 'production'
SNIP_FLASK_DEBUG: bool = False
SNIP_FLASK_SKIP_DOTENV: int = 1
# Snip settings
SNIP_STAGE: Optional[str]
def find_env():
env = (os.environ.get("SNIP_ENV")
or os.environ.get("FLASK_ENV")
or default_env)
def configure(stage: Optional[str] = None) -> SnipConfig:
config_dict = {}
if not env:
raise Exception("Could not determine environment")
# Read common configuration from .snipenv
config_dict.update(read_base_env())
logger.info(f"Using environment {env}")
# bootstrap configuration, try to determine stage first
# stage can be set from parameter or environment
if not stage:
if os.getenv("SNIP_STAGE"):
stage = os.getenv("SNIP_STAGE")
elif config_dict.get("SNIP_STAGE"):
stage = config_dict.get("SNIP_STAGE")
# Read stage configuration from .snipenv.[stage] if set
if stage: config_dict.update(read_stage_env(stage))
else: log.debug("Not using stage configuration")
def find_config_file(env):
config_file = f"config.{env}.py"
logger.info(f"Config file {config_file}")
return config_file
# Read variables from environment
config_dict.update(os.environ)
return SnipConfig(**config_dict)
def validate(config):
key = config.get("SECRET_KEY")
if isinstance(key, str):
key = key.encode("utf-8")
if not isinstance(key, bytes):
raise Exception("Secret key cannot be cast to bytes")
if len(key) < 64: # for blake2b hashing in snipper
raise Exception("Secret key must be 64 bytes long")
def read_base_env() -> dict:
""" Read base configuration from .snipenv if it exists """
base_env_path = Path(".snipenv")
log.debug(f"Reading .snipenv file from {base_env_path.resolve()}")
logger.info("Configuration is valid")
try:
base_env_dict = read_env(base_env_path)
log.info("Setting base config from .snipenv")
return base_env_dict
except FileNotFoundError:
log.debug(".snipenv file not found")
return {}
def read_stage_env(stage: str) -> dict:
""" Reads the .snipenv file of the current stage """
log.info(f"Using stage configuration for {stage}")
stage_env_name = ".snipenv."+stage
stage_env_path = Path(stage_env_name)
log.debug(f"Reading .snipenv file from {stage_env_path.resolve()}")
def configure(env=None):
logger.info("Configuring snip")
if not env:
env = find_env()
app.config.from_pyfile(find_config_file(env))
validate(app.config)
try:
stage_env_dict = read_env(stage_env_path)
log.info(f"Setting stage config from {stage_env_name}")
return stage_env_dict
except FileNotFoundError:
log.error(f"Could not find stage configuration for {stage_env_name} in {os.path.curdir}")
raise
def read_env(env_path: Path) -> dict:
""" Read env file from path into dict """
if not env_path.exists() or not env_path.is_file():
raise FileNotFoundError
env_dict = dict()
with open(env_path) as f:
for (idx, line) in enumerate(f):
# skip empty lines
if not line.strip(): continue
# skip comment lines
if line.startswith('#'): continue
try:
(k, v) = map(str.strip, line.split('=', maxsplit=1))
env_dict[k] = v
except:
log.error(f"Cannot parse {env_path}:{idx}:{line}")
raise
return env_dict