parent
20842cf499
commit
35915063d5
4 changed files with 693 additions and 0 deletions
@ -0,0 +1,3 @@ |
||||
""" |
||||
The job of a loader is to collect commands and their arguments from a text file. |
||||
""" |
@ -0,0 +1,544 @@ |
||||
# Imports |
||||
|
||||
from commonkit import parse_jinja_string, parse_jinja_template, pick, read_file, smart_cast, split_csv, File |
||||
from jinja2.exceptions import TemplateError, TemplateNotFound |
||||
import logging |
||||
import os |
||||
from ..library.snippets.mappings import MAPPINGS |
||||
|
||||
log = logging.getLogger(__name__) |
||||
|
||||
# Exports |
||||
|
||||
__all__ = ( |
||||
"BaseLoader", |
||||
) |
||||
|
||||
# Classes |
||||
|
||||
|
||||
class BaseLoader(File): |
||||
|
||||
def __init__(self, path, context=None, locations=None, mappings=None, profile="ubuntu", **kwargs): |
||||
self.context = context or dict() |
||||
self.is_loaded = False |
||||
self.locations = locations or list() |
||||
self.mappings = mappings or MAPPINGS |
||||
self.options = kwargs |
||||
self.profile = profile |
||||
self.snippets = list() |
||||
|
||||
# Always include the path to the current file in locations. |
||||
self.locations.insert(0, self.directory) |
||||
|
||||
super().__init__(path) |
||||
|
||||
def get_snippets(self): |
||||
a = list() |
||||
|
||||
for canonical_name, args, kwargs in self.snippets: |
||||
snippet = self.find_snippet(canonical_name, *args, **kwargs) |
||||
a.append(snippet) |
||||
|
||||
return a |
||||
|
||||
def find_snippet(self, name, *args, **kwargs): |
||||
# Templates require special handling. |
||||
if name == "template": |
||||
source = args[0] |
||||
target = args[1] |
||||
kwargs['locations'] = self.locations |
||||
return Template(source, target, **kwargs) |
||||
|
||||
# Convert args to a list so we can update it below. |
||||
_args = list(args) |
||||
|
||||
# The given name is not in the mappings -- which is a typo or invalid name -- but it could also be a dotted path |
||||
# to be followed down through the dictionary structure. |
||||
if name not in self.mappings[self.profile]: |
||||
if "." in name: |
||||
return self.find_snippet_by_dotted_name(name, *args, **kwargs) |
||||
|
||||
log.error("Command not found in mappings: %s" % name) |
||||
return Snippet(name, args=_args, kwargs=kwargs) |
||||
|
||||
# Formal or informal sub-commands exist in a dictionary. |
||||
if type(self.mappings[self.profile][name]) is dict: |
||||
try: |
||||
possible_sub_command = _args[0] |
||||
except IndexError: |
||||
log.warning("No sub-command argument for: %s" % name) |
||||
return Snippet(name) |
||||
|
||||
if possible_sub_command in self.mappings[self.profile][name]: |
||||
sub = _args.pop(0) |
||||
snippet = self.mappings[self.profile][name][sub] |
||||
|
||||
parser = self.mappings[self.profile][name].get('_parser', None) |
||||
|
||||
_name = "%s.%s" % (name, sub) |
||||
return Snippet(_name, args=_args, content=snippet, context=self.context, kwargs=kwargs, parser=parser) |
||||
|
||||
# Django allows pre-defined as well as adhoc commands. The name of the command is provided as the first |
||||
# argument in the config file. The following statements are only invoked if the possible_sub_command is not |
||||
# in the django dictionary. |
||||
if name == "django": |
||||
sub = _args.pop(0) |
||||
kwargs['_name'] = sub |
||||
print(kwargs) |
||||
snippet = self.mappings[self.profile]['django']['command'] |
||||
parser = self.mappings[self.profile]['django']['_parser'] |
||||
return Snippet("django.%s" % sub, args=_args, content=snippet, context=self.context, kwargs=kwargs, |
||||
parser=parser) |
||||
|
||||
log.warning("Sub-command could not be determined for: %s" % name) |
||||
return Snippet(name, args=list(args), context=self.context, kwargs=kwargs) |
||||
|
||||
# The found snippet should just be a string. |
||||
return Snippet(name, args=list(args), content=self.mappings[self.profile][name], kwargs=kwargs) |
||||
|
||||
def find_snippet_by_dotted_name(self, name, *args, **kwargs): |
||||
# This may not exist. If so, None is the value of the Snippet.content attribute. |
||||
snippet = pick(name, self.mappings[self.profile]) |
||||
|
||||
# The name of the builder callback is always the root of the given name plus _parser. |
||||
builder_name = "%s._parser" % name.split(".")[0] |
||||
parser = pick(builder_name, self.mappings[self.profile]) |
||||
|
||||
# Return the snippet instance. |
||||
return Snippet(name, args=list(args), parser=parser, content=snippet, kwargs=kwargs) |
||||
|
||||
def load(self): |
||||
"""Load the command file. |
||||
|
||||
:rtype: bool |
||||
|
||||
""" |
||||
raise NotImplementedError() |
||||
|
||||
def read_file(self): |
||||
"""Get the content of the command file. |
||||
|
||||
:rtype: str | None |
||||
|
||||
""" |
||||
if self.context is not None: |
||||
try: |
||||
return parse_jinja_template(self.path, self.context) |
||||
except Exception as e: |
||||
log.error("Failed to process %s file as template: %s" % (self.path, e)) |
||||
return None |
||||
|
||||
return read_file(self.path) |
||||
|
||||
# def _get_command(self, name, *args, **kwargs): |
||||
# args = list(args) |
||||
# |
||||
# if name not in self.mappings: |
||||
# return None |
||||
# |
||||
# if type(self.mappings[name]) is dict: |
||||
# sub = args.pop(0) |
||||
# subs = self.mappings[name] |
||||
# if sub not in subs: |
||||
# return None |
||||
# |
||||
# _command = subs[sub] |
||||
# else: |
||||
# _command = self.mappings[name] |
||||
# |
||||
# context = self.context.copy() |
||||
# context['args'] = args |
||||
# context.update(kwargs) |
||||
# |
||||
# if type(_command) in (list, tuple): |
||||
# # print(" ".join(_command)) |
||||
# a = list() |
||||
# for i in _command: |
||||
# i = parse_jinja_string(i, context) |
||||
# a.append(i) |
||||
# |
||||
# return " ".join(a) |
||||
# |
||||
# return parse_jinja_string(_command, context) |
||||
|
||||
# noinspection PyMethodMayBeStatic |
||||
def _get_key_value(self, key, value): |
||||
"""Process a key/value pair from an INI section. |
||||
|
||||
:param key: The key to be processed. |
||||
:type key: str |
||||
|
||||
:param value: The value to be processed. |
||||
|
||||
:rtype: tuple |
||||
:returns: The key and value, both of which may be modified from the originals. |
||||
|
||||
""" |
||||
if key in ("environments", "environs", "envs", "env"): |
||||
_key = "environments" |
||||
_value = split_csv(value) |
||||
elif key in ("func", "function"): |
||||
_key = "function" |
||||
_value = value |
||||
elif key == "groups": |
||||
_key = "groups" |
||||
if type(value) in (list, tuple): |
||||
_value = value |
||||
else: |
||||
_value = split_csv(value) |
||||
elif key == "items": |
||||
_key = "items" |
||||
if type(value) in (list, tuple): |
||||
_value = value |
||||
else: |
||||
_value = split_csv(value) |
||||
elif key == "tags": |
||||
_key = "tags" |
||||
_value = split_csv(value) |
||||
else: |
||||
_key = key |
||||
_value = smart_cast(value) |
||||
|
||||
return _key, _value |
||||
|
||||
|
||||
class Snippet(object): |
||||
|
||||
def __init__(self, name, args=None, content=None, context=None, kwargs=None, parser=None): |
||||
"""Initialize a snippet. |
||||
|
||||
:param name: The canonical name of the snippet. |
||||
:type name: str |
||||
|
||||
:param args: A list of arguments found in the config file. |
||||
:type args: list[str] |
||||
|
||||
:param content: The content of the snippet. |
||||
:type content: str | list[str] |
||||
|
||||
:param context: Additional context variables used to render the command. |
||||
:type context: dict |
||||
|
||||
:param kwargs: The keyword arguments found in the config file. These may be specific to the command or one of |
||||
the common options. They are accessible as dynamic attributes of the Snippet instance. |
||||
:type kwargs: dict |
||||
|
||||
:param parser: A callback that may be used to assemble the command. |
||||
:type parser: callable |
||||
|
||||
""" |
||||
self.args = args or list() |
||||
self.parser = parser |
||||
self.content = content |
||||
self.context = context or dict() |
||||
self.kwargs = kwargs or dict() |
||||
self.name = name |
||||
|
||||
sudo = self.kwargs.get("sudo", None) |
||||
if isinstance(sudo, Sudo): |
||||
self.sudo = sudo |
||||
elif type(sudo) is str: |
||||
self.sudo = Sudo(enabled=True, user=sudo) |
||||
elif sudo is True: |
||||
self.sudo = Sudo(enabled=True) |
||||
else: |
||||
self.sudo = Sudo() |
||||
|
||||
def __getattr__(self, item): |
||||
return self.kwargs.get(item) |
||||
|
||||
def __str__(self): |
||||
return str(self.name) |
||||
|
||||
def get_statement(self, cd=True, include_comment=True, include_register=True, include_stop=True): |
||||
lines = list() |
||||
if self.comment and include_comment: |
||||
lines.append("# %s" % self.comment) |
||||
|
||||
# Handle command itemization. Note that register and stop options are ignored. |
||||
if self.is_itemized: |
||||
for item in self.items: |
||||
args = list() |
||||
for arg in self.args: |
||||
args.append(arg.replace("$item", item)) |
||||
|
||||
if self.parser: |
||||
statement = self.parser(self, args=args) |
||||
else: |
||||
statement = self._parse(args=args) |
||||
|
||||
a = list() |
||||
if cd and self.cd is not None: |
||||
a.append("( cd %s &&" % self.cd) |
||||
|
||||
if self.prefix is not None: |
||||
a.append("%s &&" % self.prefix) |
||||
|
||||
if self.sudo: |
||||
statement = "%s %s" % (self.sudo, statement) |
||||
|
||||
a.append(statement) |
||||
|
||||
if cd and self.cd is not None: |
||||
a.append(")") |
||||
|
||||
lines.append(" ".join(a)) |
||||
|
||||
return "\n".join(lines) |
||||
|
||||
# Handle normal (not itemized) comands. |
||||
a = list() |
||||
if cd and self.cd is not None: |
||||
a.append("( cd %s &&" % self.cd) |
||||
|
||||
if self.prefix is not None: |
||||
a.append("%s &&" % self.prefix) |
||||
|
||||
if self.parser: |
||||
statement = self.parser(self) |
||||
else: |
||||
statement = self._parse() |
||||
|
||||
if self.sudo: |
||||
statement = "%s %s" % (self.sudo, statement) |
||||
|
||||
a.append(statement) |
||||
|
||||
if cd and self.cd is not None: |
||||
a.append(")") |
||||
|
||||
if self.condition is not None: |
||||
lines.append("if [[ %s ]]; then %s; fi;" % (self.condition, " ".join(a))) |
||||
else: |
||||
lines.append(" ".join(a)) |
||||
|
||||
if include_register and self.register is not None: |
||||
lines.append("%s=$?;" % self.register) |
||||
|
||||
if include_stop and self.stop: |
||||
lines.append("if [[ $%s -gt 0 ]]; exit 1; fi;" % self.register) |
||||
elif include_stop and self.stop: |
||||
lines.append("if [[ $? -gt 0 ]]; exit 1; fi;") |
||||
else: |
||||
pass |
||||
|
||||
return "\n".join(lines) |
||||
|
||||
@property |
||||
def is_itemized(self): |
||||
s = " ".join(self.args) |
||||
return "$item" in s |
||||
|
||||
@property |
||||
def is_valid(self): |
||||
return self.content is not None |
||||
|
||||
# def _get_statement(self): |
||||
# if self.is_itemized: |
||||
# a = list() |
||||
# for item in self.items: |
||||
# args = list() |
||||
# for arg in self.args: |
||||
# args.append(arg.replace("$item", item)) |
||||
# |
||||
# context = self.context.copy() |
||||
# context['args'] = args |
||||
# context.update(self.kwargs) |
||||
# |
||||
# if type(self.content) is list: |
||||
# b = list() |
||||
# for i in self.content: |
||||
# i = parse_jinja_string(i, context) |
||||
# b.append(i) |
||||
# |
||||
# a.append(" ".join(b)) |
||||
# else: |
||||
# a.append(parse_jinja_string(self.content, context)) |
||||
# |
||||
# return "\n".join(a) |
||||
# |
||||
# context = self.context.copy() |
||||
# context['args'] = self.args |
||||
# context.update(self.kwargs) |
||||
# |
||||
# a = list() |
||||
# if type(self.content) is list: |
||||
# b = list() |
||||
# for i in self.content: |
||||
# i = parse_jinja_string(i, context) |
||||
# b.append(i) |
||||
# |
||||
# a.append(" ".join(b)) |
||||
# else: |
||||
# a.append(parse_jinja_string(self.content, context)) |
||||
# |
||||
# return " ".join(a) |
||||
|
||||
def _parse(self, args=None, kwargs=None): |
||||
"""Build the command statement from snippet content. |
||||
|
||||
:param args: A list of arguments which override those provided by the command configuration. |
||||
:type args: list[str] |
||||
|
||||
:param kwargs: A dictionary which overrides the options provided by the command configuration. |
||||
:type kwargs: dict |
||||
|
||||
:rtype: str |
||||
|
||||
""" |
||||
context = self.context.copy() |
||||
context['args'] = args or self.args |
||||
context.update(kwargs or self.kwargs) |
||||
|
||||
if type(self.content) is list: |
||||
a = list() |
||||
for string in self.content: |
||||
output = parse_jinja_string(string, context) |
||||
if len(output) == 0: |
||||
continue |
||||
|
||||
a.append(output) |
||||
|
||||
return " ".join(a) |
||||
|
||||
return parse_jinja_string(self.content, context) |
||||
|
||||
|
||||
class Sudo(object): |
||||
"""Helper class for defining sudo options.""" |
||||
|
||||
def __init__(self, enabled=False, user="root"): |
||||
"""Initialize the helper. |
||||
|
||||
:param enabled: Indicates sudo is enabled. |
||||
:type enabled: bool |
||||
|
||||
:param user: The user to be invoked. |
||||
:type user: str |
||||
|
||||
""" |
||||
self.enabled = enabled |
||||
self.user = user |
||||
|
||||
def __bool__(self): |
||||
return self.enabled |
||||
|
||||
def __str__(self): |
||||
if self.enabled: |
||||
return "sudo -u %s" % self.user |
||||
|
||||
return "" |
||||
|
||||
|
||||
class Template(object): |
||||
|
||||
PARSER_JINJA = "jinja2" |
||||
PARSER_SIMPLE = "simple" |
||||
|
||||
def __init__(self, source, target, backup=True, parser=None, **kwargs): |
||||
self.backup_enabled = backup |
||||
self.context = kwargs.pop("context", dict()) |
||||
self.parser = parser or self.PARSER_JINJA |
||||
self.locations = kwargs.pop("locations", list()) |
||||
self.source = os.path.expanduser(source) |
||||
self.target = target |
||||
|
||||
self.kwargs = kwargs |
||||
|
||||
def __getattr__(self, item): |
||||
return self.kwargs.get(item) |
||||
|
||||
def __str__(self): |
||||
return "template" |
||||
|
||||
def get_content(self): |
||||
"""Parse the template. |
||||
|
||||
:rtype: str | None |
||||
|
||||
""" |
||||
template = self.get_template() |
||||
|
||||
if self.parser == self.PARSER_SIMPLE: |
||||
content = read_file(template) |
||||
for key, value in self.context.items(): |
||||
replace = "$%s$" % key |
||||
content = content.replace(replace, str(value)) |
||||
|
||||
return content |
||||
|
||||
try: |
||||
return parse_jinja_template(template, self.context) |
||||
except TemplateNotFound: |
||||
log.error("Template not found: %s" % template) |
||||
return None |
||||
except TemplateError as e: |
||||
log.error("Could not parse %s template: %s" % (template, e)) |
||||
return None |
||||
|
||||
def get_statement(self, cd=True, include_comment=True, include_register=True, include_stop=True): |
||||
if self.parser == self.PARSER_SIMPLE: |
||||
return self._get_simple_statement(cd=cd, include_comment=include_comment, include_register=include_register, |
||||
include_stop=include_stop) |
||||
else: |
||||
return self._get_jinja2_statement(cd=cd, include_comment=include_comment, include_register=include_register, |
||||
include_stop=include_stop) |
||||
|
||||
def get_template(self): |
||||
"""Get the template path. |
||||
|
||||
:rtype: str |
||||
|
||||
""" |
||||
source = self.source |
||||
for location in self.locations: |
||||
_source = os.path.join(location, self.source) |
||||
if os.path.exists(_source): |
||||
return _source |
||||
|
||||
return source |
||||
|
||||
@property |
||||
def is_itemized(self): |
||||
return "$item" in self.target |
||||
|
||||
@property |
||||
def is_valid(self): |
||||
return True |
||||
|
||||
def _get_command(self, content): |
||||
"""Get the cat command.""" |
||||
output = list() |
||||
|
||||
# TODO: Template backup is not system safe, but is specific to bash. |
||||
if self.backup_enabled: |
||||
output.append('if [[ -f "%s" ]]; then mv %s %s.b; fi;' % (self.target, self.target, self.target)) |
||||
|
||||
if content.startswith("#!"): |
||||
_content = content.split("\n") |
||||
first_line = _content.pop(0) |
||||
output.append('echo "%s" > %s' % (first_line, self.target)) |
||||
output.append("cat >> %s << EOF" % self.target) |
||||
output.append("\n".join(_content)) |
||||
output.append("EOF") |
||||
else: |
||||
output.append("cat > %s << EOF" % self.target) |
||||
output.append(content) |
||||
output.append("EOF") |
||||
|
||||
statement = "\n".join(output) |
||||
|
||||
# noinspection PyUnusedLocal |
||||
def _get_jinja2_statement(self, cd=True, include_comment=True, include_register=True, include_stop=True): |
||||
"""Parse a Jinja2 template.""" |
||||
content = self.get_content() |
||||
return self._get_command(content) |
||||
|
||||
# noinspection PyUnusedLocal |
||||
def _get_simple_statement(self, cd=True, include_comment=True, include_register=True, include_stop=True): |
||||
"""Parse a "simple" template.""" |
||||
content = self.get_content() |
||||
|
||||
return self._get_command(content) |
||||
|
@ -0,0 +1,75 @@ |
||||
# Imports |
||||
|
||||
from commonkit import parse_jinja_template, read_file |
||||
from configparser import ConfigParser, ParsingError |
||||
import logging |
||||
from .base import BaseLoader |
||||
|
||||
log = logging.getLogger(__name__) |
||||
|
||||
# Exports |
||||
|
||||
__all__ = ( |
||||
"INILoader", |
||||
) |
||||
|
||||
# Classes |
||||
|
||||
|
||||
class INILoader(BaseLoader): |
||||
|
||||
def load(self): |
||||
if not self.exists: |
||||
return False |
||||
|
||||
if self.context is not None: |
||||
try: |
||||
content = parse_jinja_template(self.path, self.context) |
||||
except Exception as e: |
||||
log.error("Failed to process %s INI file as template: %s" % (self.path, e)) |
||||
return False |
||||
else: |
||||
content = read_file(self.path) |
||||
|
||||
ini = ConfigParser() |
||||
try: |
||||
ini.read_string(content) |
||||
except ParsingError as e: |
||||
log.error("Failed to parse %s as an INI file: %s" % (self.path, e)) |
||||
return False |
||||
|
||||
for comment in ini.sections(): |
||||
args = list() |
||||
command_name = None |
||||
count = 0 |
||||
kwargs = self.options.copy() |
||||
kwargs['comment'] = comment |
||||
|
||||
for key, value in ini.items(comment): |
||||
# The first key/value pair is the command name and arguments. |
||||
if count == 0: |
||||
command_name = key |
||||
|
||||
# Arguments surrounded by quotes are considered to be one argument. All others are split into a |
||||
# list to be passed to the callback. It is also possible that this is a call where no arguments are |
||||
# present, so the whole thing is wrapped to protect against an index error. A TypeError is raised in |
||||
# cases where a command is provided with no positional arguments; we interpret this as True. |
||||
try: |
||||
if value[0] == '"': |
||||
args.append(value.replace('"', "")) |
||||
else: |
||||
args = value.split(" ") |
||||
except IndexError: |
||||
pass |
||||
except TypeError: |
||||
args.append(True) |
||||
else: |
||||
_key, _value = self._get_key_value(key, value) |
||||
kwargs[_key] = _value |
||||
|
||||
count += 1 |
||||
|
||||
self.snippets.append((command_name, args, kwargs)) |
||||
|
||||
self.is_loaded = True |
||||
return True |
@ -0,0 +1,71 @@ |
||||
# Imports |
||||
|
||||
from commonkit import parse_jinja_template, read_file |
||||
import logging |
||||
import yaml |
||||
from .base import BaseLoader |
||||
|
||||
log = logging.getLogger(__name__) |
||||
|
||||
# Exports |
||||
|
||||
__all__ = ( |
||||
"YMLLoader", |
||||
) |
||||
|
||||
# Classes |
||||
|
||||
|
||||
class YMLLoader(BaseLoader): |
||||
|
||||
def load(self): |
||||
if not self.exists: |
||||
return False |
||||
|
||||
content = self.read_file() |
||||
if content is None: |
||||
return False |
||||
|
||||
try: |
||||
commands = yaml.load(content, yaml.Loader) |
||||
except yaml.YAMLError as e: |
||||
log.error("Failed to parse %s as a YAML file: %s" % (self.path, e)) |
||||
return False |
||||
|
||||
for command in commands: |
||||
comment = list(command.keys())[0] |
||||
tokens = list(command.values())[0] |
||||
|
||||
args = list() |
||||
command_name = None |
||||
count = 0 |
||||
kwargs = self.options.copy() |
||||
kwargs['comment'] = comment |
||||
|
||||
for key, value in tokens.items(): |
||||
if key.startswith("_"): |
||||
continue |
||||
|
||||
if count == 0: |
||||
command_name = key |
||||
|
||||
try: |
||||
if value[0] == '"': |
||||
args.append(value.replace('"', "")) |
||||
else: |
||||
args = value.split(" ") |
||||
except IndexError: |
||||
pass |
||||
except TypeError: |
||||
args.append(True) |
||||
else: |
||||
_key, _value = self._get_key_value(key, value) |
||||
kwargs[_key] = _value |
||||
|
||||
count += 1 |
||||
|
||||
self.snippets.append((command_name, args, kwargs)) |
||||
|
||||
self.is_loaded = True |
||||
|
||||
return True |
Loading…
Reference in new issue