Started working on backend
parent
c3498b5584
commit
63168af266
|
|
@ -1,59 +0,0 @@
|
||||||
import datetime
|
|
||||||
from typing import Dict
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
def get_showings(film: str) -> Dict:
|
|
||||||
r = requests.get(f"https://www.offi.fr/cinema/evenement/{film}")
|
|
||||||
|
|
||||||
soup = BeautifulSoup(r.text)
|
|
||||||
|
|
||||||
screenings = soup.find_all("div", {"id": re.compile(r"t_sceances_[a-zA-Z0-9]+")})
|
|
||||||
|
|
||||||
output = {}
|
|
||||||
|
|
||||||
for screening in screenings:
|
|
||||||
datematch = re.match(r"t_sceances_([0-9]{4})-([0-9]{2})-([0-9]{2})", screening["id"])
|
|
||||||
|
|
||||||
date = datetime.date(int(datematch.group(1)), int(datematch.group(2)), int(datematch.group(3)))
|
|
||||||
|
|
||||||
datedata = {}
|
|
||||||
|
|
||||||
links = screening.find_all("a", {"class": "text-dark"})
|
|
||||||
|
|
||||||
for link in links:
|
|
||||||
|
|
||||||
cinema = link.find("span").text
|
|
||||||
|
|
||||||
cinetimes = []
|
|
||||||
|
|
||||||
times = link.find_next("div", {"class": "event-times-container"})
|
|
||||||
|
|
||||||
for time in times.find_all("span"):
|
|
||||||
cinetimes.append(time.text)
|
|
||||||
|
|
||||||
datedata[cinema] = cinetimes
|
|
||||||
|
|
||||||
output[date] = datedata
|
|
||||||
|
|
||||||
return output
|
|
||||||
|
|
||||||
FILMS = [
|
|
||||||
"le-ciel-peut-attendre-5799",
|
|
||||||
"tideland-26007",
|
|
||||||
"sommeil-dhiver-53367",
|
|
||||||
"a-girl-at-my-door-54742",
|
|
||||||
"37-degres-2-le-matin-228",
|
|
||||||
"mission-version-restauree-92072",
|
|
||||||
"funny-games-us-30758",
|
|
||||||
"huit-et-demi-13525",
|
|
||||||
"climax-69321",
|
|
||||||
"the-first-slam-dunk-92878"
|
|
||||||
]
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
for film in FILMS:
|
|
||||||
print(get_showings(film))
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
# pyproject.toml reference / quickstart: https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "flimder"
|
||||||
|
authors = [
|
||||||
|
{ name = "Eloi Zalczer", email = "eloi@zalczer.fr" },
|
||||||
|
]
|
||||||
|
description = "Find flims wherever they play"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
keywords = ["flimder"]
|
||||||
|
dynamic = ["version"]
|
||||||
|
dependencies = [
|
||||||
|
"requests==2.28.2",
|
||||||
|
"beautifulsoup4==4.12.2",
|
||||||
|
"flask-smorest==0.40.0",
|
||||||
|
"Flask==2.2.3",
|
||||||
|
"python-dotenv==0.17.1",
|
||||||
|
"gunicorn",
|
||||||
|
"toml",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.setuptools.dynamic]
|
||||||
|
version = { attr = "flimder.__version__" }
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
minversion = "7.0"
|
||||||
|
addopts = "--cov --cov-report=html --quiet -ra" # -ra --> report on all but "passed"
|
||||||
|
testpaths = ["test"]
|
||||||
|
python_files = ["test*.py"]
|
||||||
|
python_functions = ["test*"]
|
||||||
|
|
||||||
|
# coverage configuration reference: https://coverage.readthedocs.io/en/stable/config.html
|
||||||
|
[tool.coverage.run]
|
||||||
|
branch = true
|
||||||
|
parallel = true
|
||||||
|
include = ["src/*"]
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
show_missing = true
|
||||||
|
precision = 2
|
||||||
|
|
||||||
|
[tool.coverage.paths]
|
||||||
|
source = [
|
||||||
|
"src/flimder",
|
||||||
|
"*/site-packages/flimder",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
# black configuration reference: https://black.readthedocs.io/en/stable/usage_and_configuration/the_basics.html
|
||||||
|
line-length = 120
|
||||||
|
target-version = ["py37", "py38", "py39", "py310", "py311"]
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
# isort configuration reference: https://pycqa.github.io/isort/docs/configuration/options.html
|
||||||
|
profile = "black"
|
||||||
|
line_length = 120
|
||||||
|
src_paths = ["src", "tests"]
|
||||||
|
|
||||||
|
[tool.pylint.format]
|
||||||
|
max-line-length = 120
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
requests==2.28.2
|
||||||
|
beautifulsoup4==4.12.2
|
||||||
|
flask-smorest==0.40.0
|
||||||
|
Flask==2.2.3
|
||||||
|
python-dotenv==0.17.1
|
||||||
|
gunicorn
|
||||||
|
toml
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import flask
|
||||||
|
import werkzeug
|
||||||
|
|
||||||
|
from flimder import ext
|
||||||
|
from flimder.routes import blueprint
|
||||||
|
|
||||||
|
from flask_smorest import Api
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
|
|
||||||
|
|
||||||
|
BASE_DIR = os.path.dirname(__file__)
|
||||||
|
API_DOC_PATH = os.path.join(BASE_DIR, "apidoc.md")
|
||||||
|
CONFIG_PATH = os.environ.get("FLASK_CONF_PATH", os.path.join(BASE_DIR, "config.toml"))
|
||||||
|
CONFIG_NAME = os.environ.get("FLASK_CONF_NAME", "LOCALDEV")
|
||||||
|
|
||||||
|
|
||||||
|
def create_app(configname: str = CONFIG_NAME, configpath: str = CONFIG_PATH) -> flask.Flask:
|
||||||
|
"""Flask app factory.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
flask.Flask: The created flask application object, ready to be run.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Set HTTP protocol version to 1.1 to profit from the 'keep-alive' feature
|
||||||
|
# 'keep-alive' maintains the TCP connection open for a certain time allowing to re-use it when
|
||||||
|
# sending multiple requests
|
||||||
|
werkzeug.serving.WSGIRequestHandler.protocol_version = "HTTP/1.1"
|
||||||
|
|
||||||
|
app = flask.Flask(__name__)
|
||||||
|
|
||||||
|
ext.config.configname = configname
|
||||||
|
ext.config.configpath = configpath
|
||||||
|
|
||||||
|
ext.config.init_app(app)
|
||||||
|
|
||||||
|
with open(API_DOC_PATH, "r") as file:
|
||||||
|
apidoc = file.read()
|
||||||
|
|
||||||
|
app.config.setdefault("API_SPEC_OPTIONS", {}).setdefault("info", {})
|
||||||
|
app.config["API_SPEC_OPTIONS"]["info"]["description"] = apidoc
|
||||||
|
|
||||||
|
api = Api(app)
|
||||||
|
api.register_blueprint(blueprint)
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
## Flimder.
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
### Other documentation
|
||||||
|
|
||||||
|
| Name | Path |
|
||||||
|
| ---------- | -------------- |
|
||||||
|
| Swagger Ui | `/` |
|
||||||
|
| Redoc | `/doc/redoc` |
|
||||||
|
| Rapidoc | `/doc/rapidoc` |
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
[DEFAULT]
|
||||||
|
|
||||||
|
OPENAPI_VERSION = "3.0.2"
|
||||||
|
API_VERSION = "0.1"
|
||||||
|
API_TITLE = "Flimder"
|
||||||
|
|
||||||
|
OPENAPI_JSON_PATH = "api-spec.json"
|
||||||
|
OPENAPI_URL_PREFIX = "/"
|
||||||
|
OPENAPI_REDOC_PATH = "/redoc"
|
||||||
|
OPENAPI_REDOC_URL = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"
|
||||||
|
OPENAPI_SWAGGER_UI_PATH = "/"
|
||||||
|
OPENAPI_SWAGGER_UI_URL = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
|
||||||
|
OPENAPI_RAPIDOC_PATH = "/rapidoc"
|
||||||
|
OPENAPI_RAPIDOC_URL = "https://unpkg.com/rapidoc/dist/rapidoc-min.js"
|
||||||
|
|
||||||
|
[LOCALDEV]
|
||||||
|
|
||||||
|
DEBUG = true
|
||||||
|
TESTING = true
|
||||||
|
|
||||||
|
[PRODUCTION]
|
||||||
|
|
||||||
|
DEBUG = false
|
||||||
|
TESTING = false
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from flimder.tomlconfig import Config
|
||||||
|
|
||||||
|
config: Config = Config()
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
from marshmallow.fields import *
|
||||||
|
from webargs.fields import DelimitedList # noqa=W0611
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
|
class ScreeningsProvider(ABC):
|
||||||
|
@classmethod
|
||||||
|
@abstractmethod
|
||||||
|
def get_screenings(cls, movies: Dict[str, Dict]):
|
||||||
|
pass
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -0,0 +1,55 @@
|
||||||
|
import datetime
|
||||||
|
import re
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
|
||||||
|
class OfficielDesSpectaclesProvider:
|
||||||
|
name: str = "officiel_des_spectacles"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_movie_screenings(cls, url: str):
|
||||||
|
r = requests.get(url)
|
||||||
|
|
||||||
|
soup = BeautifulSoup(r.text, "html.parser")
|
||||||
|
|
||||||
|
screenings = soup.find_all("div", {"id": re.compile(r"t_sceances_[a-zA-Z0-9]+")})
|
||||||
|
|
||||||
|
output = {}
|
||||||
|
|
||||||
|
for screening in screenings:
|
||||||
|
datematch = re.match(r"t_sceances_([0-9]{4})-([0-9]{2})-([0-9]{2})", screening["id"])
|
||||||
|
|
||||||
|
date = datetime.date(int(datematch.group(1)), int(datematch.group(2)), int(datematch.group(3)))
|
||||||
|
|
||||||
|
datedata = {}
|
||||||
|
|
||||||
|
links = screening.find_all("a", {"class": "text-dark"})
|
||||||
|
|
||||||
|
for link in links:
|
||||||
|
cinema = link.find("span").text
|
||||||
|
|
||||||
|
cinetimes = []
|
||||||
|
|
||||||
|
times = link.find_next("div", {"class": "event-times-container"})
|
||||||
|
|
||||||
|
for time in times.find_all("span"):
|
||||||
|
cinetimes.append(time.text)
|
||||||
|
|
||||||
|
datedata[cinema] = cinetimes
|
||||||
|
|
||||||
|
output[date] = datedata
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_screenings(cls, movies: Dict[str, Dict]) -> Dict:
|
||||||
|
out = {}
|
||||||
|
|
||||||
|
for movie, params in movies.items():
|
||||||
|
screenings = cls._get_movie_screenings(params["url"])
|
||||||
|
out[movie] = screenings
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
_MOVIES_REPOSITORY_PATH = os.path.join(__file__, "..", "movies.json")
|
||||||
|
|
||||||
|
|
||||||
|
def get_movies() -> Dict:
|
||||||
|
with open(_MOVIES_REPOSITORY_PATH, "r", encoding="utf-8") as jsonfile:
|
||||||
|
data = json.load(jsonfile)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from flask_smorest import Blueprint
|
||||||
|
from flimder.providers.officiel_des_spectacles.provider import OfficielDesSpectaclesProvider
|
||||||
|
from flimder.repository import get_movies
|
||||||
|
from flimder.schemas import MovieSchema, ProviderSchema, ScreeningsSchema
|
||||||
|
|
||||||
|
PROVIDERS = [
|
||||||
|
OfficielDesSpectaclesProvider,
|
||||||
|
]
|
||||||
|
|
||||||
|
# TODO description from dunder variable.
|
||||||
|
blueprint = Blueprint("Flims finder", __name__, url_prefix="/", description="Flims finder.")
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.get("/screenings")
|
||||||
|
@blueprint.response(200, schema=ScreeningsSchema(many=True))
|
||||||
|
def get_screenings():
|
||||||
|
movies = get_movies()
|
||||||
|
|
||||||
|
out = []
|
||||||
|
|
||||||
|
for provider in PROVIDERS:
|
||||||
|
compatible_movies = {}
|
||||||
|
for movie in movies:
|
||||||
|
if provider.name in movie["providers_params"]:
|
||||||
|
compatible_movies[movie["title"]] = movie["providers_params"][provider.name]
|
||||||
|
|
||||||
|
screenings = provider.get_screenings(compatible_movies)
|
||||||
|
|
||||||
|
print(screenings)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.post("/movie")
|
||||||
|
@blueprint.arguments(schema=MovieSchema)
|
||||||
|
@blueprint.response(201)
|
||||||
|
def add_movie(body: Dict):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@blueprint.post("/providers")
|
||||||
|
@blueprint.response(200, schema=ProviderSchema(many=True))
|
||||||
|
def get_providers():
|
||||||
|
pass
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
from marshmallow import Schema
|
||||||
|
|
||||||
|
from flimder import fields
|
||||||
|
|
||||||
|
|
||||||
|
class MovieSchema(Schema):
|
||||||
|
title = fields.String()
|
||||||
|
|
||||||
|
|
||||||
|
class ScreeningSchema(Schema):
|
||||||
|
time = fields.String()
|
||||||
|
|
||||||
|
|
||||||
|
class TheaterScreeningsSchema(Schema):
|
||||||
|
theater_name = fields.String()
|
||||||
|
screenings = fields.List(fields.Nested(ScreeningSchema()))
|
||||||
|
|
||||||
|
|
||||||
|
class ScreeningsSchema(Schema):
|
||||||
|
movie = fields.Nested(MovieSchema())
|
||||||
|
screenings = fields.Mapping(fields.String(), fields.Nested(TheaterScreeningsSchema()))
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderSchema(Schema):
|
||||||
|
name = fields.String()
|
||||||
|
icon = fields.Url()
|
||||||
|
formfields = fields.List()
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
"""Small Flask Extension to load flask app configuration from a TOML file.
|
||||||
|
|
||||||
|
Heavily inspired by the YAML config parser in SpecsDB, adapted for TOML since support is included in
|
||||||
|
Python standard lib starting from python 3.11."""
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import functools
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import warnings
|
||||||
|
from collections import abc
|
||||||
|
from typing import Any, Dict, Mapping, Optional
|
||||||
|
|
||||||
|
import flask
|
||||||
|
import toml
|
||||||
|
|
||||||
|
_ENV_REG = re.compile(r"\$\{([a-zA-Z\_][a-zA-Z0-9\_]+)\|([^\n\r\0\}]*)\}")
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(maxsize=None)
|
||||||
|
def _decoder() -> toml.TomlDecoder:
|
||||||
|
"""Initialize a TOML decoder and keep it in cache.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Repo: a TOML decoder.
|
||||||
|
"""
|
||||||
|
return toml.TomlDecoder()
|
||||||
|
|
||||||
|
|
||||||
|
def load_value(v: str) -> Any:
|
||||||
|
"""Load a single value using the TOML logic.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
v (str): the string to parse.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Any: the parsed value. If the value could not be parsed, the input string is returned.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return _decoder().load_value(v)[0]
|
||||||
|
except ValueError:
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
"""Flask extension class; allow easy configuration from TOML config file.
|
||||||
|
|
||||||
|
Use `confs` property to get the configurations loaded from file.
|
||||||
|
The configuration will be loaded in the Flask app once, the 3 elements are set:
|
||||||
|
- flask app via constuctor or with `initApp` method
|
||||||
|
- configpath via constuctor or with `configpath` property
|
||||||
|
- configname via constuctor or with `configname` property
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app: The flask application, can be set at init or through initApp method
|
||||||
|
configpath: The configuration file path
|
||||||
|
configname: The selected configuration name
|
||||||
|
extendDefault: If True when using any conf, will first look for a conf with name
|
||||||
|
'default' and load its value before updating them with the selected conf values.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
app: Optional[flask.Flask] = None,
|
||||||
|
configpath: Optional[str] = None,
|
||||||
|
configname: Optional[str] = None,
|
||||||
|
extendDefault: bool = True,
|
||||||
|
):
|
||||||
|
self._app: Optional[flask.Flask] = app
|
||||||
|
self._configpath: Optional[str] = configpath
|
||||||
|
self._configname: Optional[str] = configname
|
||||||
|
self._extendDefault: bool = extendDefault
|
||||||
|
self._configs: Optional[Mapping[str, Mapping[str, Any]]] = None
|
||||||
|
|
||||||
|
if configpath is not None:
|
||||||
|
self._configs = self.read_file(configpath)
|
||||||
|
|
||||||
|
self._load()
|
||||||
|
|
||||||
|
def init_app(self, app: flask.Flask):
|
||||||
|
"""Register app."""
|
||||||
|
self._app = app
|
||||||
|
self._load()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def app(self) -> Optional[flask.Flask]:
|
||||||
|
"""The linked Flask application."""
|
||||||
|
return self._app
|
||||||
|
|
||||||
|
@property
|
||||||
|
def configpath(self) -> Optional[str]:
|
||||||
|
"""The path of the configuration file."""
|
||||||
|
return self._configpath
|
||||||
|
|
||||||
|
@configpath.setter
|
||||||
|
def configpath(self, path: str):
|
||||||
|
self._configs = self.read_file(path)
|
||||||
|
self._configpath = path
|
||||||
|
self._load()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def configname(self) -> Optional[str]:
|
||||||
|
"""The used configuration name"""
|
||||||
|
return self._configname
|
||||||
|
|
||||||
|
@configname.setter
|
||||||
|
def configname(self, name: str):
|
||||||
|
if self._configs is not None:
|
||||||
|
assert name.upper() in self.configs
|
||||||
|
self._configname = name.upper()
|
||||||
|
self._load()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def configs(self) -> Mapping[str, Mapping[str, Any]]:
|
||||||
|
"""Return configurations parsed from file as a dictionnary"""
|
||||||
|
if self._configs is None:
|
||||||
|
return None
|
||||||
|
if self._extendDefault:
|
||||||
|
default = self._configs.get("DEFAULT", {})
|
||||||
|
extended = {}
|
||||||
|
for configname, conf in self._configs.items():
|
||||||
|
extended[configname] = copy.deepcopy(default)
|
||||||
|
extended[configname].update(conf)
|
||||||
|
return extended
|
||||||
|
return copy.deepcopy(self._configs)
|
||||||
|
|
||||||
|
def _load(self) -> None:
|
||||||
|
"""Load `configname` in app."""
|
||||||
|
if self.app is None or self.configpath is None or self.configname is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
conf = self.configs[self.configname]
|
||||||
|
for key in ("ENV",):
|
||||||
|
if key in conf:
|
||||||
|
msg = (
|
||||||
|
f"'{self.configpath}:{self.configname}': {key} should be set through "
|
||||||
|
f"'FLASK_{key}' environment variable, the value is ignored"
|
||||||
|
)
|
||||||
|
del conf[key]
|
||||||
|
warnings.warn(msg)
|
||||||
|
self.app.config.from_mapping(conf)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def read_file(cls, path: str) -> Dict[str, Dict[str, Any]]:
|
||||||
|
"""Read TOML conf file, interpolate environment variables and capitalize all keys.
|
||||||
|
|
||||||
|
Patterns such as $VAR, ${VAR} and %VAR% in values are considered as environment variable
|
||||||
|
placeholders, and if a environment variable with the specified name exist then the pattern
|
||||||
|
is replaced with the value.
|
||||||
|
|
||||||
|
The pattern ${VAR:default_value} is considered as an environment variable placeholder with
|
||||||
|
a default value, the pattern will either be replaced with the corresponding environment
|
||||||
|
variable value, or with the default value.
|
||||||
|
Limitations:
|
||||||
|
- `VAR` can only contain letters, numbers and '_' and cannot start with a number
|
||||||
|
- `default_value` cannot contain '}'
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: The path of the TOML file to read
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The parsed TOML file as a dict.
|
||||||
|
"""
|
||||||
|
with open(path, "r") as file:
|
||||||
|
data = file.read()
|
||||||
|
return cls._recurse_expandvars(toml.loads(data, decoder=_decoder()))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _recurse_expandvars(cls, obj: Any) -> Any:
|
||||||
|
"""Expand environment variable"""
|
||||||
|
if isinstance(obj, str):
|
||||||
|
return cls._expandvars(obj)
|
||||||
|
if isinstance(obj, abc.Mapping):
|
||||||
|
return {key: cls._recurse_expandvars(val) for key, val in obj.items()}
|
||||||
|
if isinstance(obj, abc.Sequence):
|
||||||
|
return [cls._recurse_expandvars(elt) for elt in obj]
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _expandvars(cls, string: str) -> str:
|
||||||
|
"""Like os.path.expandvars but support default value in the format ${ENVNAME:default_val}
|
||||||
|
|
||||||
|
By definition, and contrary to expandvars behaviour, environment variable with a
|
||||||
|
default value are always expanded.
|
||||||
|
|
||||||
|
The default value cannot contains new line character, null charcter and will stop at the
|
||||||
|
first '}' encountered.
|
||||||
|
"""
|
||||||
|
string = os.path.expandvars(string)
|
||||||
|
while True:
|
||||||
|
match = _ENV_REG.search(string)
|
||||||
|
if not match:
|
||||||
|
break
|
||||||
|
pattern, envvar, default = match.group(0, 1, 2)
|
||||||
|
envval = os.environ.get(envvar, load_value(default))
|
||||||
|
string = string.replace(pattern, envval)
|
||||||
|
return string
|
||||||
Loading…
Reference in New Issue