"""
This module defines various classes of supported actions. All actions are
implemented as static methods, but are defined using classes (as opposed to
modules) so that a set of well-defined actions can be namespaced easily.
"""
import os
import shutil
[docs]def get_nested_dict(input_dict, key):
"""
Helper function to interpret a nested dict input.
"""
current = input_dict
toks = key.split("->")
n = len(toks)
for i, tok in enumerate(toks):
if tok not in current and i < n - 1:
current[tok] = {}
elif i == n - 1:
return current, toks[-1]
current = current[tok]
return None
[docs]class DictActions:
"""
Class to implement the supported mongo-like modifications on a dict.
Supported keywords include the following Mongo-based keywords, with the
usual meanings (refer to Mongo documentation for information):
_inc
_set
_unset
_push
_push_all
_add_to_set (but _each is not supported)
_pop
_pull
_pull_all
_rename
However, note that "_set" does not support modification of nested dicts
using the mongo {"a.b":1} notation. This is because mongo does not allow
keys with "." to be inserted. Instead, nested dict modification is
supported using a special "->" keyword, e.g. {"a->b": 1}
"""
[docs] @staticmethod
def set(input_dict, settings):
"""
Sets a value using MongoDB syntax.
Args:
input_dict (dict): The input dictionary to be modified.
settings (dict): The specification of the modification to be made.
"""
for k, v in settings.items():
(d, key) = get_nested_dict(input_dict, k)
d[key] = v
[docs] @staticmethod
def unset(input_dict, settings):
"""
Unsets a value using MongoDB syntax.
Args:
input_dict (dict): The input dictionary to be modified.
settings (dict): The specification of the modification to be made.
"""
for k in settings.keys():
(d, key) = get_nested_dict(input_dict, k)
del d[key]
[docs] @staticmethod
def push(input_dict, settings):
"""
Push to a list using MongoDB syntax.
Args:
input_dict (dict): The input dictionary to be modified.
settings (dict): The specification of the modification to be made.
"""
for k, v in settings.items():
(d, key) = get_nested_dict(input_dict, k)
if key in d:
d[key].append(v)
else:
d[key] = [v]
[docs] @staticmethod
def push_all(input_dict, settings):
"""
Push multiple items to a list using MongoDB syntax.
Args:
input_dict (dict): The input dictionary to be modified.
settings (dict): The specification of the modification to be made.
"""
for k, v in settings.items():
(d, key) = get_nested_dict(input_dict, k)
if key in d:
d[key].extend(v)
else:
d[key] = v
[docs] @staticmethod
def inc(input_dict, settings):
"""
Increment a value using MongdoDB syntax.
Args:
input_dict (dict): The input dictionary to be modified.
settings (dict): The specification of the modification to be made.
"""
for k, v in settings.items():
(d, key) = get_nested_dict(input_dict, k)
if key in d:
d[key] += v
else:
d[key] = v
[docs] @staticmethod
def rename(input_dict, settings):
"""
Rename a key using MongoDB syntax.
Args:
input_dict (dict): The input dictionary to be modified.
settings (dict): The specification of the modification to be made.
"""
for k, v in settings.items():
if k in input_dict:
input_dict[v] = input_dict[k]
del input_dict[k]
[docs] @staticmethod
def add_to_set(input_dict, settings):
"""
Add to set using MongoDB syntax.
Args:
input_dict (dict): The input dictionary to be modified.
settings (dict): The specification of the modification to be made.
"""
for k, v in settings.items():
(d, key) = get_nested_dict(input_dict, k)
if key in d and (not isinstance(d[key], list)):
raise ValueError(f"Keyword {k} does not refer to an array.")
if key in d and v not in d[key]:
d[key].append(v)
elif key not in d:
d[key] = v
[docs] @staticmethod
def pull(input_dict, settings):
"""
Pull an item using MongoDB syntax.
Args:
input_dict (dict): The input dictionary to be modified.
settings (dict): The specification of the modification to be made.
"""
for k, v in settings.items():
(d, key) = get_nested_dict(input_dict, k)
if key in d and (not isinstance(d[key], list)):
raise ValueError(f"Keyword {k} does not refer to an array.")
if key in d:
d[key] = [i for i in d[key] if i != v]
[docs] @staticmethod
def pull_all(input_dict, settings):
"""
Pull multiple items to a list using MongoDB syntax.
Args:
input_dict (dict): The input dictionary to be modified.
settings (dict): The specification of the modification to be made.
"""
for k, v in settings.items():
if k in input_dict and (not isinstance(input_dict[k], list)):
raise ValueError(f"Keyword {k} does not refer to an array.")
for i in v:
DictActions.pull(input_dict, {k: i})
[docs] @staticmethod
def pop(input_dict, settings):
"""
Pop item from a list using MongoDB syntax.
Args:
input_dict (dict): The input dictionary to be modified.
settings (dict): The specification of the modification to be made.
"""
for k, v in settings.items():
(d, key) = get_nested_dict(input_dict, k)
if key in d and (not isinstance(d[key], list)):
raise ValueError(f"Keyword {k} does not refer to an array.")
if v == 1:
d[key].pop()
elif v == -1:
d[key].pop(0)
[docs]class FileActions:
"""
Class of supported file actions. For FileActions, the modder class takes in
a filename as a string. The filename should preferably be a full path to
avoid ambiguity.
"""
[docs] @staticmethod
def file_create(filename, settings):
"""
Creates a file.
Args:
filename (str): Filename.
settings (dict): Must be {"content": actual_content}
"""
if len(settings) != 1:
raise ValueError("Settings must only contain one item with key " "'content'.")
for k, v in settings.items():
if k == "content":
with open(filename, "w") as f:
f.write(v)
[docs] @staticmethod
def file_move(filename, settings):
"""
Moves a file. {'_file_move': {'dest': 'new_file_name'}}
Args:
filename (str): Filename.
settings (dict): Must be {"dest": path of new file}
"""
if len(settings) != 1:
raise ValueError("Settings must only contain one item with key " "'dest'.")
for k, v in settings.items():
if k == "dest":
shutil.move(filename, v)
[docs] @staticmethod
def file_delete(filename, settings):
"""
Deletes a file. {'_file_delete': {'mode': "actual"}}
Args:
filename (str): Filename.
settings (dict): Must be {"mode": actual/simulated}. Simulated
mode only prints the action without performing it.
"""
if len(settings) != 1:
raise ValueError("Settings must only contain one item with key " "'mode'.")
for k, v in settings.items():
if k == "mode" and v == "actual":
try:
os.remove(filename)
except OSError:
# Skip file not found error.
pass
elif k == "mode" and v == "simulated":
print(f"Simulated removal of {filename}")
[docs] @staticmethod
def file_copy(filename, settings):
"""
Copies a file. {'_file_copy': {'dest': 'new_file_name'}}
Args:
filename (str): Filename.
settings (dict): Must be {"dest": path of new file}
"""
for k, v in settings.items():
if k.startswith("dest"):
shutil.copyfile(filename, v)
[docs] @staticmethod
def file_modify(filename, settings):
"""
Modifies file access
Args:
filename (str): Filename.
settings (dict): Can be "mode" or "owners"
"""
for k, v in settings.items():
if k == "mode":
os.chmod(filename, v)
if k == "owners":
os.chown(filename, v)