PKѻF : : cahier/io.py#!/usr/bin/env python3 # Copyright 2014-2015 Louis Paternault # # Cahier is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Cahier is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero Public License for more details. # # You should have received a copy of the GNU Affero Public License # along with Cahier. If not, see . """Input/Output utils.""" import datetime def ask(message, choices=None, default=None, casesensitive=True): """Ask for user input, and return it. Arguments: - message: message to print; - default: returned value is no input is given; - choices: if not None, keep asking user until input is an item of it; - casesensitive: if True, choices argument is case sensitive. """ if choices: message += " ({})".format(", ".join(choices)) else: choices = [] if default: print("TAGADA") message += " [{default}]".format( default=default, ) message += ": " if casesensitive: lc_choices = {} else: lc_choices = {s.lower():s for s in choices} while True: try: choice = input(message) except EOFError: print() raise if choice == "" and default: return default if not choices: return choice if choice in choices: return choice if (not casesensitive) and (choice in lc_choices): return lc_choices[choice] def ask_date(message, default): """Ask for user input, which is expected to be a date. This function keeps asking until valid date is provided. Arguments: - message: message displayed to user; - default: default date to return if no input is given. """ message += " (format: 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS')" while True: proposition = ask( message, default=default.strftime("%Y-%m-%d"), ) try: date = datetime.datetime.strptime( proposition, "%Y-%m-%d %H:%M:%S", ) break except ValueError: try: date = datetime.datetime.strptime( proposition, "%Y-%m-%d", ) break except ValueError: pass return date def ask_yesno(message, default): """Ask for yes or no.""" if default: default = 'yes' else: default = 'no' while True: proposition = ask( message, default=default, ) if 'yes'.startswith(proposition.lower()): return True if 'no'.startswith(proposition.lower()): return False PKʻF:cahier/config.py#!/usr/bin/env python3 # Copyright 2014-2015 Louis Paternault # # Cahier is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Cahier is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero Public License for more details. # # You should have received a copy of the GNU Affero Public License # along with Cahier. If not, see . """Management of configuration files.""" import configparser import os import shlex from cahier import errors class ConfigurationError(errors.CahierError): """Error in configuration files.""" def __init__(self, filename, message): super(ConfigurationError, self).__init__() self.filename = filename self.message = message def __str__(self): return "Configuration error ({filename}): {message}".format( filename=self.filename, message=self.message, ) def config_assert_has(config, name, section, option): """Assert that 'config' as 'section' defined, with 'option' key in it. Raise a ConfigurationError() if not. """ if not config.has_section(section): raise ConfigurationError( filename=name, message=( "Missing section {section} in file {filename}." ).format(section=section, filename=name), ) if option and (not config.has_option(section, option)): raise ConfigurationError( filename=name, message=( "Missing key {key} in section {section} in file {filename}." ).format(key=option, section=section, filename=name) ) def load_cahierrc(filename): """Load ~/.cahier/cahier.cfg, and return it as a configparser object.""" config = configparser.ConfigParser() config.read_dict({ 'options': { 'casesensitive': "True", }, 'bin': { 'editor': "$EDITOR", }, 'wiki': { 'extension': "mdwn", }, }) config.read([filename]) if config.has_section('wiki'): if 'fileformat' not in config['wiki'].keys(): raise ConfigurationError( filename=filename, message="missing key 'fileformat' in section 'wiki'.", ) else: raise ConfigurationError( filename=filename, message="missing section 'wiki'.", ) return config def load_ftplugin(filename): """Load ftplugin. Return plugin 'filename' as a configparser object. """ # Reading file config = configparser.ConfigParser() config.read_dict({ 'preprocess': {}, }) config.read([filename]) # Checking arguments for key in config['preprocess']: if key == 'name' or key.startswith('cmd'): continue raise ConfigurationError( filename=filename, message=( """"{key}" key (in section {section}) must """ """be 'name' or start with 'cmd'.""" ).format(key=key, section="preprocess") ) return config def load_profiles(dirname): """Load profiles of directory 'dirname'. Return a dictionary of profiles, as configparser objects indexed by basenames. """ profiles = {} for root, __ignored, files in os.walk(dirname): for filename in files: if filename.endswith('.cfg'): # Preprocessing basename = filename[:-len('.cfg')] fullname = os.path.join(root, filename) # Reading configuration config = configparser.ConfigParser() config.read_dict({ 'options': { 'workdays': "", }, }) config.read([fullname]) # Checking for compulsory arguments config_assert_has(config, fullname, 'directories', 'calendar') config_assert_has(config, fullname, 'config', 'setup') workdays = shlex.split(config['options']['workdays']) if len(workdays) != len(set([ day.split(':')[0] for day in workdays ])): raise ConfigurationError( fullname, ( "Only one item by day is allowed in " "key 'workdays' of section 'options'." ), ) profiles[basename] = config return profiles PKFDS ddcahier/__init__.py#!/usr/bin/env python3 # Copyright 2014-2015 Louis Paternault # # Cahier is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Cahier is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero Public License for more details. # # You should have received a copy of the GNU Affero Public License # along with Cahier. If not, see . """Aide à la gestion de cahier de texte. """ import os VERSION = "0.1.1" CAHIERRC = os.path.expanduser('~/.cahier') __COPYRIGHT__ = "(C) 2014-2015 Louis Paternault. GNU GPL 3 or later." PKػFcahier/errors.py#!/usr/bin/env python3 # Copyright 2014-2015 Louis Paternault # # Cahier is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Cahier is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero Public License for more details. # # You should have received a copy of the GNU Affero Public License # along with Cahier. If not, see . """Error definition.""" class CahierError(Exception): """General exception. Other custom exception should inherit from it.""" def __init__(self, message): super().__init__() self.message = message def __str__(self): return self.message class UnknownProfile(CahierError): """Profile cannot be found.""" def __init__(self, profile): super().__init__() self.profile = profile def __str__(self): return """No profile named "{profile}".""".format(profile=self.profile) PKF[cahier/path.py#!/usr/bin/env python3 # Copyright 2014-2015 Louis Paternault # # Cahier is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Cahier is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero Public License for more details. # # You should have received a copy of the GNU Affero Public License # along with Cahier. If not, see . """Path utils.""" import os import cahier def expand(line, filename): """Return a line of a configuration file, while processing 'filename'. The line is formatted with the following variables: - configdir: configuration directory (something liki '~/.cahier'); - basename: basename of 'filename' (without extension); - filename: same, with extension; - dirname: directory of 'filename'. """ extension = filename.split('.')[-1] if extension == filename: extension = "" else: extension = '.{}'.format(extension) return os.path.expandvars(os.path.expanduser(line.format( configdir=cahier.CAHIERRC, basename=filename[0:-len(extension)], filename=filename, dirname=absfullpath(os.path.dirname(filename)), ))) def absfullpath(path): """Return an absolute version of 'path'. User directory ('~') and environment variables are replaced by their values. """ return os.path.abspath(os.path.expanduser(os.path.expandvars(path))) PKF_ʙcahier/main.py#!/usr/bin/env python3 # Copyright 2014-2015 Louis Paternault # # Cahier is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Cahier is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero Public License for more details. # # You should have received a copy of the GNU Affero Public License # along with Cahier. If not, see . """Fonction principale""" import argparse import logging import os import shlex import sys from cahier import errors import cahier import cahier.cmd_plugins import cahier.config import cahier.io import cahier.path LOGGER = logging.getLogger(cahier.__name__) LOGGER.addHandler(logging.StreamHandler()) def select_profile(option_profile, profiles, confirm, casesensitive=False): """Return the selected profile. Arguments: - option_profile: profile set in command line arguments (None if none was given). - profiles: dictionary of profiles, as returned by cahier.config.load_profiles(). - confirm (boolean): True iff force user to confirm (unless profile is set). - casesensitive: is profile chooser case sensitive? """ # pylint: disable=too-many-branches # Is command line argument profile a valid one? if option_profile: for item in profiles.keys(): if option_profile.lower() == item.lower(): return item raise errors.UnknownProfile(option_profile) # Looking for matching paths in profile configuration cwd = os.path.abspath(os.getcwd()) match = {} for (profile, config) in profiles.items(): if config.has_option('directories', "sources"): for path in shlex.split(config['directories']['sources']): fullpath = cahier.path.absfullpath(path) if cwd.startswith(fullpath): if profile in match: if len(fullpath) > match[profile]: match[profile] = len(fullpath) else: match[profile] = len(fullpath) match = [t[0] for t in sorted(match.items(), key=lambda t: -t[1])] # match now contains the list of matching directories. If only one matches, # it is the one. Else, user has to choose. if len(match) == 0: return cahier.io.ask( "Select profile", choices=profiles.keys(), default=None, casesensitive=casesensitive, ) elif len(match) == 1: if confirm: return cahier.io.ask( "Select profile", choices=profiles.keys(), default=match[0], casesensitive=casesensitive, ) else: return match[0] else: return cahier.io.ask( "Select profile", choices=match, default=match[0], casesensitive=casesensitive, ) def commandline_parser(): """Return an argument parser""" parent = argparse.ArgumentParser(add_help=False) parent.add_argument( '-a', '--ask', action='store_true', help='Force asking profile and filename (if relevant).', ) parent.add_argument( '-p', '--profile', action='store', nargs=1, help='Force profile', ) parser = argparse.ArgumentParser( description="Manage ikiwiki calendar items.", parents=[parent], ) parser.add_argument( '--version', action='version', version=( '%(prog)s ' + cahier.VERSION ) ) cahier.cmd_plugins.load_commands(parser.add_subparsers(), [parent]) return parser def main(): """Main function.""" try: LOGGER.setLevel(logging.INFO) options = commandline_parser().parse_args(sys.argv[1:]) if not hasattr(options, "function"): raise errors.CahierError("A subcommand is required.") config = cahier.config.load_cahierrc( os.path.join(cahier.CAHIERRC, 'cahier.cfg') ) profiles = cahier.config.load_profiles( os.path.join(cahier.CAHIERRC, 'profiles') ) if options.profile: options.profile = options.profile[0] profile = select_profile( options.profile, profiles, options.ask, config['options'].getboolean('casesensitive'), ) sys.exit(options.function(config, options, profiles[profile])) except errors.CahierError as error: LOGGER.error("Error: " + str(error) + "\n") sys.exit(1) except EOFError: sys.exit(0) if __name__ == "__main__": main() PKۻF;aacahier/cmd_plugins/__init__.py#!/usr/bin/env python3 # Copyright 2014-2015 Louis Paternault # # Cahier is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Cahier is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero Public License for more details. # # You should have received a copy of the GNU Affero Public License # along with Cahier. If not, see . """Command plugin management. Subcommands are defined as plugins, which are submodules of this one. Function 'load_commands()' is called on each modules, to let it add subcommands to the parser. """ import glob import importlib import os def load_commands(subparsers, parent=None): """Load all commands.""" for name in glob.glob(os.path.join(os.path.dirname(__file__), "*.py")): if name.endswith(".py") and os.path.basename(name) != "__init__.py": plugin = importlib.import_module( 'cahier.cmd_plugins.{}'.format(os.path.basename(name[:-len('.py')])) #pylint: disable=line-too-long ) plugin.load_plugin(subparsers, parent) PKFu&wE  cahier/cmd_plugins/ikiwiki.py#!/usr/bin/env python3 # Copyright 2014-2015 Louis Paternault # # Cahier is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Cahier is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero Public License for more details. # # You should have received a copy of the GNU Affero Public License # along with Cahier. If not, see . """Define ikiwiki related command line subcommands.""" import argparse import os import shlex import subprocess import cahier.path def run_ikiwiki(config, profile, arguments): """Run ikiwiki command. Arguments: - config: configparser object corresponding to ~/.cahier/cahierrc.cfg; - profile: current profile; - arguments: arguments to pass to IkiWiki (may be None). """ # Building command command = ['ikiwiki'] command.extend(['--setup', os.path.basename(profile['config']['setup'])]) if config.has_option('wiki', 'options'): command.extend((shlex.split(config['wiki']['options']))) if arguments: command.extend(arguments) # Running command process = subprocess.Popen( command, cwd=cahier.path.absfullpath( os.path.dirname(profile['config']['setup']) ), ) return process.wait() def do_wiki(config, options, profile): """Run 'wiki' command.""" return run_ikiwiki(config, profile, options.args) def do_refresh(config, options, profile): """Run 'refresh' command.""" return run_ikiwiki(config, profile, ['--refresh'] + options.args) def do_rebuild(config, options, profile): """Run 'rebuild' command.""" return run_ikiwiki(config, profile, ['--rebuild'] + options.args) def load_plugin(subparsers, __ignored): """Load current plugin. Add parsers to argument, with appropriate actions to make them effective. Arguments: - subparsers: object to complete with custom commands; - parent: may be None. """ # Parser for 'wiki' subcommand parser_wiki = subparsers.add_parser( 'wiki', help='Run IkiWiki.', ) parser_wiki.set_defaults(function=do_wiki) parser_wiki.add_argument( 'args', nargs=argparse.REMAINDER, help="Arguments to pass to IkiWiki", default=None, ) # Parser for 'refresh' subcommand parser_refresh = subparsers.add_parser( 'refresh', help='Alias for "wiki --refresh"', aliases=['fresh'], ) parser_refresh.set_defaults(function=do_refresh) parser_refresh.add_argument( 'args', nargs=argparse.REMAINDER, help="Arguments to pass to IkiWiki", default=None, ) # Parser for 'rebuild' subcommand parser_rebuild = subparsers.add_parser( 'rebuild', help='Alias for "wiki --rebuild"', ) parser_rebuild.set_defaults(function=do_rebuild) parser_rebuild.add_argument( 'args', nargs=argparse.REMAINDER, help="Arguments to pass to IkiWiki", default=None, ) PKFcahier/cmd_plugins/git.py#!/usr/bin/env python3 # Copyright 2014-2015 Louis Paternault # # Cahier is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Cahier is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero Public License for more details. # # You should have received a copy of the GNU Affero Public License # along with Cahier. If not, see . """Define git command line subcommand.""" import argparse import subprocess import cahier.path def do_git(__ignored, options, profile): """Run git command.""" # Building command command = ['git'] if options.args: command.extend(options.args) # Running command process = subprocess.Popen( command, cwd=cahier.path.absfullpath( profile['directories']['calendar'] ), ) return process.wait() def load_plugin(subparsers, __ignored): """Load current plugin. Add parsers to argument, with appropriate actions to make them effective. Arguments: - subparsers: object to complete with custom commands; - _: unused. """ # Parser for 'git' subcommand parser_git = subparsers.add_parser('git', help='Run git.') parser_git.set_defaults(function=do_git) parser_git.add_argument( 'args', nargs=argparse.REMAINDER, help="Arguments to pass to git", default=None, ) PKFsfK5K5cahier/cmd_plugins/date.py#!/usr/bin/env python3 # Copyright 2014-2015 Louis Paternault # # Cahier is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Cahier is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero Public License for more details. # # You should have received a copy of the GNU Affero Public License # along with Cahier. If not, see . """Date-related options (new, edit, show, attach).""" import argparse import datetime import heapq import logging import os import shlex import shutil import subprocess import cahier LOGGER = logging.getLogger(__name__) WEEKDAYS = [ 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday', ] def last_items(profile, config, number=1): """Return the 'number' last items of 'profile'. """ recent = [] extension = config['wiki']['extension'] fileformat = config['wiki']['fileformat'] for filename in os.listdir( cahier.path.absfullpath(profile['directories']['calendar']) ): if filename.endswith('.' + extension): if config.has_option('wiki', 'fileformat-length'): length = config['wiki']['fileformat-length'] stripped_filename = filename[0:int(length)] else: stripped_filename = filename[:-(len(extension)+1)] date = datetime.datetime.strptime(stripped_filename, fileformat) heapq.heappush(recent, (date.timestamp(), (date, filename))) if number != -1 and len(recent) > number: heapq.heappop(recent) return [ ( os.path.join( cahier.path.absfullpath(profile['directories']['calendar']), item[1][1], ), item[1][0] ) for item in [heapq.heappop(recent) for _ in range(len(recent))] ] def make_hour(string): """Take an (maybe) incomplete hour, and return an hour matching "%H:%M:%S". """ if string: string = ":".join(string) else: string = "00" while string.count(':') < 2: string += ":00" return string def next_date(date, workdays, skip): """Return the date following date, respecting workdays. Arguments: - date: start date (return the first one after this one, and after today. - workdays: work days, with time. - skip: numbers of matching days to skip. """ day = datetime.timedelta(days=1) date += day if not workdays: today = datetime.date.today() if (date + datetime.timedelta(days=skip)).date() < today: return datetime.date.today() else: return date + datetime.timedelta(days=skip) workdays = dict([ (day[0], make_hour(day[1:])) for day in [day.split(':') for day in shlex.split(workdays)] ]) date -= day while skip > -1: date += day skip -= 1 while WEEKDAYS[date.isoweekday() - 1] not in workdays.keys(): date += day return datetime.datetime.combine( date.date(), datetime.datetime.strptime( workdays[WEEKDAYS[date.isoweekday() - 1]], "%H:%M:%S" ).time() ) def copy_template(filename, date, config, dry): """Copy template for filename. If an appropriate template is defined in configuration, copy it as 'filename', passing 'date' as an argument to string formatting. If not, create an empty file as 'filename'. """ if config.has_option('wiki', 'template'): if dry: LOGGER.info( "Copying template {src} to {dst}.\n".format( src=cahier.path.expand(config['wiki']['template'], filename), dst=filename, ) ) else: template = open( cahier.path.expand(config['wiki']['template'], filename), 'r' ) newfile = open(filename, 'a') newfile.write(template.read().format(date=date)) template.close() newfile.close() else: if dry: LOGGER.info("Creating empty file {}.\n".format(filename)) else: open(filename, 'a').close() def attach_file(filename, destination, plugin_directory, dry): """Attach file to a calendar entry. Arguments: - filename: filename to attach; - destination: directory in which 'filename' will be copied; - plugin_directory: directory of ftplugins, used to (maybe) preprocess filename, - dry: if set, only print what wourd be done. """ extension = filename.split('.')[-1] if extension == filename: extension = None if extension and os.path.exists(os.path.join( plugin_directory, "{}.cfg".format(extension) )): plugin = cahier.config.load_ftplugin(os.path.join( plugin_directory, "{}.cfg".format(extension), )) for command in [option for option in plugin.options('preprocess') if option.startswith('cmd') ]: command = cahier.path.expand( plugin['preprocess'][command], filename ) if dry: LOGGER.info("Running '{}'.\n".format(command)) else: subprocess.call(command, shell=True) filename = cahier.path.expand( plugin['preprocess']['name'], filename ) basename = os.path.basename(filename) if os.path.exists(os.path.join(destination, basename)): if 'n' == cahier.io.ask( """File {basename} already exist in directory {destination}. Overwrite?""".format( #pylint: disable=line-too-long basename=basename, destination=destination ), choices=['y', 'n'], default='n', ): return if dry: LOGGER.info("Copying {} to {}.\n".format(filename, destination)) else: shutil.copy2(filename, destination) def run_editnew(command, config, options, profile): """Run edit or new command. Arguments: - command: command line subcommand to run; - config: configparer object representing ~/.cahierrc; - options: command line options; - profile: current profile, as a configparser object. """ items = last_items(profile, config) last_filename, last_date = items[0] if command == 'edit': filename = last_filename if options.ask: print("\n".join([ os.path.relpath( items[0][:-len('.mdwn')], cahier.path.absfullpath(profile['directories']['calendar']) #pylint: disable=line-too-long ) for items in last_items(profile, config, -1) ])) filename = os.path.join( os.path.dirname(filename), "{}.mdwn".format(cahier.io.ask( "Filename", default=os.path.basename(filename[:-len('.mdwn')]) )), ) elif command == 'new': newdate = next_date( last_date, profile['options']['workdays'], skip=options.skip, ) if options.ask: print("\n".join([ "{:%Y-%m-%d %H:%M:%S}".format(items[1]) for items in last_items(profile, config, -1) ])) cahier.io.ask_date('Date', newdate) filename = os.path.join( cahier.path.absfullpath( profile['directories']['calendar'] ), "{}.mdwn".format( newdate.strftime(config['wiki']['fileformat']) ), ) copy_template(filename, newdate, config, options.dry) if options.dry: LOGGER.info( "Running '{cmd}'.\n".format( cmd=cahier.path.expand(config['bin']['editor'], filename) ) ) else: subprocess.call( cahier.path.expand(config['bin']['editor'], filename), shell=True, ) return 0 def do_new(config, options, profile): """Run command "new".""" return run_editnew('new', config, options, profile) def do_edit(config, options, profile): """Run command "edit".""" return run_editnew('edit', config, options, profile) def do_show(config, options, profile): """Run command "show".""" items = last_items(profile, config, options.s) print('\n'.join([ "{0}: {1:%c}".format(*item) for item in [ ( os.path.relpath( item[0], cahier.path.absfullpath(profile['directories']['calendar']) ), item[1], ) for item in items ] ])) return 0 def do_attach(config, options, profile): """Run command "attach".""" items = last_items(profile, config) last_filename = items[0][0] if options.ask: print("\n".join([ os.path.relpath( items[0][:-len('.mdwn')], cahier.path.absfullpath(profile['directories']['calendar']), ) for items in last_items(profile, config, -1) ])) last_filename = os.path.join( os.path.dirname(last_filename), "{}.mdwn".format(cahier.io.ask( "Entry", default=os.path.basename(last_filename[:-len('.mdwn')]) )), ) dirname = last_filename[0:-len('.mdwn')] if not os.path.exists(dirname): if options.dry: LOGGER.info("Creating directory {}.\n".format(dirname)) else: os.mkdir(dirname) for filename in options.files: attach_file( filename, dirname, os.path.join(cahier.CAHIERRC, "ftplugins"), options.dry, ) return 0 def do_rm(config, options, profile): """Run command "remove".""" items = last_items(profile, config) if options.dry: LOGGER.info("Removing {}.\n".format(items[0][0])) else: if ( options.force or ((not options.force) and cahier.io.ask_yesno( 'Remove {} ?'.format(os.path.relpath( items[0][0], cahier.path.absfullpath(profile['directories']['calendar']) #pylint: disable=line-too-long )), True, ) ) ): os.remove(items[0][0]) return 0 def load_plugin(subparsers, parent): """Load current plugin. Add parsers to argument, with appropriate actions to make them effective. Arguments: - subparsers: object to complete with custom commands; - parent: may be None. """ # Common options common = argparse.ArgumentParser(add_help=False) common.add_argument( '-n', '--dry-run', action='store_true', help='Does nothing, but print entry that would be done.', dest='dry', default=False, ) # Parser for 'new' subcommand parser_new = subparsers.add_parser( 'new', help='Add new date item.', parents=parent + [common], ) parser_new.set_defaults(function=do_new) parser_new.add_argument( '-s', '--skip', metavar='NUMBER', action='store', type=int, help='Number of entries to skip.', default=0 ) # Parser for 'edit' subcommand parser_edit = subparsers.add_parser( 'edit', help='Edit last entry.', parents=parent + [common], ) parser_edit.set_defaults(function=do_edit) # Parser for 'show' subcommand parser_show = subparsers.add_parser( 'show', help='Show last entry.', parents=parent + [common], ) parser_show.set_defaults(function=do_show) parser_show.add_argument( '-s', metavar='NUMBER', action='store', type=int, help='Number of entries to show.', default=1 ) # Parser for 'attach' subcommand parser_attach = subparsers.add_parser( 'attach', help='Attach file to current entry.', parents=parent + [common], ) parser_attach.set_defaults(function=do_attach) parser_attach.add_argument( 'files', nargs='+', help="Files to attach to latest date." ) # Parser for 'rm' subcommand parser_rm = subparsers.add_parser( 'rm', help='Remove the last entry', parents=parent + [common], ) parser_rm.add_argument( '-f', dest='force', action='store_true', help='Force deletion.', default=False ) parser_rm.set_defaults(function=do_rm) PKFdcahier/cmd_plugins/cd.py#!/usr/bin/env python3 # Copyright 2014-2015 Louis Paternault # # Cahier is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Cahier is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero Public License for more details. # # You should have received a copy of the GNU Affero Public License # along with Cahier. If not, see . """Launch a shell in calendar directory""" import os import shlex import subprocess def do_cd(config, __ignored, profile): """Run a shell in calendar directory Arguments: - config: configparer object representing ~/.cahierrc; - _: command line options; - profile: current profile, as a configparser object. """ if not config.has_option('bin', 'shell'): config['bin']['shell'] = '$SHELL' command = [ os.path.expandvars(os.path.expanduser(item)) for item in shlex.split(config['bin']['shell']) ] process = subprocess.Popen( command, cwd=os.path.expandvars(os.path.expanduser( profile['directories']['calendar'] )), ) return process.wait() def load_plugin(subparsers, parent): """Load current plugin. Add parsers to argument, with appropriate actions to make them effective. Arguments: - subparsers: object to complete with custom commands; - parent: may be None. """ # Parser for 'new' subcommand parser_cd = subparsers.add_parser( 'cd', help='Run a shell in calendar directory.', parents=parent ) parser_cd.set_defaults(function=do_cd) PK۳9HKݍTT&Cahier-0.1.1.dist-info/DESCRIPTION.rstcahier — One-directory-a-day calendar management ================================================ |sources| |pypi| |documentation| |license| .. note:: I am no longer using this software, so it will not be improved. Feel free to ask questions and to submit bugs anyway, but this will not be my priority. -- Louis What's new? ----------- See `changelog `_. Download and install -------------------- See the end of list for a (quick and dirty) Debian package. * From sources: * Non-Python dependency: Although this program can be used without them, it has be built to be used with `git `_ and `ikiwiki `_. * Download: https://pypi.python.org/pypi/cahier * Install (in a `virtualenv`, if you do not want to mess with your distribution installation system):: python3 setup.py install * From pip:: pip install cahier * Quick and dirty Debian (and Ubuntu?) package This requires `stdeb `_ to be installed:: python3 setup.py --command-packages=stdeb.command bdist_deb sudo dpkg -i deb_dist/cahier-_all.deb Documentation ------------- * The compiled documentation is available on `readthedocs `_ * To compile it from source, download and run:: cd doc && make html .. |documentation| image:: http://readthedocs.org/projects/cahier/badge :target: http://cahier.readthedocs.org .. |pypi| image:: https://img.shields.io/pypi/v/cahier.svg :target: http://pypi.python.org/pypi/cahier .. |license| image:: https://img.shields.io/pypi/l/cahier.svg :target: http://www.gnu.org/licenses/gpl-3.0.html .. |sources| image:: https://img.shields.io/badge/sources-cahier-brightgreen.svg :target: http://git.framasoft.org/spalax/cahier PKҳ9Hb--'Cahier-0.1.1.dist-info/entry_points.txt[console_scripts] cahier = cahier.main:main PK۳9HzD$Cahier-0.1.1.dist-info/metadata.json{"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Intended Audience :: End Users/Desktop", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Topic :: Utilities"], "extensions": {"python.commands": {"wrap_console": {"cahier": "cahier.main:main"}}, "python.details": {"contacts": [{"email": "spalax@gresille.org", "name": "Louis Paternault", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://git.framasoft.org/spalax/cahier"}}, "python.exports": {"console_scripts": {"cahier": "cahier.main:main"}}}, "generator": "bdist_wheel (0.26.0)", "license": "GPLv3 or any later version", "metadata_version": "2.0", "name": "Cahier", "summary": "One-directory-a-day calendar management", "version": "0.1.1"}PKҳ9H]*6$Cahier-0.1.1.dist-info/top_level.txtcahier PK۳9H}\\Cahier-0.1.1.dist-info/WHEELWheel-Version: 1.0 Generator: bdist_wheel (0.26.0) Root-Is-Purelib: true Tag: py3-none-any PK۳9H/#N N Cahier-0.1.1.dist-info/METADATAMetadata-Version: 2.0 Name: Cahier Version: 0.1.1 Summary: One-directory-a-day calendar management Home-page: https://git.framasoft.org/spalax/cahier Author: Louis Paternault Author-email: spalax@gresille.org License: GPLv3 or any later version Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) Classifier: Intended Audience :: End Users/Desktop Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Topic :: Utilities cahier — One-directory-a-day calendar management ================================================ |sources| |pypi| |documentation| |license| .. note:: I am no longer using this software, so it will not be improved. Feel free to ask questions and to submit bugs anyway, but this will not be my priority. -- Louis What's new? ----------- See `changelog `_. Download and install -------------------- See the end of list for a (quick and dirty) Debian package. * From sources: * Non-Python dependency: Although this program can be used without them, it has be built to be used with `git `_ and `ikiwiki `_. * Download: https://pypi.python.org/pypi/cahier * Install (in a `virtualenv`, if you do not want to mess with your distribution installation system):: python3 setup.py install * From pip:: pip install cahier * Quick and dirty Debian (and Ubuntu?) package This requires `stdeb `_ to be installed:: python3 setup.py --command-packages=stdeb.command bdist_deb sudo dpkg -i deb_dist/cahier-_all.deb Documentation ------------- * The compiled documentation is available on `readthedocs `_ * To compile it from source, download and run:: cd doc && make html .. |documentation| image:: http://readthedocs.org/projects/cahier/badge :target: http://cahier.readthedocs.org .. |pypi| image:: https://img.shields.io/pypi/v/cahier.svg :target: http://pypi.python.org/pypi/cahier .. |license| image:: https://img.shields.io/pypi/l/cahier.svg :target: http://www.gnu.org/licenses/gpl-3.0.html .. |sources| image:: https://img.shields.io/badge/sources-cahier-brightgreen.svg :target: http://git.framasoft.org/spalax/cahier PK۳9HCahier-0.1.1.dist-info/RECORDCahier-0.1.1.dist-info/DESCRIPTION.rst,sha256=AttlyaWuu8k31RY0N0xnR6WHXltLRREW91hsj4VTmFE,1876 Cahier-0.1.1.dist-info/METADATA,sha256=6FiAuQRnBtV_6jUJn4yVliVqoIYzEcoUbxE9vjuPNx0,2638 Cahier-0.1.1.dist-info/RECORD,, Cahier-0.1.1.dist-info/WHEEL,sha256=zX7PHtH_7K-lEzyK75et0UBa3Bj8egCBMXe1M4gc6SU,92 Cahier-0.1.1.dist-info/entry_points.txt,sha256=Q5j1nd0EkdsRS1hsS-RvcnFulKEgOysNJztkHWeiZGQ,45 Cahier-0.1.1.dist-info/metadata.json,sha256=3q99vSX2pQ7hrHFRD7OuLxDsoRzuMaSLrYVByAlwDq4,1015 Cahier-0.1.1.dist-info/top_level.txt,sha256=j8iIZRfHTI2Pvc51dm2dGn5Jy4NhezGU0l4lRsYs2Oo,7 cahier/__init__.py,sha256=_tzJ19sDlfsOtWT7SCyUmnP5zXv7ldaiZlCcVtZA_sc,868 cahier/config.py,sha256=0-XFH4TWzLZejzbNXXb6nZSCp4Duj7w8kGk_dMnYXBo,5021 cahier/errors.py,sha256=AQbKu7wB2-forW4ab-_6oIO9l1hNJriSe3yhtyywBMI,1220 cahier/io.py,sha256=ryXaHqO3QcQGtvl-Y5hvsQgDUrhPvK5bnvE1_Lmm9sQ,3130 cahier/main.py,sha256=YIT3LmUgHwIQH8xMwZD91jIdBnfvX4p6hrb-gQF6H18,5067 cahier/path.py,sha256=3DvWUkqXbIt4Hmc-RyagKcl0iaj1l1i2SI5U7qc_wjk,1720 cahier/cmd_plugins/__init__.py,sha256=YXLogafoxPLHA3I8t0viA7OhxdT-D7YS9vht7mVuOQ0,1377 cahier/cmd_plugins/cd.py,sha256=GP-V7ZMZVBGZ35vggjMyvxw5WpnUmbuunCs-Rmew66k,1930 cahier/cmd_plugins/date.py,sha256=VuZjCoMT23YGf6A3MnJVDDLRWWuoW8OQrQ4_LNMe5gg,13643 cahier/cmd_plugins/git.py,sha256=_knNP00KiUbbk11OpCSAQ5_YiTOxbAENoo5m-xszJjY,1699 cahier/cmd_plugins/ikiwiki.py,sha256=0FFfjz71ZWUECEMIVB7N0mZCgLEpjMxFrYL3viFW6Zw,3357 PKѻF : : cahier/io.pyPKʻF:d cahier/config.pyPKFDS dd/ cahier/__init__.pyPKػF#cahier/errors.pyPKF[(cahier/path.pyPKF_ʙ/cahier/main.pyPKۻF;aaCcahier/cmd_plugins/__init__.pyPKFu&wE  -Icahier/cmd_plugins/ikiwiki.pyPKFVcahier/cmd_plugins/git.pyPKFsfK5K5_]cahier/cmd_plugins/date.pyPKFdcahier/cmd_plugins/cd.pyPK۳9HKݍTT&Cahier-0.1.1.dist-info/DESCRIPTION.rstPKҳ9Hb--':Cahier-0.1.1.dist-info/entry_points.txtPK۳9HzD$Cahier-0.1.1.dist-info/metadata.jsonPKҳ9H]*6$Cahier-0.1.1.dist-info/top_level.txtPK۳9H}\\.Cahier-0.1.1.dist-info/WHEELPK۳9H/#N N ħCahier-0.1.1.dist-info/METADATAPK۳9HOCahier-0.1.1.dist-info/RECORDPK .