コード例 #1
0
ファイル: story.py プロジェクト: Arent128/npc
def latest(thingtype, **kwargs):
    """
    Open the latest plot and/or session file

    Args:
        prefs (Settings): Settings object to use. Uses internal settings by
            default.

    Returns:
        Result object. Openable will contain the path(s) to the requested file(s).
    """
    prefs = kwargs.get('prefs', settings.InternalSettings())
    plot_path = prefs.get('paths.required.plot')
    session_path = prefs.get('paths.required.session')

    if not (path.exists(plot_path) and path.exists(session_path)):
        return result.FSError(
            errmsg="Cannot access paths '{}' and/or '{}'".format(
                plot_path, session_path))

    latest_plot = latest_file_info(plot_path, PLOT_REGEX)
    latest_session = latest_file_info(session_path, SESSION_REGEX)
    if thingtype == 'both':
        openable = [latest_plot['path'], latest_session['path']]
    elif thingtype == 'session':
        openable = [latest_session['path']]
    elif thingtype == 'plot':
        openable = [latest_plot['path']]
    else:
        return result.OptionError(
            errmsg="Unrecognized type '{}'".format(thingtype))

    return result.Success(openable=openable)
コード例 #2
0
def init(create_types=False, create_all=False, **kwargs):
    """
    Create the basic directories for a campaign.

    This will create the directories this tool expects to find within a
    campaign. Other directories are left to the user.

    Args:
        create_types (bool): Whether to create directories for each character
            type
        create_all (bool): Whether to create all optional directories.
        campaign_name (str): Name of the campaign. Defaults to the name of the
            current directory.
        dryrun (bool): Do not create anything. This adds a string of changes
            that would be made to the returned Result object's printables
            variable.
        verbose (bool): Detail all changes made in the Result object's
            printables variable.
        prefs (Settings): Settings object to use. Uses internal settings by
            default.

    Returns:
        Result object. Openable will be empty.
    """
    prefs = kwargs.get('prefs', settings.InternalSettings())
    campaign_name = kwargs.get('campaign_name', path.basename(getcwd()))
    dryrun = kwargs.get('dryrun', False)
    verbose = kwargs.get('verbose', False)

    changelog = []

    def log_change(message):
        if dryrun or verbose:
            changelog.append(message)

    def new_dir(path_name):
        log_change(path_name)
        if not dryrun:
            makedirs(path_name, mode=0o775, exist_ok=True)

    for key, required_path in prefs.get('paths.required').items():
        if key in ["additional_paths"]:
            # create user-specified dirs
            for extra_path in required_path:
                new_dir(extra_path)
            continue
        new_dir(required_path)
    if not path.exists(prefs.get_settings_path('campaign')):
        new_dir('.npc')
        log_change(prefs.get_settings_path('campaign'))
        if not dryrun:
            with open(prefs.get_settings_path('campaign'), 'a') as settings_file:
                json.dump({'campaign_name': campaign_name}, settings_file, indent=4)

    if create_types or create_all:
        cbase = prefs.get('paths.required.characters')
        for type_path in prefs.get_type_paths():
            new_dir(path.join(cbase, type_path))

    return result.Success(printables=changelog)
コード例 #3
0
def report(*tags, search=None, ignore=None, fmt=None, outfile=None, **kwargs):
    """
    Create a report for the given tags

    The tabular report shows how many characters have each unique value for each
    tag.

    Args:
        tag (list): Tag names to report on. Can contain strings and lists of
            strings.
        search (list): Paths to search for character files. Items can be strings
            or lists of strings.
        ignore (list): Paths to ignore
        fmt (str|None): Output format to use. Recognized values are defined in
            formatters.get_report_formatter. Pass "default" or None to get the
            format from settings.
        outfile (string|None): Filename to put the listed data. None and "-"
            print to stdout.
        prefs (Settings): Settings object to use. Uses internal settings by
            default.

    Returns:
        Result object. Openable will contain the output file if given.
    """
    prefs = kwargs.get('prefs', settings.InternalSettings())

    if not search:
        search = ['.']
    if not ignore:
        ignore = []
    ignore.extend(prefs.get_ignored_paths('report'))
    if not fmt or fmt == 'default':
        fmt = prefs.get('report.default_format')

    # use a list so we can iterate more than once
    characters = list(parser.get_characters(flatten(search), ignore))

    # Construct a dict keyed by tag name whose values are Counters. Each Counter
    # is initialized with a flattened list of lists and we let it count the
    # duplicates.
    table_data = {
        tag: Counter(flatten([c.tags.get(tag, 'None') for c in characters]))
        for tag in flatten(tags)
    }

    formatter = formatters.get_report_formatter(fmt)
    if not formatter:
        return result.OptionError(
            errmsg="Cannot create output of format '{}'".format(fmt))
    with util.smart_open(outfile,
                         binary=(fmt in formatters.BINARY_TYPES)) as outstream:
        response = formatter(table_data, outstream=outstream, prefs=prefs)

    # pass errors straight through
    if not response.success:
        return response

    openable = [outfile] if outfile and outfile != '-' else None

    return result.Success(openable=openable)
コード例 #4
0
ファイル: html.py プロジェクト: Arent128/npc
def report(tables, outstream, **kwargs):
    """
    Create one or more html tables

    Table data format:
    The tables arg must be a dictionary of collections.Counter objects indexed
    by the name of the tag whose data is stored in the Counter. The tag name
    will be titleized and used as the header for that column of the report.

    Args:
        tables (dict): Table data to use
        outstream (stream): Output stream
        prefs (Settings): Settings object. Used to get the location of template
            files.
        encoding (string): Encoding format of the output text. Overrides the
            value in settings.
    """
    prefs = kwargs.get('prefs', settings.InternalSettings())
    encoding = kwargs.get('encoding', prefs.get('report.html_encoding'))

    encoding_options = {
        'output_encoding': encoding,
        'encoding_errors': 'xmlcharrefreplace'
    }

    with tempfile.TemporaryDirectory() as tempdir:
        table_template = Template(filename=prefs.get("report.templates.html"),
                                  module_directory=tempdir,
                                  **encoding_options)

        for key, table in tables.items():
            outstream.write(table_template.render(data=table, tag=key))

    return result.Success()
コード例 #5
0
ファイル: markdown.py プロジェクト: Arent128/npc
def report(tables, outstream, **kwargs):
    """
    Create one or more MultiMarkdown tables

    Table data format:
    The tables arg must be a dictionary of collections.Counter objects indexed
    by the name of the tag whose data is stored in the Counter. The tag name
    will be titleized and used as the header for that column of the report.

    Args:
        tables (dict): Table data to use. Tables has a very particular format.
        outstream (stream): Output stream
        prefs (Settings): Settings object. Used to get the location of template
            files.
    """
    prefs = kwargs.get('prefs', settings.InternalSettings())

    with tempfile.TemporaryDirectory() as tempdir:
        table_template = Template(
            filename=prefs.get("report.templates.markdown"),
            module_directory=tempdir)

        for key, table in tables.items():
            outstream.write(table_template.render(data=table, tag=key))

    return result.Success()
コード例 #6
0
def reorg(*search, ignore=None, purge=False, verbose=False, commit=False, **kwargs):
    """
    Move character files into the correct paths.

    Character files are moved so that their path matches the ideal path as
    closely as possible. No new directories are created.

    This function ignores tags not found in Character.KNOWN_TAGS.

    Args:
        search (list): Paths to search for character files. Items can be strings
            or lists of strings.
        ignore (list): Paths to ignore
        purge (bool): Whether empty directories should be deleted after all
            files have been moved.
        verbose (bool): Whether to print changes as they are made
        commit (bool): Whether to actually move files around
        prefs (Settings): Settings object to use. Uses internal settings by
            default.

    Returns:
        Result object. Openable will be empty.
    """
    prefs = kwargs.get('prefs', settings.InternalSettings())
    if not ignore:
        ignore = []
    ignore.extend(prefs.get_ignored_paths('reorg'))
    show_changes = verbose or not commit

    changelog = []

    base_path = prefs.get('paths.required.characters')
    if not path.exists(base_path):
        return result.FSError(errmsg="Cannot access '{}'".format(base_path))

    if show_changes:
        changelog.append("Move characters")
    for parsed_character in parser.get_characters(flatten(search), ignore):
        new_path = util.create_path_from_character(parsed_character, base_path=base_path)
        if path.normcase(path.normpath(new_path)) != path.normcase(path.normpath(path.dirname(parsed_character['path']))):
            if show_changes:
                changelog.append("* Move {} to {}".format(parsed_character['path'], new_path))
            if commit:
                try:
                    shmove(parsed_character['path'], new_path)
                except OSError as e:
                    if show_changes:
                        changelog.append("\t- dest path already exists; skipping")

    if purge:
        if show_changes:
            changelog.append("Purge empty directories")
        for empty_path in util.find_empty_dirs(base_path):
            if show_changes:
                changelog.append("* Remove empty directory {}".format(empty_path))
            if commit:
                rmdir(empty_path)

    return result.Success(printables=changelog)
コード例 #7
0
def lint(*search, ignore=None, fix=False, strict=False, report=True, **kwargs):
    """
    Check character files for completeness and correctness.

    This function checks that every character file has a few required tags, and
    applies extra checking for some character types. See util.Character.validate
    for details.

    This command normally ignores unknown tags. In strict mode, it will report
    the presence of any tag not expected by the character class.

    Args:
        search (list): Paths to search for character files. Items can be strings
            or lists of strings.
        ignore (list): Paths to ignore
        fix (bool): Whether to automatically fix errors when possible
        strict (bool): Whether to include non-critical errors and omissions
        report (bool): Do not include files in the return data, only problem
            descriptions
        prefs (Settings): Settings object to use. Uses internal settings by
            default.

    Returns:
        Result object. On success, openable attribute will contain a list of all
        files that had errors.
    """
    prefs = kwargs.get('prefs', settings.InternalSettings())
    if not ignore:
        ignore = []
    ignore.extend(prefs.get_ignored_paths('lint'))

    openable = []
    printable = []

    # check each character
    characters = parser.get_characters(flatten(search), ignore)
    for character in characters:
        if character.tags('nolint').present:
            continue

        character.validate(strict)
        character.problems.extend(
            linters.lint(character, fix=fix, strict=strict, prefs=prefs))

        # Report problems on one line if possible, or as a block if there's more than one
        if not character.valid:
            charpath = character.path
            if not report:
                openable.append(charpath)
            if len(character.problems) > 1:
                printable.append("File '{}':".format(charpath))
                for detail in character.problems:
                    printable.append("    {}".format(detail))
            else:
                printable.append("{} in '{}'".format(character.problems[0],
                                                     charpath))

    return result.Success(openable=openable, printables=printable)
コード例 #8
0
def dump(*search,
         ignore=None,
         do_sort=False,
         metadata=False,
         outfile=None,
         **kwargs):
    """
    Dump the raw character data, unaltered.

    Always formats the data as json.

    Args:
        search (List): Paths to search for character files
        ignore (List): Paths to ignore
        do_sort (bool): Whether to sort the characters before dumping
        metadata (bool): Whether to prepend metadata to the output
        outfile (string|None): Filename to put the dumped data. None and "-"
            print to stdout.
        prefs (Settings): Settings object to use. Uses internal settings by
            default.

    Returns:
        Result object. If outfile pointed to a real file, the openable attribute
        will contain that filename.
    """
    prefs = kwargs.get('prefs', settings.InternalSettings())
    if not ignore:
        ignore = []
    ignore.extend(prefs.get_ignored_paths('dump'))
    sort_by = kwargs.get('sort_by', prefs.get('dump.sort_by'))

    characters = parser.get_characters(flatten(search), ignore)
    if do_sort:
        sorter = util.character_sorter.CharacterSorter(sort_by, prefs=prefs)
        characters = sorter.sort(characters)

    characters = [c.dump() for c in characters]

    # make some json
    if metadata:
        meta = {'meta': True, **prefs.get_metadata('json')}
        characters = itertools.chain([meta], characters)

    with util.smart_open(outfile) as outstream:
        json.dump([c for c in characters], outstream, cls=CharacterEncoder)

    openable = [outfile] if outfile and outfile != '-' else None

    return result.Success(openable=openable)
コード例 #9
0
def find(*rules, search=None, ignore=None, **kwargs):
    """
    Find characters in the campaign that match certain rules

    Searches for character objects in the campaign that match the given
    rules. To search an arbitrary list of Character objects, use
    find_characters.

    Args:
        rules (str): One or more strings that describe which characters to
            find. Passed directly to find_characters.
        search (list): Paths to search for character files. Items can be strings
            or lists of strings.
        ignore (list): Paths to ignore
        dryrun (bool): Whether to print the character file paths instead of
            opening them
        prefs (Settings): Settings object to use. Uses internal settings by
            default.

    Returns:
        Result object. Openable will contain a list of file paths to the
        matching Character objects.
    """
    prefs = kwargs.get('prefs', settings.InternalSettings())
    dryrun = kwargs.get('dryrun', False)
    if search is None:
        search = []

    if not ignore:
        ignore = []
    ignore.extend(prefs.get_ignored_paths('find'))

    rules = list(flatten(rules))

    # use a list so we can iterate more than once
    characters = list(parser.get_characters(flatten(search), ignore))

    filtered_chars = find_characters(rules, characters=characters)

    paths = [char.get('path') for char in filtered_chars]

    if dryrun:
        openable = []
        printables = paths
    else:
        openable = paths
        printables = []

    return result.Success(openable=openable, printables=printables)
コード例 #10
0
ファイル: create_character.py プロジェクト: aurule/npc
def werewolf(name, auspice, *, tribe=None, pack=None, **kwargs):
    """
    Create a Werewolf character.

    Args:
        name (str): Base file name
        auspice (str): Name of the character's Auspice.
        tribe (str): Name of the character's Tribe. Leave empty for Ghost Wolves.
        pack (str): name of the character's pack.
        dead (bool|str): Whether to add the @dead tag. Pass False to exclude it
            (the default), an empty string to inlcude it with no details given,
            and a non-empty string to include the tag along with the contents of
            the argument.
        foreign (bool|str): Details of non-standard residence. Leave empty to
            exclude the @foreign tag.
        location (str): Details about where the character lives. Leave empty to
            exclude the @location tag.
        groups (list): One or more names of groups the character belongs to.
            Used to derive path.
        prefs (Settings): Settings object to use. Uses internal settings by
            default.

    Returns:
        Result object. Openable will contain the path to the new character file.
    """
    prefs = kwargs.get('prefs', settings.InternalSettings())
    groups = kwargs.get('groups', [])
    location = kwargs.get('location', False)
    dead = kwargs.get('dead', False)
    foreign = kwargs.get('foreign', False)

    # build minimal Character
    temp_char = _minimal_character(
        ctype='werewolf',
        groups=groups,
        dead=dead,
        foreign=foreign,
        location=location,
        prefs=prefs)
    temp_char.tags('auspice').append(auspice.title())
    if tribe:
        temp_char.tags('tribe').append(tribe.title())
    if pack:
        temp_char.tags('pack').append(pack)

    return _cp_template_for_char(name, temp_char, prefs)
コード例 #11
0
ファイル: html.py プロジェクト: Arent128/npc
def listing(characters, outstream, *, metadata=None, partial=False, **kwargs):
    """
    Create an html character listing

    Args:
        characters (list): Character info dicts to show
        outstream (stream): Output stream
        metadata_format (string|None): Whether to include metadata, and what
            format to use. Accepts a value of 'meta'. Metadata will always
            contain a title and creation date, if included.
        metadata (dict): Additional metadata to insert. Ignored unless
            metadata_format is set. The keys 'title', and 'created' will
            overwrite the generated values for those keys.
        partial (bool): Whether to produce partial output by omitting the head
            and other tags. Only the content of the body tag is created.
            Does not allow metadata to be included, so the metadata_format and
            metadata args are ignored.
        prefs (Settings): Settings object. Used to get the location of template
            files.
        encoding (string): Encoding format of the output text. Overrides the
            value in settings.
        sectioners (List[BaseSectioner]): List of BaseSectioner objects to
            render section templates based on character attributes. Omit to
            suppress sections.
        progress (function): Callback function to track the progress of
            generating a listing. Must accept the current count and total count.
            Should print to stderr.

    Returns:
        A util.Result object. Openable will not be set.
    """
    prefs = kwargs.get('prefs', settings.InternalSettings())
    if metadata is None:
        metadata = {}

    renderer = npc.formatters.HtmlFormatter(
        metadata=metadata,
        sectioners=kwargs.get('sectioners', []),
        update_progress=kwargs.get('update_progress', lambda i, t: False),
        partial=partial,
        metadata_format=kwargs.get('metadata_format'),
        encoding=kwargs.get('encoding', prefs.get('listing.html_encoding')),
        prefs=prefs)

    return renderer.render(characters, outstream)
コード例 #12
0
def standard(name, ctype, *, dead=False, foreign=False, **kwargs):
    """
    Create a character without extra processing.

    Simple characters don't have any unique tags or file annotations. Everything
    is based on their type.

    Args:
        name (str): Base file name. Format is "<character name> - <brief note>".
        ctype (str): Character type. Must have a template configured in prefs.
        dead (bool|str): Whether to add the @dead tag. Pass False to exclude it
            (the default), an empty string to inlcude it with no details given,
            and a non-empty string to include the tag along with the contents of
            the argument.
        foreign (bool|str): Details of non-standard residence. Leave empty to
            exclude the @foreign tag.
        location (str): Details about where the character lives. Leave empty to
            exclude the @location tag.
        groups (list): One or more names of groups the character belongs to.
            Used to derive path.
        prefs (Settings): Settings object to use. Uses internal settings by
            default.

    Returns:
        Result object. Openable will contain the new character file.
    """
    prefs = kwargs.get('prefs', settings.InternalSettings())
    groups = kwargs.get('groups', [])
    location = kwargs.get('location', False)
    ctype = ctype.lower()

    if not prefs.get("types.{}.sheet_template".format(ctype)):
        return result.ConfigError(
            errmsg="Character type '{}' does not have a sheet template".format(
                ctype))

    # build minimal character
    temp_char = _minimal_character(ctype=ctype.title(),
                                   groups=groups,
                                   dead=dead,
                                   foreign=foreign,
                                   location=location,
                                   prefs=prefs)

    return _cp_template_for_char(name, temp_char, prefs)
コード例 #13
0
def latest(thingtype='', **kwargs):
    """
    Open the latest plot and/or session file

    Args:
        thingtype (str): Type of the things to return. Use "session" to get the
            latest session file, "plot" to get the latest plot, and anything
            else to get all plot and session files.
        prefs (Settings): Settings object to use. Uses internal settings by
            default.

    Returns:
        Result object. Openable will contain the path(s) to the requested file(s).
    """
    prefs = kwargs.get('prefs', settings.InternalSettings())
    plot_dir = Path(prefs.get('paths.required.plot'))
    session_dir = Path(prefs.get('paths.required.session'))

    if not plot_dir.exists():
        return result.FSError(
            errmsg="Cannot access plot path '{}'".format(plot_dir))

    if not session_dir.exists():
        return result.FSError(
            errmsg="Cannot access session path '{}'".format(session_dir))

    plot_template = prefs.get('story.templates.plot')
    plot_regex = regex_from_template(plot_template)
    latest_plot = latest_file(plot_dir, plot_regex)

    session_template = prefs.get('story.templates.session')
    session_regex = regex_from_template(session_template)
    latest_session = latest_file(session_dir, session_regex)

    if thingtype == 'session':
        openable = [str(latest_session.path)]
    elif thingtype == 'plot':
        openable = [str(latest_plot.path)]
    else:
        openable = [str(latest_session.path), str(latest_plot.path)]

    return result.Success(openable=openable)
コード例 #14
0
def open_settings(location, show_defaults=False, settings_type=None, **kwargs):
    """
    Open the named settings file.

    If the desired settings file does not exist, an empty file is created and
    then opened.

    Args:
        location (str): Which settings file to open. One of 'user' or 'campaign'.
        show_defaults (bool): Whether the default settings file should be opened
            for reference alongside the specified settings file.
        settings_type (str): Determines which kind of settings file to open,
            like base settings or changeling settings. If left unset, base
            settings are opened. One of 'base' or 'changeling'.
        prefs (Settings): Settings object to use. Uses internal settings by
            default.

    Returns:
        Result object. Openable will contain the desired settings file. If true
        was passed in show_defaults, it will also contain the reference settings
        file.
    """
    prefs = kwargs.get('prefs', settings.InternalSettings())
    if settings_type:
        settings_type = settings_type.lower()

    target_path = prefs.get_settings_path(location, settings_type)
    if not target_path.exists():
        dirname = target_path.parent
        makedirs(dirname, mode=0o775, exist_ok=True)
        with open(target_path, 'a') as settings_file:
            settings_file.write('{}')

    if show_defaults:
        openable = [
            prefs.get_settings_path('default', settings_type,
                                    target_path.suffix), target_path
        ]
    else:
        openable = [target_path]
    return result.Success(openable=openable)
コード例 #15
0
def create_path_from_character(character: Character,
                               *,
                               base_path=None,
                               hierarchy=None,
                               **kwargs):
    """
    Determine the best file path for a character.

    The path is created underneath base_path. It only includes directories
    which already exist. It's used by character creation, linting, and reorg.
    Its behavior is controlled by the hierarchy parameter.

    Args:
        character: Parsed character data
        base_path (str): Base path for character files
        hierarchy (str): Path spec defining which folders to create
        prefs (Settings): Settings object to use. Uses internal settings by
            default.

    Returns:
        Constructed file path based on the character data.

    hierarchy Format
    ================

    The hierarchy argument is a string made up of one or more path components
    separated by a '/' character. Each component can either be a collection of
    characters or a tag substitution. Literal characters are inserted without
    being changed, as long as that folder already exists:

        create_path_from_character(..., hierarchy="Awesome/Heroes")

    This will attempt to add two folders to the constructed path, "Awesome" and
    then "Heroes".

    Substitutions are surrounded by {curly braces} and can either name a tag, or
    be a conditional. Conditionals check whether the character has data for the
    named tag, then inserts the second part of the substitution literally:

        create_path_from_character(..., hierarchy="{school?Student}")

    This will check if the character has data in its "school" tag, and if it
    does, attempt to add the "Student" folder to the constructed path.

        create_path_from_character(..., hierarchy="{school}")

    On the other hand, this will attempt to add a folder with the same name as
    the first value for "school" that the character has. So a character whose
    first school is "Middleton High" will make the function try to add a folder
    named "Middleton High" to the path.

    Alorithm summary
    ================

    * iterate through components of the hierarchy
    * anything not inside {curly braces} is inserted literally
    * everything else is interpreted

    conditionals:
        * check for '?' operator
        *   translate tag name if needed
        *   test tag presence
        *       most tags: has_tag(tag)
        *       foreign: foreign or wanderer
        *       type: not 'Unknown'
        *       *+ranks: any ranks exist
        *   if character has that tag, insert the literal
    tags:
        * translate tag name if needed
        * if the character has that tag:
        *   type: get 'types.type_key.type_path'
        *   group: use only the first group
        *   rank(s): iterate first group's ranks and add folders
        *   groups: iterate group value in order, trying to add a new path
        *       component for each
        *   groups+ranks: iterate group values, add folder, iterate that group's
        *       ranks and add folders
        *   locations: inserts first location, then first foreign
        *   all other tags: insert their first value
    """
    prefs = kwargs.get('prefs', settings.InternalSettings())

    if not base_path:
        base_path = prefs.get('paths.required.characters')
    if not hierarchy:
        hierarchy = prefs.get('paths.hierarchy')

    target_path = base_path

    def add_path_if_exists(base, potential):
        """Add a directory to the base path if that directory exists."""
        test_path = path.join(base, potential)
        if path.exists(test_path):
            return test_path
        return base

    def placeholder(component):
        return prefs.get(
            'types.{char_type}.missing_values.{component}'.format(
                char_type=character.type_key, component=component), '')

    for component in hierarchy.split('/'):
        if not (component.startswith('{') and component.endswith('}')):
            # No processing needed. Insert the literal and move on.
            target_path = add_path_if_exists(target_path, component)
            continue

        component = component.strip('{}')

        if '?' in component:
            tag_name, literal = component.split('?')
            tag_name = prefs.translate_tag_for_character_type(
                character.type_key, tag_name)
            if tag_name == 'foreign':
                # "foreign?" gets special handling to check the wanderer tag as well
                if character.foreign:
                    target_path = add_path_if_exists(target_path, literal)
            elif character.has_items(tag_name):
                target_path = add_path_if_exists(target_path, literal)
            continue

        tag_name = prefs.translate_tag_for_character_type(
            character.type_key, component)
        if tag_name == 'type':
            # get the translated type path for the character's type
            target_path = add_path_if_exists(
                target_path,
                prefs.get('types.{}.type_path'.format(character.type_key),
                          placeholder('type')))
        elif tag_name == 'group':
            # get just the first group
            target_path = add_path_if_exists(target_path,
                                             character.get_first('group'))
        elif tag_name in ['rank', 'ranks']:
            # iterate all ranks for the first group and add each one as a folder
            for rank in character.get_ranks(character.get_first('group')):
                target_path = add_path_if_exists(target_path, rank)
        elif tag_name == 'groups':
            # iterate all group values and try to add each one as a folder
            for group in character['group']:
                target_path = add_path_if_exists(target_path, group)
        elif tag_name == 'groups+ranks':
            # Iterate all groups, add each as a folder, then iterate all ranks
            # for that group and add each of those as folders
            for group in character['group']:
                target_path = add_path_if_exists(target_path, group)
                for rank in character.get_ranks(group):
                    target_path = add_path_if_exists(target_path, rank)
        elif tag_name == 'locations':
            # use the first location entry, or foreign entry
            target_path = add_path_if_exists(
                target_path,
                character.get_first('location', placeholder('location')))
            target_path = add_path_if_exists(
                target_path,
                character.get_first('foreign', placeholder('foreign')))
        else:
            # every other tag gets to use its first value
            target_path = add_path_if_exists(
                target_path,
                character.get_first(key=tag_name,
                                    default=placeholder(tag_name)))

    return target_path
コード例 #16
0
ファイル: listing.py プロジェクト: Arent128/npc
def make_list(*search,
              ignore=None,
              fmt=None,
              metadata=None,
              title=None,
              outfile=None,
              **kwargs):
    """
    Generate a listing of NPCs.

    The default listing templates ignore tags not found in Character.KNOWN_TAGS.

    Args:
        search (list): Paths to search for character files. Items can be strings
            or lists of strings.
        ignore (list): Paths to ignore
        fmt (str): Format of the output. Supported types are 'markdown', 'md',
            'htm', 'html', and 'json'. Pass 'default' or None to get format from
            settings.
        metadata (str|None): Whether to include metadata in the output and what
            kind of metadata to use. Pass 'default' to use the format configured
            in Settings.

            The markdown format allows either 'mmd' (MultiMarkdown) or
            'yfm'/'yaml' (Yaml Front Matter) metadata.

            The json format only allows one form of metadata, so pass any truthy
            value to include the metadata keys.
        title (str|None): The title to put in the metadata, if included.
            Overrides the title from settings.
        outfile (string|None): Filename to put the listed data. None and "-"
            print to stdout.
        do_sort (bool): Whether to avoid sorting altogether. Defaults to True.
        sort_by (string|None): Sort order for characters. Defaults to the value of
            "list_sort" in settings.
        headings (List[string]): List of tag names to group characters by
        partial (bool): Whether to omit headers and footers and just render body
            content. Defaults to false.
        prefs (Settings): Settings object to use. Uses internal settings by
            default.
        progress (function): Callback function to track the progress of
            generating a listing. Must accept the current count and total count.
            Should print to stderr. Not used by all formatters.

    Returns:
        Result object. Openable will contain the output file if given.
    """
    prefs = kwargs.get('prefs', settings.InternalSettings())
    if not ignore:
        ignore = []
    ignore.extend(prefs.get_ignored_paths('listing'))
    do_sort = kwargs.get('do_sort', True)
    partial = kwargs.get('partial', False)
    update_progress = kwargs.get('progress', lambda i, t: False)

    sort_order = kwargs.get('sort_by',
                            prefs.get('listing.sort_by')) if do_sort else []
    headings = kwargs.get('headings', sort_order)

    characters = _process_directives(
        parser.get_characters(flatten(search), ignore))
    if do_sort:
        sorter = util.character_sorter.CharacterSorter(sort_order, prefs=prefs)
        characters = sorter.sort(characters)

    if fmt == "default" or not fmt:
        fmt = prefs.get('listing.default_format')
    out_type = formatters.get_canonical_format_name(fmt)

    formatter = formatters.get_listing_formatter(out_type)
    if not formatter:
        return result.OptionError(
            errmsg="Cannot create output of format '{}'".format(out_type))

    if metadata == 'default' and out_type != 'json':
        # Ensure 'default' metadata type gets replaced with the right default
        # metadata format. Irrelevant for json format.
        metadata_type = prefs.get(
            'listing.metadata.{}.default_format'.format(out_type))
    else:
        metadata_type = metadata

    meta = prefs.get_metadata(out_type)
    if title:
        meta['title'] = title

    header_offset = int(prefs.get('listing.base_header_level'))
    sectioners = [
        formatters.sectioners.get_sectioner(key=g,
                                            heading_level=i + header_offset,
                                            prefs=prefs)
        for i, g in enumerate(headings)
    ]

    with util.smart_open(outfile,
                         binary=(out_type
                                 in formatters.BINARY_TYPES)) as outstream:
        response = formatter(characters,
                             outstream,
                             metadata_format=metadata_type,
                             metadata=meta,
                             prefs=prefs,
                             sectioners=sectioners,
                             partial=partial,
                             update_progress=update_progress)

    # pass errors straight through
    if not response.success:
        return response

    openable = [outfile] if outfile and outfile != '-' else None

    return result.Success(openable=openable)
コード例 #17
0
ファイル: create_character.py プロジェクト: aurule/npc
def changeling(name, seeming, kith, *,
                      court=None, motley=None, dead=False, foreign=False, **kwargs):
    """
    Create a Changeling character.

    Args:
        name (str): Base file name
        seeming (str): Name of the character's Seeming. Added to the file with
            notes.
        kith (str): Name of the character's Kith. Added to the file with notes.
        court (str|none): Name of the character's Court. Used to derive path.
        motley (str|none): Name of the character's Motley.
        dead (bool|str): Whether to add the @dead tag. Pass False to exclude it
            (the default), an empty string to inlcude it with no details given,
            and a non-empty string to include the tag along with the contents of
            the argument.
        foreign (bool|str): Details of non-standard residence. Leave empty to
            exclude the @foreign tag.
        location (str): Details about where the character lives. Leave empty to
            exclude the @location tag.
        groups (list): One or more names of groups the character belongs to.
            Used to derive path.
        freehold (str): Name of the freehold the changeling belongs to. Leave
            empty to exclude @freehold tag.
        entitlement (str): Name of the entitlement the changeling belongs to.
            Leave empty to exclude @entitlement tag.
        prefs (Settings): Settings object to use. Uses internal settings by
            default.

    Returns:
        Result object. Openable will contain the path to the new character file.
    """
    prefs = kwargs.get('prefs', settings.InternalSettings())
    groups = kwargs.get('groups', [])
    location = kwargs.get('location', False)
    freehold = kwargs.get('freehold', None)
    entitlement = kwargs.get('entitlement', None)

    # build minimal Character
    temp_char = _minimal_character(
        ctype='changeling',
        groups=groups,
        dead=dead,
        foreign=foreign,
        location=location,
        prefs=prefs)
    temp_char.tags('seeming').append(seeming.title())
    temp_char.tags('kith').append(kith.title())
    if court:
        temp_char.tags('court').append(court.title())
    if motley:
        temp_char.tags('motley').append(motley)
    if freehold:
        temp_char.tags('freehold').append(freehold)
    if entitlement:
        temp_char.tags('entitlement').append(entitlement)

    def _insert_sk_data(data):
        """Insert seeming and kith in the advantages block of a template"""

        seeming_re = re.compile(
            r'^(\s+)seeming(\s+)\w+$',
            re.MULTILINE | re.IGNORECASE
        )
        kith_re = re.compile(
            r'^(\s+)kith(\s+)\w+$',
            re.MULTILINE | re.IGNORECASE
        )

        seeming_name = temp_char.tags('seeming').first_value()
        seeming_key = seeming.lower()
        if seeming_key in prefs.get('changeling.seemings'):
            seeming_notes = "{}; {}".format(
                prefs.get("changeling.blessings.{}".format(seeming_key)),
                prefs.get("changeling.curses.{}".format(seeming_key)))
            data = seeming_re.sub(
                r"\g<1>Seeming\g<2>{} ({})".format(seeming_name, seeming_notes),
                data
            )
        else:
            print_err("Unrecognized seeming '{}'".format(seeming_name))

        kith_name = temp_char.tags('kith').first_value()
        kith_key = kith.lower()
        if kith_key in prefs.get('changeling.kiths.{}'.format(seeming_key), []):
            kith_notes = prefs.get("changeling.blessings.{}".format(kith_key))
            data = kith_re.sub(
                r"\g<1>Kith\g<2>{} ({})".format(kith_name, kith_notes),
                data
            )
        else:
            print_err("Unrecognized kith '{}' for seeming '{}'".format(kith_name, seeming_name))

        return data

    return _cp_template_for_char(name, temp_char, prefs, fn=_insert_sk_data)
コード例 #18
0
ファイル: story.py プロジェクト: Arent128/npc
def session(**kwargs):
    """
    Create the files for a new game session.

    Finds the plot and session log files for the last session, copies the plot,
    and creates a new empty session log. If the latest plot file is ahead of
    the latest session, a new plot file will *not* be created. Likewise if the
    latest session file is ahead, a new session file will *not* be created.

    Args:
        prefs (Settings): Settings object to use. Uses internal settings by
            default.

    Returns:
        Result object. Openable will contain the current and previous session
        log and plot planning files.
    """
    prefs = kwargs.get('prefs', settings.InternalSettings())
    plot_path = prefs.get('paths.required.plot')
    session_path = prefs.get('paths.required.session')

    if not (path.exists(plot_path) and path.exists(session_path)):
        return result.FSError(
            errmsg="Cannot access paths '{}' and/or '{}'".format(
                plot_path, session_path))

    latest_plot = latest_file_info(plot_path, PLOT_REGEX)
    latest_session = latest_file_info(session_path, SESSION_REGEX)

    new_number = min(latest_plot['number'], latest_session['number']) + 1

    openable = []
    if latest_session['exists']:
        if latest_session['number'] < new_number:
            # create new session log
            old_session_path = latest_session['path']
            new_session_path = path.join(
                session_path,
                "session {num}{ext}".format(num=new_number,
                                            ext=latest_session['ext']))
            shcopy(prefs.get('story.session_template'), new_session_path)
        else:
            # present existing session files, since we don't have to create one
            old_session_path = path.join(
                session_path,
                "session {num}{ext}".format(num=latest_session['number'] - 1,
                                            ext=latest_session['ext']))
            new_session_path = latest_session['path']
        openable.extend((new_session_path, old_session_path))
    else:
        # no existing session, so just copy the template
        template_path = prefs.get('story.session_template')
        new_session_path = path.join(
            session_path,
            "session {num}{ext}".format(num=new_number,
                                        ext=path.splitext(template_path)[1]))
        shcopy(template_path, new_session_path)
        openable.append(new_session_path)

    if latest_plot['exists']:
        if latest_plot['number'] < new_number:
            # copy old plot
            old_plot_path = latest_plot['path']
            new_plot_path = path.join(
                plot_path, "plot {num}{ext}".format(num=new_number,
                                                    ext=latest_plot['ext']))
            shcopy(old_plot_path, new_plot_path)
        else:
            # present existing plot files, since we don't have to create one
            old_plot_path = path.join(
                plot_path,
                "plot {num}{ext}".format(num=latest_plot['number'] - 1,
                                         ext=latest_plot['ext']))
            new_plot_path = latest_plot['path']
        openable.extend((new_plot_path, old_plot_path))
    else:
        # no old plot to copy, so create a blank
        new_plot_path = path.join(
            plot_path,
            "plot {num}{ext}".format(num=new_number,
                                     ext=prefs.get('story.plot_ext')))
        with open(new_plot_path, 'w') as new_plot:
            new_plot.write(' ')
        openable.append(new_plot_path)

    return result.Success(openable=openable)
コード例 #19
0
def session(**kwargs):
    """
    Create the files for a new game session.

    Finds the plot and session log files for the last session, copies the plot,
    and creates a new empty session log. If the latest plot file is ahead of
    the latest session, a new plot file will *not* be created. Likewise if the
    latest session file is ahead, a new session file will *not* be created.

    Args:
        prefs (Settings): Settings object to use. Uses internal settings by
            default.

    Returns:
        Result object. Openable will contain the current and previous session
        log and plot planning files.
    """
    prefs = kwargs.get('prefs', settings.InternalSettings())
    plot_dir = Path(prefs.get('paths.required.plot'))
    session_dir = Path(prefs.get('paths.required.session'))

    if not plot_dir.exists():
        return result.FSError(
            errmsg="Cannot access plot path '{}'".format(plot_dir))

    if not session_dir.exists():
        return result.FSError(
            errmsg="Cannot access session path '{}'".format(session_dir))

    plot_template = prefs.get('story.templates.plot')
    if SEQUENCE_KEYWORD not in str(plot_template):
        return result.ConfigError(
            errmsg="Plot template has no number placeholder ({})".format(
                SEQUENCE_KEYWORD))
    plot_regex = regex_from_template(plot_template)
    latest_plot = latest_file(plot_dir, plot_regex)

    session_template = prefs.get('story.templates.session')
    if SEQUENCE_KEYWORD not in str(session_template):
        return result.ConfigError(
            errmsg="Session template has no number placeholder ({})".format(
                SEQUENCE_KEYWORD))
    session_regex = regex_from_template(session_template)
    latest_session = latest_file(session_dir, session_regex)

    new_number = min(latest_plot.number, latest_session.number) + 1

    def copy_templates(dest_dir, templates):
        """
        Create new story files from templates.

        This is responsible for creating the new file name based on
        `new_number`, loading the template contents, substituting the "NNN" and
        "((COPY))" keywords, and writing the result to the new file.
        """
        def old_file_contents(old_file_path):
            """
            Get the previous file's contents.


            """
            try:
                with open(old_file_path, 'r') as old_file:
                    return old_file.read()
            except (FileNotFoundError, IsADirectoryError):
                return ''

        for template_path in templates:
            if SEQUENCE_KEYWORD not in str(template_path):
                print_err("Template {} has no number placeholder ({})".format(
                    template_path, SEQUENCE_KEYWORD))
                continue

            new_file_name = template_path.name.replace(SEQUENCE_KEYWORD,
                                                       str(new_number))
            destination = dest_dir.joinpath(new_file_name)
            if destination.exists():
                continue

            with open(template_path, 'r') as f:
                data = f.read()

            data = data.replace(SEQUENCE_KEYWORD, str(new_number))
            if COPY_KEYWORD in data:
                file_regex = regex_from_template(template_path)
                old_file_path = latest_file(dest_dir, file_regex).path
                data = data.replace(COPY_KEYWORD,
                                    old_file_contents(old_file_path))

            with open(destination, 'w') as f:
                f.write(data)

    plot_templates = flatten([
        prefs.get('story.templates.plot'),
        prefs.get('story.templates.plot_extras')
    ])
    copy_templates(plot_dir, plot_templates)

    session_templates = flatten([
        prefs.get('story.templates.session'),
        prefs.get('story.templates.session_extras')
    ])
    copy_templates(session_dir, session_templates)

    openable = [
        str(latest_file(session_dir, session_regex).path),
        str(latest_file(plot_dir, plot_regex).path)
    ]
    old_session_name = session_template.name.replace(SEQUENCE_KEYWORD,
                                                     str(new_number - 1))
    old_session = session_dir.joinpath(old_session_name)
    if old_session.exists():
        openable.append(str(old_session))
    old_plot_name = plot_template.name.replace(SEQUENCE_KEYWORD,
                                               str(new_number - 1))
    old_plot = plot_dir.joinpath(old_plot_name)
    if old_plot.exists():
        openable.append(str(old_plot))

    return result.Success(openable=openable)