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
|
2021-09-16 14:48:16 +00:00
|
|
|
from .__about__ import __app__, __version__
|
2021-04-13 20:14:43 +00:00
|
|
|
from .types import Config, ConfigValue
|
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
|
|
|
|
2021-06-02 13:16:32 +00:00
|
|
|
class JinjaEnvironment(jinja2.Environment):
|
|
|
|
loader: jinja2.BaseLoader
|
|
|
|
|
|
|
|
def __init__(self, template_roots: List[str]) -> None:
|
|
|
|
loader = jinja2.FileSystemLoader(template_roots)
|
|
|
|
super().__init__(loader=loader, undefined=jinja2.StrictUndefined)
|
|
|
|
|
|
|
|
|
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
|
2021-06-02 13:16:32 +00:00
|
|
|
environment = JinjaEnvironment(template_roots)
|
2020-01-16 10:52:53 +00:00
|
|
|
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
|
2021-04-13 20:14:43 +00:00
|
|
|
environment.globals["iter_values_named"] = self.iter_values_named
|
|
|
|
environment.globals["patch"] = self.patch
|
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
|
2021-04-13 20:14:43 +00:00
|
|
|
environment.globals["rsa_import_key"] = utils.rsa_import_key
|
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
|
feat: run all services as unprivileged containers
With this change, containers are no longer run as "root" but as unprivileged
users. This is necessary in some environments, notably some Kubernetes
clusters.
To make this possible, we need to manually fix bind-mounted volumes in
docker-compose. This is pretty much equivalent to the behaviour in Kubernetes,
where permissions are fixed at runtime if the volume owner is incorrect. Thus,
we have a consistent behaviour between docker-compose and Kubernetes.
We achieve this by bind-mounting some repos inside "*-permissions" services.
These services run as root user on docker-compose and will fix the required
permissions, as per build/permissions/setowner.sh These services simply do not
run on Kubernetes, where we don't rely on bind-mounted volumes. There, we make
use of Kubernete's built-in volume ownership feature.
With this change, we get rid of the "openedx-dev" Docker image, in the sense
that it no longer has its own Dockerfile. Instead, the dev image is now simply
a different target in the multi-layer openedx Docker image. This makes it much
faster to build the openedx-dev image.
Because we declare the APP_USER_ID in the dev/docker-compose.yml file, we need
to pass the user ID from the host there. The only way to achieve that is with a
tutor config variable. The downside of this approach is that the
dev/docker-compose.yml file is no longer portable from one machine to the next.
We consider that this is not such a big issue, as it affects the development
environment only.
We take this opportunity to replace the base image of the "forum" image. There
is now no need to re-install ruby inside the image. The total image size is
only decreased by 10%, but re-building the image is faster.
In order to run the smtp service as non-root, we switch from namshi/smtp to
devture/exim-relay. This change should be backward-compatible.
Note that the nginx container remains privileged. We could switch to
nginxinc/nginx-unprivileged, but it's probably not worth the effort, as we are
considering to get rid of the nginx container altogether.
Close #323.
2021-09-23 10:04:19 +00:00
|
|
|
environment.globals["HOST_USER_ID"] = utils.get_user_id()
|
2021-09-16 14:48:16 +00:00
|
|
|
environment.globals["TUTOR_APP"] = __app__.replace("-", "_")
|
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-06-02 13:16:32 +00:00
|
|
|
env_templates: List[str] = self.environment.loader.list_templates()
|
2021-04-06 10:09:00 +00:00
|
|
|
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-04-13 20:14:43 +00:00
|
|
|
def iter_values_named(
|
|
|
|
self,
|
|
|
|
prefix: Optional[str] = None,
|
|
|
|
suffix: Optional[str] = None,
|
|
|
|
allow_empty: bool = False,
|
|
|
|
) -> Iterable[ConfigValue]:
|
|
|
|
"""
|
|
|
|
Iterate on all config values for which the name match the given pattern.
|
|
|
|
|
|
|
|
Note that here we only iterate on the values, not the key names. Empty
|
|
|
|
values (those that evaluate to boolean `false`) will not be yielded, unless
|
|
|
|
`allow_empty` is True.
|
|
|
|
TODO document this in the plugins API
|
|
|
|
"""
|
|
|
|
for var_name, value in self.config.items():
|
|
|
|
if prefix is not None and not var_name.startswith(prefix):
|
|
|
|
continue
|
|
|
|
if suffix is not None and not var_name.endswith(suffix):
|
|
|
|
continue
|
|
|
|
if not allow_empty and not value:
|
|
|
|
continue
|
|
|
|
yield value
|
|
|
|
|
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):
|
2019-05-29 09:14:06 +00:00
|
|
|
try:
|
2021-04-13 20:14:43 +00:00
|
|
|
patches.append(self.render_str(patch))
|
|
|
|
except exceptions.TutorError:
|
|
|
|
fmt.echo_error(
|
|
|
|
"Error rendering patch '{}' from plugin {}".format(name, plugin)
|
2019-05-29 09:14:06 +00:00
|
|
|
)
|
2021-04-13 20:14:43 +00:00
|
|
|
raise
|
2019-05-29 09:14:06 +00:00
|
|
|
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 [
|
|
|
|
"apps/",
|
|
|
|
"build/",
|
|
|
|
"dev/",
|
|
|
|
"k8s/",
|
|
|
|
"local/",
|
|
|
|
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-11-08 14:41:35 +00:00
|
|
|
def upgrade_obsolete(_root: str) -> None:
|
2021-10-14 10:47:23 +00:00
|
|
|
"""
|
|
|
|
Add here ad-hoc commands to upgrade the environment.
|
|
|
|
"""
|
2020-03-12 10:59:50 +00:00
|
|
|
|
|
|
|
|
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-13 20:14:43 +00:00
|
|
|
def render_unknown(config: Config, value: Any) -> Any:
|
2019-01-22 20:25:04 +00:00
|
|
|
"""
|
2021-04-13 20:14:43 +00:00
|
|
|
Render an unknown `value` object with the selected config.
|
2019-01-22 20:25:04 +00:00
|
|
|
|
2021-04-13 20:14:43 +00:00
|
|
|
If `value` is a dict, its values are also rendered.
|
2019-01-22 20:25:04 +00:00
|
|
|
"""
|
2019-06-05 13:43:51 +00:00
|
|
|
if isinstance(value, str):
|
|
|
|
return render_str(config, value)
|
2021-04-13 20:14:43 +00:00
|
|
|
elif isinstance(value, dict):
|
|
|
|
return {k: render_unknown(config, v) for k, v in value.items()}
|
2019-06-05 13:43:51 +00:00
|
|
|
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.
|
|
|
|
"""
|
2021-10-18 09:43:40 +00:00
|
|
|
return {
|
|
|
|
"0": "ironwood",
|
|
|
|
"3": "ironwood",
|
|
|
|
"10": "juniper",
|
|
|
|
"11": "koa",
|
|
|
|
"12": "lilac",
|
|
|
|
"13": "maple",
|
|
|
|
}[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)
|