Source code for gantt_project_maker.main

"""
This is the main start-up file of the project planner
"""

import argparse
import codecs
import locale
import logging
import sys
from datetime import datetime
from pathlib import Path

import yaml

from gantt_project_maker import __version__
from gantt_project_maker.colors import set_custom_colors
from gantt_project_maker.project_classes import (
    ProjectPlanner,
    SCALES,
    parse_date,
    extend_suffix,
)
from gantt_project_maker.utils import check_if_date

__author__ = "Eelco van Vliet"
__copyright__ = "Eelco van Vliet"
__license__ = "MIT"

_logger = logging.getLogger(__name__)


############################################################################


[docs] def get_info_from_file_or_settings(settings, key): """ Get the information directly from the settings or from a separate file if a filename is given Parameters ---------- settings: dict Settings file key: str Key of the settings file we want to read Returns ------- dict: Structure with information from the settings or separate file """ information = settings[key] if isinstance(information, str): with codecs.open(information, "r", encoding="UTF-8") as stream: information = yaml.load(stream=stream, Loader=yaml.Loader) return information
[docs] def get_pasted_employees(args_employee, employees_info): """get the list of full names or requested employees and return as a comma separated string""" all_employees = [] for employee in args_employee: employee_name = get_employee_name(employees_info, employee) all_employees.append(employee_name) all_employees = ", ".join(all_employees) return all_employees
[docs] def get_employee_name(employees_info, employee): """get the full name of the employee from the settings file""" try: request_employee = employees_info[employee] except KeyError as err: _logger.warning(err) raise KeyError( f"Employee {employee} given via argument is not find in section 'employees' " f"in settings file. Please add this employee to your settings" ) else: try: request_name = request_employee["name"] except KeyError as err: _logger.warning(err) raise KeyError("'name' not given for employee {request_employee} ") return request_name
[docs] def parse_args(args): """Parse command line parameters Args: args (List[str]): command line parameters as a list of strings (for example, ``["--help"]``). Returns: obj:`argparse.Namespace`: command line parameters namespace """ parser = argparse.ArgumentParser( description="A front end to the python-gantt project planning" ) parser.add_argument("settings_filename", help="Name of the configuration file") parser.add_argument("--output_filename", help="Name of the text output file") parser.add_argument( "--version", action="version", version=f"gantt_project_maker {__version__}", ) parser.add_argument( "-v", "--verbose", dest="loglevel", help="set loglevel to INFO", action="store_const", const=logging.INFO, default=logging.INFO, ) parser.add_argument( "-vv", "--debug", dest="loglevel", help="set loglevel to DEBUG", action="store_const", const=logging.DEBUG, ) parser.add_argument( "-q", "--quiet", dest="loglevel", help="set loglevel to WARNING", action="store_const", const=logging.WARNING, ) parser.add_argument( "-vvv", "--very_verbose", help="Also show the logging of the gantt module", action="store_true", ) parser.add_argument( "-s", "--scale", help="The scale of the grid of the project scheme", choices=set(SCALES.keys()), ) parser.add_argument( "--details", help="Add all the tasks with the detail attribute", action="store_true", default=True, ) parser.add_argument( "--no_details", help="Suppress all the tasks with the detail attribute.", action="store_false", dest="details", ) parser.add_argument( "-e", "--export_to_xlsx", help="Export the project plan to Excel", action="append", nargs="*", ) parser.add_argument( "-c", "--collaps_tasks", help="Collaps the tasks per project to one task to remove details", action="store_true", ) parser.add_argument( "-b", "--resources", help="Write the resources file of the planning", action="store_true", ) parser.add_argument( "--vacations", help="Write the vacations file of all the employees", action="store_true", ) parser.add_argument( "-m", "--employee", help="Only use the projects of this employee. Can be given multiple times for multiple " "employees.", action="append", ) parser.add_argument( "-f", "--filter_employees", help="Only include tasks to which this employee contributes. Can be given multiple times for multiple " "employees.", action="append", ) parser.add_argument( "--projects", help="Only include the main project given in this list. Argument can be given multiple times for multiple " "projects to include. Alternatively, a comma-separated list of projects may be given. If not given, " "all projects defined under 'projects' will be included.", action="append", ) parser.add_argument( "-p", "--period", help="On export this period from the list of periods as given in the settings file. If " "not given, all the periods are writen to file", action="append", ) parser.add_argument( "--start_planning", type=check_if_date, help="Start of the planning. If not given, the value given in de settings file is taken", ) parser.add_argument( "--end_planning", type=check_if_date, help="End of the planning. If not given, the value given in de settings file is taken", ) parser.add_argument( "--weeks_margin_left", type=int, help="Shifts start of planning with this number of weeks left without adding projects. Default is read " "from the settings file. This value overrides the default", ) parser.add_argument( "--weeks_margin_right", type=int, help="Shifts start of planning with this number of weeks right without adding projects. Default is read " "from the settings file. This value overrides the default.", ) parser.add_argument("--suffix", help="Add a suffix to the final output filename ") parser.add_argument("--pdf", help="Save the svg also as pdf ", action="store_true") return parser.parse_args(args)
[docs] def setup_logging(loglevel: int): """Setup basic logging Args: loglevel (int): minimum loglevel for emitting messages """ if loglevel == logging.DEBUG: log_format = "[%(levelname)5s]:%(filename)s/%(lineno)d: %(message)s" else: log_format = "[%(levelname)s] %(message)s" logging.basicConfig( level=loglevel, stream=sys.stdout, format=log_format, datefmt="%Y-%m-%d %H:%M:%S", )
[docs] def check_if_items_are_available( requested_items: list, available_items: dict, label: str = "" ): """ Check is the passed items in the list are available in the keys of the dictionary Args: requested_items (list): All requested items in the list available_items (dict): The dictionary with the keys for which the check is performed label (str, optional): Used for information to the screen """ unique_available_items = set(list(available_items.keys())) if missing_items := set(requested_items).difference(unique_available_items): raise ValueError( f"The {label} {missing_items} are not defined in the settings file.\n" f"The following keys are available: {unique_available_items}" ) return True
[docs] def get_projects_from_arguments(projects_args) -> list or None: """ Get the projects from the command line arguments and return a list Args: projects_args (list or None): The list of projects may contain comma-separated values Returns: list or None: The list of projects """ projects = None if projects_args is not None: projects = list() for project in projects_args: all_projects = project.split(",") projects.extend(all_projects) return projects
[docs] def make_banner(width=80) -> None: """ Make a banner with the start time Args: width (int, optional): Width of the banner. Defaults to 80 """ print("-" * width) exe = Path(sys.argv[0]).stem now = datetime.now() print( f"Start '{exe} {' '.join(sys.argv[1:])}'\nat {now.date()} {now.time().strftime('%H:%M')} " ) print("-" * width)
[docs] def main(args): """Wrapper allowing: func:`postal_code2nuts` to be called with string arguments in a CLI fashion Instead of returning the value from: func:`postal_code2nuts`, it prints the result to the ``stdout`` in a nicely formatted message. Args: args (List[str]): command line parameters as a list of strings (for example, ``["--verbose", "42"]``). """ args = parse_args(args) if args.loglevel < logging.WARNING: make_banner() setup_logging(args.loglevel) if args.very_verbose: gantt_logger = logging.getLogger("Gantt") gantt_logger.setLevel(args.loglevel) _logger.info("Reading settings file {}".format(args.settings_filename)) with codecs.open(args.settings_filename, "r", encoding="UTF-8") as stream: settings = yaml.load(stream=stream, Loader=yaml.Loader) general_settings = settings["general"] try: project_settings_per_project_leader = settings[ "project_settings_file_per_employee" ] except KeyError as err: _logger.warning(err) raise KeyError( "Entry project_settings_file_per_employee not found. Are you sure this" "is the main settingsfile and not the settings file of an employee?" ) period_info = settings["periods"] dayfirst = general_settings["dayfirst"] if args.scale is not None: scale_key = args.scale else: scale_key = general_settings.get("scale", "daily") scale = SCALES[scale_key] if args.start_planning is None: start = parse_date(general_settings["planning_start"], dayfirst=dayfirst) else: start = parse_date(args.start_planning, dayfirst=dayfirst) if args.end_planning is None: end = parse_date(general_settings["planning_end"], dayfirst=dayfirst) else: end = parse_date(args.end_planning, dayfirst=dayfirst) if args.weeks_margin_left is None: weeks_margin_left = general_settings.get("weeks_margin_left") else: weeks_margin_left = args.weeks_margin_left if args.weeks_margin_right is None: weeks_margin_right = general_settings.get("weeks_margin_right") else: weeks_margin_right = args.weeks_margin_right programma_title = general_settings["title"] programma_color = general_settings.get("color") vacation_color = general_settings.get("vacation_color", programma_color) output_directories = general_settings.get("output_directories") excel_info = settings.get("excel") employees_info = get_info_from_file_or_settings(settings=settings, key="employees") vacations_info = get_info_from_file_or_settings(settings=settings, key="vacations") progress_file_info = get_info_from_file_or_settings( settings=settings, key="progress_file_definitions" ) if args.employee: all_employees = get_pasted_employees( args.employee, employees_info=employees_info ) programma_title += f"/{all_employees}" if args.filter_employees: all_filter_employees = get_pasted_employees( args.filter_employees, employees_info=employees_info ) programma_title += f": {all_filter_employees}" filter_projects = get_projects_from_arguments(args.projects) fill = "black" stroke = "black" stroke_width = 0 font_family = "Verdana" if font_info := general_settings.get("font_info"): fill = font_info.get("fill", fill) stroke = font_info.get("stroke", stroke) stroke_width = font_info.get("stroke_width", stroke_width) font_family = font_info.get("font_family", font_family) if custom_colors := general_settings.get("custom_colors"): set_custom_colors(custom_colors=custom_colors) vacations_title_default = "Vacations" if country_code := general_settings.get("country_code"): locale.setlocale(locale.LC_TIME, country_code) if country_code.startswith("nl"): vacations_title_default = "Vakanties" vacations_title = general_settings.get("vacations_title", vacations_title_default) if output_directories is not None: planning_directory = Path(output_directories.get("planning", ".")) resources_directory = Path(output_directories.get("resources", ".")) excel_directory = Path(output_directories.get("excel", ".")) vacations_directory = Path(output_directories.get("vacations", ".")) else: planning_directory = Path(".") resources_directory = Path(".") excel_directory = Path(".") vacations_directory = Path(".") if args.employee is not None: check_if_items_are_available( requested_items=args.employee, available_items=project_settings_per_project_leader, label="employee project", ) if args.filter_employees is not None: check_if_items_are_available( requested_items=args.filter_employees, available_items=employees_info, label="employee task", ) if args.period is not None: check_if_items_are_available( requested_items=args.period, available_items=period_info, label="period" ) # read the settings file per employee settings_per_project_leader = {} for ( project_leader_key, employee_settings_file, ) in project_settings_per_project_leader.items(): if args.employee is not None and project_leader_key not in args.employee: _logger.debug( f"Skip reading settings file for employee {project_leader_key}" ) continue _logger.info( f"Reading settings file {employee_settings_file} of employee {project_leader_key}" ) with codecs.open(employee_settings_file, encoding="UTF-8") as stream: settings_per_project_leader[project_leader_key] = yaml.load( stream=stream, Loader=yaml.Loader ) if args.output_filename is None: output_filename = Path(args.settings_filename).with_suffix(".svg") # add employees from input arguments to output file name if args.employee is not None: output_filename = extend_suffix( output_filename=output_filename, extensions=args.employee ) # add filtered employees from input arguments to output file name if args.filter_employees is not None: extensions = ["contributors"] + sorted(args.filter_employees) output_filename = extend_suffix( output_filename=output_filename, extensions=extensions ) else: # Treat the output filename as defined on the command line output_filename = Path(args.output_filename).with_suffix(".svg") today = None try: today_reference = general_settings["reference_date"] except KeyError: _logger.debug("No date found") else: if today_reference is not None: if today_reference == "today": today = datetime.today().date() _logger.debug("Setting date to today {}".format(today)) else: today = parse_date(today_reference, dayfirst=dayfirst) _logger.debug("Setting date to {}".format(today)) else: _logger.debug("today key found be no date defined") # Start the planning planning = ProjectPlanner( programma_title=programma_title, vacations_title=vacations_title, programma_color=programma_color, vacation_color=vacation_color, output_file_name=output_filename, planning_start=start, planning_end=end, weeks_margin_left=weeks_margin_left, weeks_margin_right=weeks_margin_right, today=today, dayfirst=dayfirst, scale=scale, period_info=period_info, excel_info=excel_info, details=args.details, filter_employees=args.filter_employees, save_svg_as_pdf=args.pdf, collaps_tasks=args.collaps_tasks, periods=args.period, progress_file_info=progress_file_info, ) # add global information, vacations and employees planning.add_global_information( fill=fill, stroke=stroke, stroke_width=stroke_width, font_family=font_family ) planning.add_vacations(vacations_info=vacations_info) planning.add_employees(employees_info=employees_info) # Add the general tasks per employee. It is not mandatory to add tasks_and_milestones, # however, you may. The advantage is that multiply tasks can share the same milestone for ( project_leader_key, project_leader_settings, ) in settings_per_project_leader.items(): if tasks_and_milestones_info := project_leader_settings.get( "tasks_and_milestones" ): _logger.info(f"Adding global tasks en milestones of {project_leader_key} ") variables_info = project_leader_settings.get("variables") planning.add_tasks_and_milestones( tasks_and_milestones_info=tasks_and_milestones_info, variables_info=variables_info, ) # Voeg nu de projecten per employee toe. for ( project_leader_key, project_leader_settings, ) in settings_per_project_leader.items(): if args.employee is not None and project_leader_key not in args.employee: _logger.debug(f"Skip employee {project_leader_key}") continue project_employee_info = project_leader_settings["general"] subprojects_info = project_leader_settings["projects"] variables_info = project_leader_settings.get("variables") progress_info = project_leader_settings.get("progress_files") subprojects_selection = project_employee_info["projects"] subprojects_title = project_employee_info["title"] subprojects_color = project_employee_info.get("color") if filter_projects is not None: # in case a project is given on the command line argument, only allow projects given on the filter. subprojects_selection = list( set(subprojects_selection).intersection(set(filter_projects)) ) if subprojects_selection: planning.make_projects( project_leader_key=project_leader_key, subprojects_info=subprojects_info, subprojects_selection=subprojects_selection, subprojects_title=subprojects_title, subprojects_color=subprojects_color, variables_info=variables_info, ) # Everything has been added to the planning. Write it to file planning.write_planning( write_resources=args.resources, write_vacations=args.vacations, planning_output_directory=planning_directory, resource_output_directory=resources_directory, vacations_output_directory=vacations_directory, periods=args.period, suffix=args.suffix, ) if args.export_to_xlsx is not None: if args.export_to_xlsx[0]: excel_output_formats = [out for out in args.export_to_xlsx[0]] else: excel_output_formats = ["all"] planning.export_to_excel( excel_output_directory=excel_directory, excel_output_formats=excel_output_formats, )
[docs] def run(): """Calls: func:`main` passing the CLI arguments extracted from: obj:`sys.argv` This function can be used as an entry point to create console scripts with setuptools. """ main(sys.argv[1:])
if __name__ == "__main__": # ^ This is a guard statement that will prevent the following code from # being executed in the case someone imports this file instead of # executing it as a script. # https://docs.python.org/3/library/__main__.html # After installing your project with pip, users can also run your Python # modules as scripts via the ``-m`` flag, as defined in PEP 338:: # # python -m gantt_projectplanner.skeleton 42 # run()