.snipenv configuration
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Read configuration from: - environment - .snipenv - .snipenv.[SNIP_STAGE]
This commit is contained in:
parent
95683625da
commit
9d10f4f5b9
8 changed files with 184 additions and 59 deletions
1
.snipenv
Normal file
1
.snipenv
Normal file
|
@ -0,0 +1 @@
|
||||||
|
SNIP_STAGE=local
|
9
.snipenv.local
Normal file
9
.snipenv.local
Normal 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
|
1
Pipfile
1
Pipfile
|
@ -14,6 +14,7 @@ flask = "*"
|
||||||
flask-sqlalchemy = "*"
|
flask-sqlalchemy = "*"
|
||||||
base58 = "*"
|
base58 = "*"
|
||||||
flask-wtf = "*"
|
flask-wtf = "*"
|
||||||
|
pydantic = "*"
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
python_version = "3.8"
|
python_version = "3.8"
|
||||||
|
|
36
Pipfile.lock
generated
36
Pipfile.lock
generated
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "5dc5b4443b620da8c4ee4052dea107005a6edec09fe78eae470daecc8d2fc570"
|
"sha256": "85510df90332ce819c847463e8d8ef3410d9d43774844df83e1a82d677498586"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
@ -107,6 +107,34 @@
|
||||||
],
|
],
|
||||||
"version": "==1.1.1"
|
"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": {
|
"sqlalchemy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:009e8388d4d551a2107632921320886650b46332f61dc935e70c8bcf37d8e0d6",
|
"sha256:009e8388d4d551a2107632921320886650b46332f61dc935e70c8bcf37d8e0d6",
|
||||||
|
@ -204,11 +232,11 @@
|
||||||
},
|
},
|
||||||
"ipython": {
|
"ipython": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2e22c1f74477b5106a6fb301c342ab8c64bb75d702e350f05a649e8cb40a0fb8",
|
"sha256:c987e8178ced651532b3b1ff9965925bfd445c279239697052561a9ab806d28f",
|
||||||
"sha256:a331e78086001931de9424940699691ad49dfb457cea31f5471eae7b78222d5e"
|
"sha256:cbb2ef3d5961d44e6a963b9817d4ea4e1fa2eb589c371a470fed14d8d40cbd6a"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==7.18.1"
|
"version": "==7.19.0"
|
||||||
},
|
},
|
||||||
"ipython-genutils": {
|
"ipython-genutils": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
|
@ -1,13 +1,45 @@
|
||||||
|
import os
|
||||||
from flask import Flask
|
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__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
from . import config
|
app.config.update(
|
||||||
config.configure("local")
|
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
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
db = SQLAlchemy(app)
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
|
||||||
|
##############
|
||||||
|
# Setup snip #
|
||||||
|
##############
|
||||||
from . import api
|
from . import api
|
||||||
from . import views
|
from . import views
|
||||||
|
|
4
snip/__main__.py
Normal file
4
snip/__main__.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
from . import app
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run()
|
|
@ -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
|
|
139
snip/config.py
139
snip/config.py
|
@ -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 os
|
||||||
import logging
|
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 = [
|
# Flask settings
|
||||||
"SQLALCHEMY_DATABASE_URI",
|
SNIP_FLASK_SECRET: SecretStr
|
||||||
"SQLALCHEMY_TRACK_MODIFICATIONS",
|
SNIP_FLASK_ENVIRONMENT: Literal['development', 'production'] = 'production'
|
||||||
"SECRET_KEY",
|
SNIP_FLASK_DEBUG: bool = False
|
||||||
"DEBUG",
|
SNIP_FLASK_SKIP_DOTENV: int = 1
|
||||||
"ENV"
|
|
||||||
]
|
|
||||||
|
|
||||||
|
# Snip settings
|
||||||
|
SNIP_STAGE: Optional[str]
|
||||||
|
|
||||||
def find_env():
|
def configure(stage: Optional[str] = None) -> SnipConfig:
|
||||||
env = (os.environ.get("SNIP_ENV")
|
config_dict = {}
|
||||||
or os.environ.get("FLASK_ENV")
|
|
||||||
or default_env)
|
|
||||||
|
|
||||||
if not env:
|
# Read common configuration from .snipenv
|
||||||
raise Exception("Could not determine environment")
|
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):
|
# Read variables from environment
|
||||||
config_file = f"config.{env}.py"
|
config_dict.update(os.environ)
|
||||||
logger.info(f"Config file {config_file}")
|
|
||||||
return config_file
|
|
||||||
|
|
||||||
|
return SnipConfig(**config_dict)
|
||||||
|
|
||||||
def validate(config):
|
def read_base_env() -> dict:
|
||||||
key = config.get("SECRET_KEY")
|
""" Read base configuration from .snipenv if it exists """
|
||||||
if isinstance(key, str):
|
base_env_path = Path(".snipenv")
|
||||||
key = key.encode("utf-8")
|
log.debug(f"Reading .snipenv file from {base_env_path.resolve()}")
|
||||||
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")
|
|
||||||
|
|
||||||
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):
|
try:
|
||||||
logger.info("Configuring snip")
|
stage_env_dict = read_env(stage_env_path)
|
||||||
if not env:
|
log.info(f"Setting stage config from {stage_env_name}")
|
||||||
env = find_env()
|
return stage_env_dict
|
||||||
app.config.from_pyfile(find_config_file(env))
|
except FileNotFoundError:
|
||||||
validate(app.config)
|
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
|
||||||
|
|
Loading…
Reference in a new issue