2019-01-22 20:25:04 +00:00
|
|
|
import codecs
|
|
|
|
import os
|
2021-04-06 10:09:00 +00:00
|
|
|
from copy import deepcopy
|
|
|
|
from typing import Any, Iterable, List, Optional, Type, Union
|
2019-01-22 20:25:04 +00:00
|
|
|
|
|
|
|
import jinja2
|
2019-08-20 15:04:18 +00:00
|
|
|
import pkg_resources
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2021-04-06 10:09:00 +00:00
|
|
|
from . import exceptions, fmt, plugins, utils
|
2019-03-18 16:26:37 +00:00
|
|
|
from .__about__ import __version__
|
2021-04-06 10:09:00 +00:00
|
|
|
from .types import Config
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-08-20 15:04:18 +00:00
|
|
|
TEMPLATES_ROOT = pkg_resources.resource_filename("tutor", "templates")
|
2019-03-18 16:26:37 +00:00
|
|
|
VERSION_FILENAME = "version"
|
Support .woff and .woff2 file format for fonts
Right now if I add .woff or .woff2 fonts files to an Indigo-based theme's `lms/static/fonts` directory, I get the following error:
```
$ /indigo-folder# make
tutor config render --extra-config ./config-totem.yml ./theme "$(tutor config printroot)/env/build/openedx/themes/indigo-totem"
Error loading template lms/static/fonts/NotoSans-Bold.woff
Traceback (most recent call last):
File "/home/maarten/.local/bin/tutor", line 8, in <module>
sys.exit(main())
File "/home/maarten/.local/lib/python3.8/site-packages/tutor/commands/cli.py", line 38, in main
cli() # pylint: disable=no-value-for-parameter
File "/usr/lib/python3/dist-packages/click/core.py", line 764, in __call__
return self.main(*args, **kwargs)
File "/usr/lib/python3/dist-packages/click/core.py", line 717, in main
rv = self.invoke(ctx)
File "/usr/lib/python3/dist-packages/click/core.py", line 1137, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
File "/usr/lib/python3/dist-packages/click/core.py", line 1137, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
File "/usr/lib/python3/dist-packages/click/core.py", line 956, in invoke
return ctx.invoke(self.callback, **ctx.params)
File "/usr/lib/python3/dist-packages/click/core.py", line 555, in invoke
return callback(*args, **kwargs)
File "/usr/lib/python3/dist-packages/click/decorators.py", line 27, in new_func
return f(get_current_context().obj, *args, **kwargs)
File "/home/maarten/.local/lib/python3.8/site-packages/tutor/commands/config.py", line 86, in render
renderer.render_all_to(dst)
File "/home/maarten/.local/lib/python3.8/site-packages/tutor/env.py", line 153, in render_all_to
rendered = self.render_file(template)
File "/home/maarten/.local/lib/python3.8/site-packages/tutor/env.py", line 137, in render_file
template = self.environment.get_template(path)
File "/usr/lib/python3/dist-packages/jinja2/environment.py", line 830, in get_template
return self._load_template(name, self.make_globals(globals))
File "/usr/lib/python3/dist-packages/jinja2/environment.py", line 804, in _load_template
template = self.loader.load(self, name, globals)
File "/usr/lib/python3/dist-packages/jinja2/loaders.py", line 113, in load
source, filename, uptodate = self.get_source(environment, name)
File "/usr/lib/python3/dist-packages/jinja2/loaders.py", line 175, in get_source
contents = f.read().decode(self.encoding)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xb4 in position 10: invalid start byte
make: *** [Makefile:2: render] Error 1
```
2020-10-15 11:35:30 +00:00
|
|
|
BIN_FILE_EXTENSIONS = [".ico", ".jpg", ".png", ".ttf", ".woff", ".woff2"]
|
2019-03-18 16:26:37 +00:00
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2019-05-11 17:31:18 +00:00
|
|
|
class Renderer:
|
|
|
|
@classmethod
|
2021-04-06 10:09:00 +00:00
|
|
|
def instance(cls: Type["Renderer"], config: Config) -> "Renderer":
|
2020-10-15 14:28:55 +00:00
|
|
|
# Load template roots: these are required to be able to use
|
|
|
|
# {% include .. %} directives
|
|
|
|
template_roots = [TEMPLATES_ROOT]
|
|
|
|
for plugin in plugins.iter_enabled(config):
|
|
|
|
if plugin.templates_root:
|
|
|
|
template_roots.append(plugin.templates_root)
|
|
|
|
|
|
|
|
return cls(config, template_roots, ignore_folders=["partials"])
|
2019-05-11 17:31:18 +00:00
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
2021-04-06 10:09:00 +00:00
|
|
|
config: Config,
|
2021-02-25 08:09:14 +00:00
|
|
|
template_roots: List[str],
|
|
|
|
ignore_folders: Optional[List[str]] = None,
|
|
|
|
):
|
2020-01-16 10:52:53 +00:00
|
|
|
self.config = deepcopy(config)
|
2020-01-16 14:40:38 +00:00
|
|
|
self.template_roots = template_roots
|
2020-01-16 21:25:57 +00:00
|
|
|
self.ignore_folders = ignore_folders or []
|
|
|
|
self.ignore_folders.append(".git")
|
2019-05-11 22:11:44 +00:00
|
|
|
|
2020-01-16 10:52:53 +00:00
|
|
|
# Create environment
|
|
|
|
environment = jinja2.Environment(
|
|
|
|
loader=jinja2.FileSystemLoader(template_roots),
|
|
|
|
undefined=jinja2.StrictUndefined,
|
|
|
|
)
|
|
|
|
environment.filters["common_domain"] = utils.common_domain
|
2020-04-04 16:22:15 +00:00
|
|
|
environment.filters["encrypt"] = utils.encrypt
|
2020-01-16 10:52:53 +00:00
|
|
|
environment.filters["list_if"] = utils.list_if
|
2019-12-24 16:22:12 +00:00
|
|
|
environment.filters["long_to_base64"] = utils.long_to_base64
|
2020-04-04 16:22:15 +00:00
|
|
|
environment.filters["random_string"] = utils.random_string
|
2020-01-16 10:52:53 +00:00
|
|
|
environment.filters["reverse_host"] = utils.reverse_host
|
2019-12-24 16:22:12 +00:00
|
|
|
environment.filters["rsa_private_key"] = utils.rsa_private_key
|
2020-01-16 10:52:53 +00:00
|
|
|
environment.filters["walk_templates"] = self.walk_templates
|
|
|
|
environment.globals["patch"] = self.patch
|
2019-12-24 16:22:12 +00:00
|
|
|
environment.globals["rsa_import_key"] = utils.rsa_import_key
|
2020-01-16 10:52:53 +00:00
|
|
|
environment.globals["TUTOR_VERSION"] = __version__
|
|
|
|
self.environment = environment
|
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def iter_templates_in(self, *prefix: str) -> Iterable[str]:
|
2020-11-07 15:37:43 +00:00
|
|
|
"""
|
|
|
|
The elements of `prefix` must contain only "/", and not os.sep.
|
|
|
|
"""
|
2021-02-25 08:09:14 +00:00
|
|
|
full_prefix = "/".join(prefix)
|
2021-04-06 10:09:00 +00:00
|
|
|
env_templates: List[
|
|
|
|
str
|
|
|
|
] = self.environment.loader.list_templates() # type:ignore[no-untyped-call]
|
|
|
|
for template in env_templates:
|
2021-02-25 08:09:14 +00:00
|
|
|
if template.startswith(full_prefix) and self.is_part_of_env(template):
|
2020-01-16 10:52:53 +00:00
|
|
|
yield template
|
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def walk_templates(self, subdir: str) -> Iterable[str]:
|
2020-01-16 10:52:53 +00:00
|
|
|
"""
|
|
|
|
Iterate on the template files from `templates/<subdir>`.
|
2019-05-11 22:11:44 +00:00
|
|
|
|
2020-01-16 10:52:53 +00:00
|
|
|
Yield:
|
|
|
|
path: template path relative to the template root
|
|
|
|
"""
|
|
|
|
yield from self.iter_templates_in(subdir + "/")
|
2019-05-11 17:31:18 +00:00
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def is_part_of_env(self, path: str) -> bool:
|
2020-01-16 21:25:57 +00:00
|
|
|
"""
|
|
|
|
Determines whether a template should be rendered or not. Note that here we don't
|
|
|
|
rely on the OS separator, as we are handling templates
|
|
|
|
"""
|
|
|
|
parts = path.split("/")
|
|
|
|
basename = parts[-1]
|
|
|
|
is_excluded = False
|
|
|
|
is_excluded = (
|
|
|
|
is_excluded or basename.startswith(".") or basename.endswith(".pyc")
|
|
|
|
)
|
|
|
|
is_excluded = is_excluded or basename == "__pycache__"
|
|
|
|
for ignore_folder in self.ignore_folders:
|
|
|
|
is_excluded = is_excluded or ignore_folder in parts
|
|
|
|
return not is_excluded
|
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def find_os_path(self, template_name: str) -> str:
|
2020-11-07 15:37:43 +00:00
|
|
|
path = template_name.replace("/", os.sep)
|
2020-01-16 14:40:38 +00:00
|
|
|
for templates_root in self.template_roots:
|
|
|
|
full_path = os.path.join(templates_root, path)
|
|
|
|
if os.path.exists(full_path):
|
|
|
|
return full_path
|
|
|
|
raise ValueError("Template path does not exist")
|
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def patch(self, name: str, separator: str = "\n", suffix: str = "") -> str:
|
2020-01-08 11:20:24 +00:00
|
|
|
"""
|
|
|
|
Render calls to {{ patch("...") }} in environment templates from plugin patches.
|
|
|
|
"""
|
2019-05-29 09:14:06 +00:00
|
|
|
patches = []
|
2020-01-16 10:52:53 +00:00
|
|
|
for plugin, patch in plugins.iter_patches(self.config, name):
|
|
|
|
patch_template = self.environment.from_string(patch)
|
2019-05-29 09:14:06 +00:00
|
|
|
try:
|
2020-01-16 10:52:53 +00:00
|
|
|
patches.append(patch_template.render(**self.config))
|
2019-05-29 09:14:06 +00:00
|
|
|
except jinja2.exceptions.UndefinedError as e:
|
|
|
|
raise exceptions.TutorError(
|
|
|
|
"Missing configuration value: {} in patch '{}' from plugin {}".format(
|
|
|
|
e.args[0], name, plugin
|
|
|
|
)
|
|
|
|
)
|
|
|
|
rendered = separator.join(patches)
|
|
|
|
if rendered:
|
|
|
|
rendered += suffix
|
|
|
|
return rendered
|
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def render_str(self, text: str) -> str:
|
2020-01-16 10:52:53 +00:00
|
|
|
template = self.environment.from_string(text)
|
|
|
|
return self.__render(template)
|
2019-05-11 17:31:18 +00:00
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def render_template(self, template_name: str) -> Union[str, bytes]:
|
2020-02-27 16:14:00 +00:00
|
|
|
"""
|
|
|
|
Render a template file. Return the corresponding string. If it's a binary file
|
|
|
|
(as indicated by its path), return bytes.
|
2020-11-07 15:37:43 +00:00
|
|
|
|
|
|
|
The template_name *always* uses "/" separators, and is not os-dependent. Do not pass the result of
|
|
|
|
os.path.join(...) to this function.
|
2020-02-27 16:14:00 +00:00
|
|
|
"""
|
2020-11-07 15:37:43 +00:00
|
|
|
if is_binary_file(template_name):
|
2020-01-16 14:40:38 +00:00
|
|
|
# Don't try to render binary files
|
2020-11-07 15:37:43 +00:00
|
|
|
with open(self.find_os_path(template_name), "rb") as f:
|
2020-01-16 14:40:38 +00:00
|
|
|
return f.read()
|
|
|
|
|
2020-01-16 10:52:53 +00:00
|
|
|
try:
|
2020-11-07 15:37:43 +00:00
|
|
|
template = self.environment.get_template(template_name)
|
2020-01-16 10:52:53 +00:00
|
|
|
except Exception:
|
2020-11-07 15:37:43 +00:00
|
|
|
fmt.echo_error("Error loading template " + template_name)
|
2020-01-16 10:52:53 +00:00
|
|
|
raise
|
2020-01-16 14:40:38 +00:00
|
|
|
|
2020-01-16 10:52:53 +00:00
|
|
|
try:
|
|
|
|
return self.__render(template)
|
|
|
|
except (jinja2.exceptions.TemplateError, exceptions.TutorError):
|
2020-11-07 15:37:43 +00:00
|
|
|
fmt.echo_error("Error rendering template " + template_name)
|
2020-01-16 10:52:53 +00:00
|
|
|
raise
|
|
|
|
except Exception:
|
2020-11-07 15:37:43 +00:00
|
|
|
fmt.echo_error("Unknown error rendering template " + template_name)
|
2020-01-16 10:52:53 +00:00
|
|
|
raise
|
2019-06-05 13:43:51 +00:00
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def render_all_to(self, root: str, *prefix: str) -> None:
|
2020-11-07 15:37:43 +00:00
|
|
|
"""
|
|
|
|
`prefix` can be used to limit the templates to render.
|
|
|
|
"""
|
|
|
|
for template_name in self.iter_templates_in(*prefix):
|
|
|
|
rendered = self.render_template(template_name)
|
|
|
|
dst = os.path.join(root, template_name.replace("/", os.sep))
|
2020-01-16 14:40:38 +00:00
|
|
|
write_to(rendered, dst)
|
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def __render(self, template: jinja2.Template) -> str:
|
2020-01-16 10:52:53 +00:00
|
|
|
try:
|
|
|
|
return template.render(**self.config)
|
|
|
|
except jinja2.exceptions.UndefinedError as e:
|
|
|
|
raise exceptions.TutorError(
|
|
|
|
"Missing configuration value: {}".format(e.args[0])
|
|
|
|
)
|
2019-06-05 13:43:51 +00:00
|
|
|
|
2020-01-16 10:52:53 +00:00
|
|
|
|
2021-04-06 10:09:00 +00:00
|
|
|
def save(root: str, config: Config) -> None:
|
2019-03-18 16:26:37 +00:00
|
|
|
"""
|
2020-01-16 10:52:53 +00:00
|
|
|
Save the full environment, including version information.
|
|
|
|
"""
|
|
|
|
root_env = pathjoin(root)
|
|
|
|
for prefix in [
|
|
|
|
"android/",
|
|
|
|
"apps/",
|
|
|
|
"build/",
|
|
|
|
"dev/",
|
|
|
|
"k8s/",
|
|
|
|
"local/",
|
|
|
|
"webui/",
|
|
|
|
VERSION_FILENAME,
|
|
|
|
"kustomization.yml",
|
|
|
|
]:
|
|
|
|
save_all_from(prefix, root_env, config)
|
|
|
|
|
|
|
|
for plugin in plugins.iter_enabled(config):
|
|
|
|
if plugin.templates_root:
|
|
|
|
save_plugin_templates(plugin, root, config)
|
|
|
|
|
2020-03-12 10:59:50 +00:00
|
|
|
upgrade_obsolete(root)
|
2020-01-16 10:52:53 +00:00
|
|
|
fmt.echo_info("Environment generated in {}".format(base_dir(root)))
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def upgrade_obsolete(root: str) -> None:
|
2020-03-12 10:59:50 +00:00
|
|
|
# tutor.conf was renamed to _tutor.conf in order to be the first config file loaded
|
|
|
|
# by nginx
|
|
|
|
nginx_tutor_conf = pathjoin(root, "apps", "nginx", "tutor.conf")
|
|
|
|
if os.path.exists(nginx_tutor_conf):
|
|
|
|
os.remove(nginx_tutor_conf)
|
|
|
|
|
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def save_plugin_templates(
|
2021-04-06 10:09:00 +00:00
|
|
|
plugin: plugins.BasePlugin, root: str, config: Config
|
2021-02-25 08:09:14 +00:00
|
|
|
) -> None:
|
2019-07-02 20:16:44 +00:00
|
|
|
"""
|
|
|
|
Save plugin templates to plugins/<plugin name>/*.
|
|
|
|
Only the "apps" and "build" subfolders are rendered.
|
|
|
|
"""
|
2020-01-16 10:52:53 +00:00
|
|
|
plugins_root = pathjoin(root, "plugins")
|
2019-07-02 20:16:44 +00:00
|
|
|
for subdir in ["apps", "build"]:
|
2020-01-16 10:52:53 +00:00
|
|
|
subdir_path = os.path.join(plugin.name, subdir)
|
|
|
|
save_all_from(subdir_path, plugins_root, config)
|
2019-07-02 20:16:44 +00:00
|
|
|
|
|
|
|
|
2021-04-06 10:09:00 +00:00
|
|
|
def save_all_from(prefix: str, root: str, config: Config) -> None:
|
2019-01-22 20:25:04 +00:00
|
|
|
"""
|
2020-01-16 10:52:53 +00:00
|
|
|
Render the templates that start with `prefix` and store them with the same
|
2020-11-07 15:37:43 +00:00
|
|
|
hierarchy at `root`. Here, `prefix` can be the result of os.path.join(...).
|
2019-01-22 20:25:04 +00:00
|
|
|
"""
|
2020-01-16 10:52:53 +00:00
|
|
|
renderer = Renderer.instance(config)
|
2020-11-07 15:37:43 +00:00
|
|
|
renderer.render_all_to(root, prefix.replace(os.sep, "/"))
|
2019-07-02 20:16:44 +00:00
|
|
|
|
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def write_to(content: Union[str, bytes], path: str) -> None:
|
2020-01-16 14:40:38 +00:00
|
|
|
"""
|
2020-02-27 16:14:00 +00:00
|
|
|
Write some content to a path. Content can be either str or bytes.
|
2020-01-16 14:40:38 +00:00
|
|
|
"""
|
2021-02-25 08:09:14 +00:00
|
|
|
utils.ensure_file_directory_exists(path)
|
2020-01-16 14:40:38 +00:00
|
|
|
if isinstance(content, bytes):
|
2021-02-25 08:09:14 +00:00
|
|
|
with open(path, mode="wb") as of_binary:
|
|
|
|
of_binary.write(content)
|
2020-11-20 15:05:56 +00:00
|
|
|
else:
|
2021-02-25 08:09:14 +00:00
|
|
|
with open(path, mode="w", encoding="utf8", newline="\n") as of_text:
|
|
|
|
of_text.write(content)
|
2019-03-22 17:50:16 +00:00
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2021-04-06 10:09:00 +00:00
|
|
|
def render_file(config: Config, *path: str) -> Union[str, bytes]:
|
2019-05-11 17:31:18 +00:00
|
|
|
"""
|
|
|
|
Return the rendered contents of a template.
|
|
|
|
"""
|
2020-06-17 10:11:54 +00:00
|
|
|
renderer = Renderer.instance(config)
|
2020-11-07 15:37:43 +00:00
|
|
|
template_name = "/".join(path)
|
|
|
|
return renderer.render_template(template_name)
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2021-04-06 10:09:00 +00:00
|
|
|
def render_dict(config: Config) -> None:
|
2019-01-22 20:25:04 +00:00
|
|
|
"""
|
|
|
|
Render the values from the dict. This is useful for rendering the default
|
|
|
|
values from config.yml.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
config (dict)
|
|
|
|
"""
|
2021-04-06 10:09:00 +00:00
|
|
|
rendered: Config = {}
|
2019-01-22 20:25:04 +00:00
|
|
|
for key, value in config.items():
|
|
|
|
if isinstance(value, str):
|
2019-03-22 17:50:16 +00:00
|
|
|
rendered[key] = render_str(config, value)
|
2019-01-22 20:25:04 +00:00
|
|
|
else:
|
|
|
|
rendered[key] = value
|
|
|
|
for k, v in rendered.items():
|
|
|
|
config[k] = v
|
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2021-04-06 10:09:00 +00:00
|
|
|
def render_unknown(config: Config, value: Any) -> Any:
|
2019-06-05 13:43:51 +00:00
|
|
|
if isinstance(value, str):
|
|
|
|
return render_str(config, value)
|
|
|
|
return value
|
|
|
|
|
2019-06-05 17:57:30 +00:00
|
|
|
|
2021-04-06 10:09:00 +00:00
|
|
|
def render_str(config: Config, text: str) -> str:
|
2019-01-22 20:25:04 +00:00
|
|
|
"""
|
|
|
|
Args:
|
|
|
|
text (str)
|
|
|
|
config (dict)
|
|
|
|
|
|
|
|
Return:
|
|
|
|
substituted (str)
|
|
|
|
"""
|
2020-01-16 10:52:53 +00:00
|
|
|
return Renderer.instance(config).render_str(text)
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def check_is_up_to_date(root: str) -> None:
|
2019-06-05 13:43:51 +00:00
|
|
|
if not is_up_to_date(root):
|
|
|
|
message = (
|
|
|
|
"The current environment stored at {} is not up-to-date: it is at "
|
|
|
|
"v{} while the 'tutor' binary is at v{}. You should upgrade "
|
|
|
|
"the environment by running:\n"
|
|
|
|
"\n"
|
2019-06-05 17:45:22 +00:00
|
|
|
" tutor config save"
|
2019-06-05 13:43:51 +00:00
|
|
|
)
|
2020-02-27 16:14:00 +00:00
|
|
|
fmt.echo_alert(
|
|
|
|
message.format(base_dir(root), current_version(root), __version__)
|
|
|
|
)
|
2019-06-05 13:43:51 +00:00
|
|
|
|
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def is_up_to_date(root: str) -> bool:
|
2019-05-11 17:31:18 +00:00
|
|
|
"""
|
|
|
|
Check if the currently rendered version is equal to the current tutor version.
|
|
|
|
"""
|
2020-02-27 16:14:00 +00:00
|
|
|
return current_version(root) == __version__
|
2019-03-18 16:26:37 +00:00
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def needs_major_upgrade(root: str) -> bool:
|
2019-12-24 16:22:12 +00:00
|
|
|
"""
|
|
|
|
Return the current version as a tuple of int. E.g: (1, 0, 2).
|
|
|
|
"""
|
|
|
|
current = int(current_version(root).split(".")[0])
|
|
|
|
required = int(__version__.split(".")[0])
|
2020-06-15 15:57:14 +00:00
|
|
|
return 0 < current < required
|
2019-12-24 16:22:12 +00:00
|
|
|
|
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def current_release(root: str) -> str:
|
2019-12-24 16:22:12 +00:00
|
|
|
"""
|
|
|
|
Return the name of the current Open edX release.
|
|
|
|
"""
|
2020-09-17 10:53:14 +00:00
|
|
|
return {"0": "ironwood", "3": "ironwood", "10": "juniper", "11": "koa"}[
|
2020-06-15 15:57:14 +00:00
|
|
|
current_version(root).split(".")[0]
|
|
|
|
]
|
2019-12-24 16:22:12 +00:00
|
|
|
|
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def current_version(root: str) -> str:
|
2019-03-18 16:26:37 +00:00
|
|
|
"""
|
2019-05-11 17:31:18 +00:00
|
|
|
Return the current environment version. If the current environment has no version,
|
2019-12-24 16:22:12 +00:00
|
|
|
return "0.0.0".
|
2019-03-18 16:26:37 +00:00
|
|
|
"""
|
|
|
|
path = pathjoin(root, VERSION_FILENAME)
|
|
|
|
if not os.path.exists(path):
|
2019-12-24 16:22:12 +00:00
|
|
|
return "0.0.0"
|
2019-03-18 16:26:37 +00:00
|
|
|
return open(path).read().strip()
|
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def read_template_file(*path: str) -> str:
|
2019-01-22 20:25:04 +00:00
|
|
|
"""
|
2019-05-11 17:31:18 +00:00
|
|
|
Read raw content of template located at `path`.
|
2019-01-22 20:25:04 +00:00
|
|
|
"""
|
2019-03-22 17:50:16 +00:00
|
|
|
src = template_path(*path)
|
2019-05-05 09:45:24 +00:00
|
|
|
with codecs.open(src, encoding="utf-8") as fi:
|
2019-01-22 20:25:04 +00:00
|
|
|
return fi.read()
|
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def is_binary_file(path: str) -> bool:
|
2020-01-16 14:40:38 +00:00
|
|
|
ext = os.path.splitext(path)[1]
|
|
|
|
return ext in BIN_FILE_EXTENSIONS
|
|
|
|
|
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def template_path(*path: str, templates_root: str = TEMPLATES_ROOT) -> str:
|
2019-05-11 17:31:18 +00:00
|
|
|
"""
|
|
|
|
Return the template file's absolute path.
|
|
|
|
"""
|
2020-01-16 10:52:53 +00:00
|
|
|
return os.path.join(templates_root, *path)
|
2019-03-22 17:50:16 +00:00
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def data_path(root: str, *path: str) -> str:
|
2019-05-11 17:31:18 +00:00
|
|
|
"""
|
|
|
|
Return the file's absolute path inside the data directory.
|
|
|
|
"""
|
|
|
|
return os.path.join(root_dir(root), "data", *path)
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def pathjoin(root: str, *path: str) -> str:
|
2019-05-11 17:31:18 +00:00
|
|
|
"""
|
|
|
|
Return the file's absolute path inside the environment.
|
|
|
|
"""
|
|
|
|
return os.path.join(base_dir(root), *path)
|
2019-03-18 16:26:37 +00:00
|
|
|
|
2019-04-19 22:02:47 +00:00
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def base_dir(root: str) -> str:
|
2019-05-11 17:31:18 +00:00
|
|
|
"""
|
|
|
|
Return the environment base directory.
|
|
|
|
"""
|
|
|
|
return os.path.join(root_dir(root), "env")
|
|
|
|
|
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def root_dir(root: str) -> str:
|
2019-05-11 17:31:18 +00:00
|
|
|
"""
|
|
|
|
Return the project root directory.
|
|
|
|
"""
|
|
|
|
return os.path.abspath(root)
|