import os
import json
import shutil
from dataclasses import asdict
from typing import Union
from tractor_beam.utils.globals import _f, check
from dataclasses import dataclass
from typing import List, Optional, Dict, Any
[docs]
@dataclass
# The `CustomJob` class in Python defines attributes for a custom job, including a function, headers,
# and types.
class CustomJob:
func: Optional[str] = None
headers: Optional[dict] = None
types: Optional[list] = None
[docs]
@dataclass
# The class `Job` in Python defines attributes for a job, including URL, types, beacon, delay, and
# custom job information.
class Job:
url: str
types: Optional[List[str]] = None
tasks: Optional[List[str]] = None
beacon: Optional[str] = None
delay: Optional[float] = None
custom: Optional[CustomJob] = None
# The `Settings` class in Python initializes an object with a project name, directory path, and a list
# of job instances, performing validation checks on the input parameters.
[docs]
class Settings:
def __init__(self, name: str, proj_dir: str, jobs: List[Job]):
if not name or not isinstance(name, str):
raise ValueError("name must be a non-empty string")
if not proj_dir or not isinstance(proj_dir, str):
raise ValueError("proj_dir must be a non-empty string")
if not jobs or not isinstance(jobs, list) or not all(isinstance(job, Job) for job in jobs):
raise ValueError("jobs must be a non-empty list of Job instances")
self.name = name
self.proj_dir = proj_dir
self.jobs = jobs
[docs]
@dataclass
# The `Schema` class has two attributes, `role` of type `str` and `settings` of type `Settings`.
class Schema:
role: str
settings: Settings
# The `ConfigEncoder` class in Python provides custom serialization for `Job` and `Settings` objects
# by overriding the `default` method of `JSONEncoder` and implementing a `to_serializable` method.
[docs]
class ConfigEncoder(json.JSONEncoder):
[docs]
def default(self, obj):
if isinstance(obj, Job) or isinstance(obj, Settings):
return self.to_serializable(obj) # Reuse the to_serializable function
# Let the base class default method raise the TypeError
return json.JSONEncoder.default(self, obj)
[docs]
def to_serializable(self, obj):
if isinstance(obj, Job):
return asdict(obj)
elif isinstance(obj, Settings):
# Assuming jobs is a list of Job instances
return {"name": obj.name, "proj_dir": obj.proj_dir, "jobs": [self.to_serializable(job) for job in obj.jobs]}
elif hasattr(obj, "__dict__"):
return obj.__dict__
else:
return str(obj) # Fallback for unsupported types
# The `Config` class in Python provides methods to load, parse, save, unbox, create, and destroy
# project configurations based on provided settings.
[docs]
class Config:
def __init__(self, conf: Union[str, dict, None] = None):
self.load_conf(conf)
[docs]
def load_conf(self, conf):
if isinstance(conf, str):
try:
with open(conf, 'r') as f:
conf_dict = json.load(f)
conf_instance = self.parse_conf(conf_dict)
if conf_instance:
self.conf = conf_instance
if self.conf and isinstance(self.conf, Schema):
_f('success', f'Config loaded from - {self.conf.settings.name}')
else:
_f('fatal', 'Failed to parse configuration')
else:
self.conf = None
_f('fatal', 'Failed to parse configuration from file')
except (ValueError, json.JSONDecodeError):
self.conf = None
except FileNotFoundError:
self.conf = None
elif isinstance(conf, dict):
conf_instance = self.parse_conf(conf)
if conf_instance:
self.conf = conf_instance
_f('success', f'Config loaded from - {self.conf.settings.name}')
else:
self.conf = None
_f('fatal', 'Failed to parse configuration from dictionary')
else:
self.conf = None
_f('fatal', f'Config not found - {conf}')
return self.conf
[docs]
def parse_conf(self, conf_dict: Dict[str, Any]) -> Schema:
if not conf_dict.get("role") or not isinstance(conf_dict["role"], str):
self.conf = None
_f("fatal", "role must be a non-empty string")
settings_dict = conf_dict["settings"]
if not settings_dict.get("name") or not isinstance(settings_dict["name"], str):
self.conf = None
_f("fatal", "settings name must be a non-empty string")
if not settings_dict.get("proj_dir") or not isinstance(settings_dict["proj_dir"], str):
self.conf = None
_f("fatal", "settings proj_dir must be a non-empty string")
if not settings_dict.get("jobs") or not isinstance(settings_dict["jobs"], list):
self.conf = None
_f("fatal", "settings jobs must be a list")
jobs_list = [Job(**job_dict) for job_dict in settings_dict["jobs"]]
if not jobs_list:
self.conf = None
_f("fatal", "settings jobs list must not be empty")
settings = Settings(name=settings_dict["name"],
proj_dir=settings_dict["proj_dir"],
jobs=jobs_list)
return Schema(role=conf_dict["role"], settings=settings)
[docs]
def save(self):
"""
This function saves a configuration object to a JSON file.
:return: The `save` method returns either the saved configuration object (`self.conf`) and the
path where it was saved (`conf_path`), or it returns `None` if there was an error or if there
was no configuration to save.
"""
if self.conf:
conf_path = os.path.join(self.conf.settings.proj_dir, self.conf.settings.name, 'config.json')
try:
with open(conf_path, 'w') as f:
json.dump(asdict(self.conf), f, cls=ConfigEncoder)
_f('info', f'Config saved to - {conf_path}')
return self.conf, conf_path
except TypeError as e:
_f('fatal', e)
return None
else:
_f('fatal', 'No configuration to save')
return None
[docs]
def unbox(self, overwrite: bool = False):
"""
The function `unbox` creates a project directory based on configuration settings, with an option
to overwrite existing directory.
:param overwrite: The `overwrite` parameter in the `unbox` method is a boolean flag that
determines whether to overwrite an existing directory if it already exists. If `overwrite` is
set to `True` and the directory at `proj_path` already exists, the method will delete the
existing directory and create a, defaults to False
:type overwrite: bool (optional)
:return: The `unbox` method returns the result of the `self.save()` method.
"""
if not self.conf:
return _f('fatal', 'Configuration is not loaded')
proj_path = os.path.join(self.conf.settings.proj_dir, self.conf.settings.name)
if overwrite and check(proj_path):
shutil.rmtree(proj_path)
os.makedirs(proj_path)
elif not check(proj_path):
os.makedirs(proj_path)
else:
_f('fatal', f'Exists - {proj_path}')
return None
_f('success', f'Unboxed! πΈπ¦ - {proj_path}')
return self.save()
[docs]
def create(self, config: dict = None):
"""
This Python function creates a project directory based on a provided configuration dictionary.
:param config: The `config` parameter in the `create` method is a dictionary that contains
configuration settings for a project. This method checks if the `config` parameter is provided,
parses the configuration, creates a project directory based on the parsed settings, and writes
the configuration to a `config.json` file in
:type config: dict
:return: either a success message indicating that the project has been successfully created and
the configuration has been saved in a JSON file, or a fatal error message indicating that the
project already exists or the provided config schema does not match the requirements.
"""
if config:
parsed_config = self.parse_conf(config)
if parsed_config:
proj_path = os.path.join(parsed_config.settings.proj_dir, parsed_config.settings.name)
if check(proj_path):
return _f('fatal', f'Exists - {proj_path}')
os.makedirs(proj_path)
with open(os.path.join(proj_path, 'config.json'), 'w') as f:
_f('success', f'Unboxed! πΈπ¦ using - {proj_path}')
return json.dump(asdict(parsed_config), f)
else:
_f('fatal', 'Your config schema does not match the requirements')
return None
[docs]
def destroy(self, confirm: str = None):
if not self.conf or not check(self.conf.settings.proj_dir):
return _f('fatal', 'Invalid path or configuration not loaded')
proj_path = os.path.join(self.conf.settings.proj_dir, self.conf.settings.name)
if confirm == self.conf.settings.name:
shutil.rmtree(proj_path)
_f('warn', f'{confirm} destroyed')
else:
_f('fatal', 'You did not confirm - `Config.destroy(confirm="your_config_name")`')