<commit_before>"""
Data structures module for SmartyStreets API.

These structures simply wrap Python built in data structures that match the API's JSON responses,
including some convenience methods for simple access.
"""
import logging

class Address(dict):
    """
    Class for handling a single address response
    """

    @property
    def location(self):
        """
        Returns the geolocation as a lat/lng pair
        """
        try:
            lat, lng = self['metadata']['latitude'], self['metadata']['longitude']
        except KeyError:
            return None
        if not lat or not lng:
            return None
        return lat, lng

    @property
    def confirmed(self):
        """
        Returns a boolean whether this address is DPV confirmed
        The property does not specify *how* or what extent.
        """
        valid = ['Y', 'S', 'D']
        match_code = self.get('analysis', {}).get('dpv_match_code', '')
        return match_code in valid

    @property
    def id(self):
        """
        Returns the input id
        """
        try:
            return self['input_id']
        except KeyError:
            return None

    @property
    def index(self):
        """
        Returns the input_index
        """
        try:
            return self['input_index']
        except KeyError:
            return None

    @property
    def vacant(self):
        if 'dpv_vacant' in self['analysis']:
            return 1 if self['analysis']['dpv_vacant'] == 'Y' else 0
        return None

    # function that checks if fields exist, returns None if not
    def lookup(self, group, field):
        if group in self:
            if field in self[group]:
                return self[group][field]
            # logging.getLogger('smarystreets').error('[group] {} [field] {}'.format(group, field))
            return None
        logging.getLogger('smarystreets').error('[group] {}'.format(group))
        return None

    @property
    def addressee(self):
        """Returns addressee"""
        return self.get('addressee', None)

    @property
    def delivery_line_1(self):
        return self.get('delivery_line_1', None)

    @property
    def delivery_line_2(self):
        return self.get('delivery_line_2', None)

    @property
    def last_line(self):
        return self.get('last_line', None)

    @property
    def footnotes(self):
        return self['analysis'].get('footnotes', None)

    @property
    def analysis_active(self):
        return self['analysis'].get('active', None)

    @property
    def analysis_dpv_vacant(self):
        return self['analysis'].get('dpv_vacant', None)

    @property
    def analysis_dpv_cmra(self):
        return self['analysis'].get('dpv_cmra', None)

    @property
    def components_street_suffix(self):
        return self['components'].get('street_suffix', None)

    @property
    def primary_number(self):
        return self['components'].get('primary_number', None)

    @property
    def components_street_name(self):
        return self['components'].get('street_name', None)

    @property
    def components_city_name(self):
        return self['components'].get('city_name', None)

    @property
    def components_state_abbreviation(self):
        return self['components'].get('state_abbreviation', None)

    @property
    def components_zipcode(self):
        return self['components'].get('zipcode', None)

    @property
    def components_plus4_code(self):
        return self['components'].get('plus4_code', None)

    @property
    def metadata_dst(self):
        return self['metadata'].get('dst', None)

    @property
    def metadata_county_name(self):
        return self['metadata'].get('county_name', None)

    @property
    def metadata_congressional_district(self):
        return self['metadata'].get('congressional_district', None)

    @property
    def metadata_county_fips(self):
        return self['metadata'].get('county_fips', None)

class AddressCollection(list):
    """
    Class for handling multiple responses.
    """
    id_lookup = {}  # For user supplied input_id
    index_lookup = {}  # For SmartyStreets input_index

    def __init__(self, results):
        """
        Constructor for an AddressCollection

        :param addresses: a list of dictionaries providing address information
        :return:
        """
        addresses = []
        for index, result in enumerate(results):
            address = Address(result)
            addresses.append(address)
            self.index_lookup[address.index] = index
            if address.id:
                self.id_lookup[address.id] = index
        super(AddressCollection, self).__init__(addresses)

    def get(self, key):
        """
        Returns an address by user controlled input ID

        :param key: an input_id used to tag a lookup address
        :return: a matching Address
        """
        try:
            return self[self.id_lookup.get(key)]
        except TypeError:
            raise KeyError

    def get_index(self, key):
        """
        Returns an address by input index, a value that matches the list index of the provided
        lookup value, not necessarily the result.

        :param key: an input_index matching the index of the provided address
        :return: a matching Address
        """
        try:
            return self[self.index_lookup.get(key)]
        except TypeError:
            raise KeyError
<commit_msg>remove logging for field lookup<commit_after>"""
Data structures module for SmartyStreets API.

These structures simply wrap Python built in data structures that match the API's JSON responses,
including some convenience methods for simple access.
"""
import logging

class Address(dict):
    """
    Class for handling a single address response
    """

    @property
    def location(self):
        """
        Returns the geolocation as a lat/lng pair
        """
        try:
            lat, lng = self['metadata']['latitude'], self['metadata']['longitude']
        except KeyError:
            return None
        if not lat or not lng:
            return None
        return lat, lng

    @property
    def confirmed(self):
        """
        Returns a boolean whether this address is DPV confirmed
        The property does not specify *how* or what extent.
        """
        valid = ['Y', 'S', 'D']
        match_code = self.get('analysis', {}).get('dpv_match_code', '')
        return match_code in valid

    @property
    def id(self):
        """
        Returns the input id
        """
        try:
            return self['input_id']
        except KeyError:
            return None

    @property
    def index(self):
        """
        Returns the input_index
        """
        try:
            return self['input_index']
        except KeyError:
            return None

    @property
    def vacant(self):
        if 'dpv_vacant' in self['analysis']:
            return 1 if self['analysis']['dpv_vacant'] == 'Y' else 0
        return None

    # function that checks if fields exist, returns None if not
    def lookup(self, group, field):
        if group in self:
            if field in self[group]:
                return self[group][field]
            # logging.getLogger('smarystreets').error('[group] {} [field] {}'.format(group, field))
            return None
        # logging.getLogger('smarystreets').error('[group] {}'.format(group))
        return None

    @property
    def addressee(self):
        """Returns addressee"""
        return self.get('addressee', None)

    @property
    def delivery_line_1(self):
        return self.get('delivery_line_1', None)

    @property
    def delivery_line_2(self):
        return self.get('delivery_line_2', None)

    @property
    def last_line(self):
        return self.get('last_line', None)

    @property
    def footnotes(self):
        return self['analysis'].get('footnotes', None)

    @property
    def analysis_active(self):
        return self['analysis'].get('active', None)

    @property
    def analysis_dpv_vacant(self):
        return self['analysis'].get('dpv_vacant', None)

    @property
    def analysis_dpv_cmra(self):
        return self['analysis'].get('dpv_cmra', None)

    @property
    def components_street_suffix(self):
        return self['components'].get('street_suffix', None)

    @property
    def primary_number(self):
        return self['components'].get('primary_number', None)

    @property
    def components_street_name(self):
        return self['components'].get('street_name', None)

    @property
    def components_city_name(self):
        return self['components'].get('city_name', None)

    @property
    def components_state_abbreviation(self):
        return self['components'].get('state_abbreviation', None)

    @property
    def components_zipcode(self):
        return self['components'].get('zipcode', None)

    @property
    def components_plus4_code(self):
        return self['components'].get('plus4_code', None)

    @property
    def metadata_dst(self):
        return self['metadata'].get('dst', None)

    @property
    def metadata_county_name(self):
        return self['metadata'].get('county_name', None)

    @property
    def metadata_congressional_district(self):
        return self['metadata'].get('congressional_district', None)

    @property
    def metadata_county_fips(self):
        return self['metadata'].get('county_fips', None)

class AddressCollection(list):
    """
    Class for handling multiple responses.
    """
    id_lookup = {}  # For user supplied input_id
    index_lookup = {}  # For SmartyStreets input_index

    def __init__(self, results):
        """
        Constructor for an AddressCollection

        :param addresses: a list of dictionaries providing address information
        :return:
        """
        addresses = []
        for index, result in enumerate(results):
            address = Address(result)
            addresses.append(address)
            self.index_lookup[address.index] = index
            if address.id:
                self.id_lookup[address.id] = index
        super(AddressCollection, self).__init__(addresses)

    def get(self, key):
        """
        Returns an address by user controlled input ID

        :param key: an input_id used to tag a lookup address
        :return: a matching Address
        """
        try:
            return self[self.id_lookup.get(key)]
        except TypeError:
            raise KeyError

    def get_index(self, key):
        """
        Returns an address by input index, a value that matches the list index of the provided
        lookup value, not necessarily the result.

        :param key: an input_index matching the index of the provided address
        :return: a matching Address
        """
        try:
            return self[self.index_lookup.get(key)]
        except TypeError:
            raise KeyError
<|endoftext|>


<commit_before>#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Executable module to test unittest-xml-reporting.
"""

import os
from cStringIO import StringIO
import testsuite_cases as testcases
import unittest
import xmlrunner

class XMLTestRunnerTestCase(unittest.TestCase):
    """XMLTestRunner test case.
    """

    # Directory where the fixtures are placed
    TEST_FIXTURES_DIR = 'src/xmlrunner/tests/fixtures'

    def _fixture(name):
        "Read the content of a XML fixture."
        fixture_file = file('%s/%s.xml' % \
            (XMLTestRunnerTestCase.TEST_FIXTURES_DIR, name))
        content = ''.join(fixture_file.readlines())
        content = content.replace('{{PATH}}', os.path.abspath('.'))
        fixture_file.close()
        return content

    _fixture = staticmethod(_fixture)

    def _run_test_class(self, test_class, expected=None):
        "Run a test class and compare it to a given fixture."
        expected_xml = ''
        if expected:
            expected_xml = XMLTestRunnerTestCase._fixture(expected)
        self.runner.run(unittest.makeSuite(test_class))
        self.assertEqual(expected_xml, self.stream.getvalue())

    def setUp(self):
        "Setup the objects needed to run the tests."
        self.stream = StringIO()
        self.fake_stream = StringIO()
        self.runner = xmlrunner.XMLTestRunner(output=self.stream, \
            stream=self.fake_stream, elapsed_times=False)

    def tearDown(self):
        self.stream.close()
        self.fake_stream.close()

    def test_empty_test_class(self):
        "Empty test class should not generate a XML file."
        self._run_test_class(testcases.EmptyTestCase)

    def test_success_test_method(self):
        "Check the XML output of a test class with a successful test method."
        self._run_test_class(testcases.SuccessfulTestCase, \
            'successful_test_case')

    def test_failed_test_case(self):
        "Check the XML output of a test class with a failed test method."
        self._run_test_class(testcases.FailedTestCase, \
            'failed_test_case')

    def test_errord_test_case(self):
        "Check the XML output of a test class with an errord test method."
        self._run_test_class(testcases.ErrordTestCase, \
            'errord_test_case')

    def test_mixed_test_case(self):
        "Check the XML output of a test class with all sorts of outcomes."
        self._run_test_class(testcases.MixedTestCase, \
            'mixed_test_case')

if __name__ == '__main__':
    unittest.main()
<commit_msg>Comment.<commit_after>#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Executable module to test unittest-xml-reporting.
"""

import os
from cStringIO import StringIO
import testsuite_cases as testcases
import unittest
import xmlrunner

class XMLTestRunnerTestCase(unittest.TestCase):
    """XMLTestRunner test case.
    """

    # Directory where the fixtures are placed
    TEST_FIXTURES_DIR = 'src/xmlrunner/tests/fixtures'

    def _fixture(name):
        "Read the content of a XML fixture."
        fixture_file = file('%s/%s.xml' % \
            (XMLTestRunnerTestCase.TEST_FIXTURES_DIR, name))
        content = ''.join(fixture_file.readlines())
        content = content.replace('{{PATH}}', os.path.abspath('.'))
        fixture_file.close()
        return content

    _fixture = staticmethod(_fixture)

    def _run_test_class(self, test_class, expected=None):
        "Run a test class and compare it to a given fixture."
        expected_xml = ''
        if expected:
            expected_xml = XMLTestRunnerTestCase._fixture(expected)
        self.runner.run(unittest.makeSuite(test_class))
        self.assertEqual(expected_xml, self.stream.getvalue())

    def setUp(self):
        "Setup the objects needed to run the tests."
        self.stream = StringIO()
        self.fake_stream = StringIO()
        self.runner = xmlrunner.XMLTestRunner(output=self.stream, \
            stream=self.fake_stream, elapsed_times=False)

    def tearDown(self):
        "Free resources after each test."
        self.stream.close()
        self.fake_stream.close()

    def test_empty_test_class(self):
        "Empty test class should not generate a XML file."
        self._run_test_class(testcases.EmptyTestCase)

    def test_success_test_method(self):
        "Check the XML output of a test class with a successful test method."
        self._run_test_class(testcases.SuccessfulTestCase, \
            'successful_test_case')

    def test_failed_test_case(self):
        "Check the XML output of a test class with a failed test method."
        self._run_test_class(testcases.FailedTestCase, \
            'failed_test_case')

    def test_errord_test_case(self):
        "Check the XML output of a test class with an errord test method."
        self._run_test_class(testcases.ErrordTestCase, \
            'errord_test_case')

    def test_mixed_test_case(self):
        "Check the XML output of a test class with all sorts of outcomes."
        self._run_test_class(testcases.MixedTestCase, \
            'mixed_test_case')

if __name__ == '__main__':
    unittest.main()
<|endoftext|>


<commit_before>"""
    Main CLI engine for versionner
    Parse input options, and make all dirty jobs.
"""

from __future__ import print_function

from versionner import utils

utils.validate_python_version()

import argparse
import pathlib
import re
import shutil
import sys
import tempfile
import time
import traceback

import semver

import versionner
from versionner import config
from versionner import version
from versionner import vcs

def parse_args(args, **defaults):
    """
    Parse input arguments of script.

    :rtype : argparse.Namespace
    :param args:
    :return:
    """
    prog = pathlib.Path(sys.argv[0]).parts[-1].replace('.py', '')
    prog_version = "%%(prog)s %s" % versionner.__version__

    # pylint: disable=invalid-name
    p = argparse.ArgumentParser(prog=prog, description='Helps manipulating version of the project')
    p.add_argument('--file', '-f', dest='version_file', type=str,
        default=defaults.get('version_file'),
        help="path to file where version is saved")
    p.add_argument('--version', '-v', action="version", version=prog_version)
    p.add_argument('--date-format', type=str,
        default=defaults.get('date_format'),
        help="Date format used in project files")
    p.add_argument('--vcs-engine', type=str, default=defaults.get('vcs_engine'),
        help="Select VCS engine (only git is supported currently)", )
    p.add_argument('--vcs-commit-message', '-m', type=str, default=defaults.get('vcs_commit_message'),
        help="Commit message used when committing changes")
    p.add_argument('--verbose', action="store_true", help="Be more verbose if it's possible")

    def get_command_name(name):
        # pylint: disable=missing-docstring
        _name = name

        def _():
            # pylint: disable=missing-docstring
            return _name

        return _

    sub = p.add_subparsers()

    p_init = sub.add_parser('init',
        help="Create new version file")
    p_init.add_argument('value', nargs='?', default=defaults.get('default_init_version'), type=str,
        help="Initial version")
    p_init.add_argument('--commit', '-c', action='store_true',
        help="Commit changes done by `up` command (only if there is no changes in repo before)")
    p_init.set_defaults(get_command=get_command_name('init'))

    p_up = sub.add_parser('up',
        help="Increase version")
    p_up.add_argument('--commit', '-c', action='store_true',
        help="Commit changes done by `up` command (only if there is no changes in repo before)")
    p_up.add_argument('value', nargs='?', type=int,
        help="Increase version by this value (default: 1)")
    p_up.set_defaults(get_command=get_command_name('up'))

    p_up_gr = p_up.add_mutually_exclusive_group()
    up_part = defaults.get('up_part')
    p_up_gr.add_argument('--major', '-j', action="store_true",
        help="increase major part of version" + (" (project default)" if up_part == 'major' else ""))
    p_up_gr.add_argument('--minor', '-n', action="store_true",
        help="increase minor part of version" + (" (project default)" if up_part == 'minor' else ""))
    p_up_gr.add_argument('--patch', '-p', action="store_true",
        help="increase patch part of version" + (" (project default)" if up_part == 'patch' else ""))
    p_up_gr.set_defaults(get_command=get_command_name('up'))

    p_set = sub.add_parser('set',
        help="Set version to specified one")
    p_set.add_argument('--major', '-j', type=int,
        help="set major part of version to MAJOR")
    p_set.add_argument('--minor', '-n', type=int,
        help="set minor part of version to MINOR")
    p_set.add_argument('--patch', '-p', type=int,
        help="set patch part of version to PATCH")
    p_set.add_argument('--prerelease', '-r', type=str,
        help="set prerelease part of version to PRERELEASE")
    p_set.add_argument('--build', '-b', type=str,
        help="set build part of version to BUILD")
    p_set.add_argument('--commit', '-c', action='store_true',
        help="Commit changes done by `set` command (only if there is no changes in repo before)")
    p_set.add_argument('value', nargs='?', type=str,
        help="set version to this value")
    p_set.set_defaults(get_command=get_command_name('set'))

    p_tag = sub.add_parser('tag',
        help="Create VCS tag with current version")
    p_tag.add_argument('--vcs-tag-param', dest='vcs_tag_params', type=str, action="append",
        help="Additional params for VCS for \"tag\" command")
    p_tag.set_defaults(get_command=get_command_name('tag'))

    args = p.parse_args(args)
    args.version_file = pathlib.Path(args.version_file).absolute()

    if not hasattr(args, 'get_command'):
        args.get_command = get_command_name(None)

    if args.get_command() == 'build':
        if not args.version_file.exists():
            p.error("Version file \"%s\" doesn't exists" % args.version_file)

    elif args.get_command() == 'major':
        if not args.version_file.exists():
            p.error("Version file \"%s\" doesn't exists" % args.version_file)

    elif args.get_command() == 'value':
        if args.version_file.exists():
            p.error("Version file \"%s\" already exists" % args.version_file)

    elif args.get_command() == 'tag':
        if not args.vcs_tag_params:
            args.vcs_tag_params = defaults.get('vcs_tag_params', [])

    return args

def update_project_files(args, cfg, proj_version):
    """
    Update version string in project files

    :rtype : dict
    :param args:script arguments
    :param cfg:project configuration
    :param proj_version:current version
    :return:dict :raise ValueError:
    """
    counters = {'files': 0, 'changes': 0}

    for project_file in cfg.files:
        if not project_file.file.exists():
            print("File \"%s\" not found" % project_file.filename, file=sys.stderr)
            continue

        # prepare data
        date_format = project_file.date_format or args.date_format

        rxp = re.compile(project_file.search, project_file.search_flags)
        replace = project_file.replace % {
            "date": time.strftime(date_format),
            "major": proj_version.major,
            "minor": proj_version.minor,
            "patch": proj_version.patch,
            "prerelease": proj_version.prerelease,
            "version": str(proj_version),
            "build": proj_version.build,
        }

        # update project files
        with \
                project_file.file.open(mode="r", encoding=project_file.encoding) as fh_in, \
                tempfile.NamedTemporaryFile(mode="w", encoding=project_file.encoding, delete=False) as fh_out:
            if project_file.match == 'line':
                changes = 0
                for line in fh_in:
                    (line, cnt) = rxp.subn(replace, line)
                    if cnt:
                        changes += cnt
                    fh_out.write(line)

                if changes:
                    counters['files'] += 1
                    counters['changes'] += changes

            elif project_file.match == 'file':
                data = fh_in.read()
                (data, cnt) = rxp.subn(replace, data)
                if cnt:
                    counters['files'] += 1
                    counters['changes'] += cnt
                fh_out.write(data)

            else:
                raise ValueError("Unknown match type: \"%s\"" % project_file.match)

            fh_out.close()

            shutil.copystat(project_file.filename, fh_out.name)
            pathlib.Path(fh_out.name).rename(project_file.filename)

    return counters

def command_up(cfg, args):
    """
    Realize tasks for 'up' command

    :param cfg:
    :param args:
    :return:
    """

    vcs_handler = None
    if args.commit:
        vcs_handler = vcs.VCS(args.vcs_engine)
        vcs_handler.raise_if_cant_commit()

    version_file = version.VersionFile(args.version_file)

    try:
        current = version_file.read()
    except FileNotFoundError:
        print('Version file not found', file=sys.stderr)
        sys.exit(1)

    if args.major:
        new = current.up('major', args.value)
    elif args.minor:
        new = current.up('minor', args.value)
    elif args.patch:
        new = current.up('patch', args.value)
    else:
        new = current.up(cfg.up_part, args.value)

    version_file.write(new)
    current = new

    quant = update_project_files(args, cfg, current)

    if args.commit:
        files = {str(file.file) for file in cfg.files}
        files.add(cfg.version_file)
        vcs_handler.add_to_stage(files)
        vcs_handler.create_commit(args.vcs_commit_message % current)

    return {'current_version': current, 'quant': quant, 'commit': args.commit}

def command_set(cfg, args):
    """
    Realize tasks for 'set' command

    :param cfg:
    :param args:
    :return:
    """

    vcs_handler = None
    if args.commit:
        vcs_handler = vcs.VCS(args.vcs_engine)
        vcs_handler.raise_if_cant_commit()

    version_file = version.VersionFile(args.version_file)

    try:
        current = version_file.read()
    except FileNotFoundError:
        print('Version file not found', file=sys.stderr)
        sys.exit(1)

    if args.value:
        parsed = semver.parse(args.value)
        new = version.Version(parsed)
    else:
        new = version.Version(current)
        for type_ in version.Version.VALID_FIELDS:
            value = getattr(args, type_)
            if value:
                new = new.set(type_, value)

    version_file.write(new)
    current = new

    quant = update_project_files(args, cfg, current)

    if args.commit:
        files = {str(file.file) for file in cfg.files}
        files.add(cfg.version_file)
        vcs_handler.add_to_stage(files)
        vcs_handler.create_commit(args.vcs_commit_message % current)

    return {'current_version': current, 'quant': quant, 'commit': args.commit}

# pylint: disable=unused-argument
def command_init(cfg, args):
    """
    Realize tasks for 'init' command

    :param cfg:
    :param args:
    :return:
    """

    vcs_handler = None
    if args.commit:
        vcs_handler = vcs.VCS(args.vcs_engine)
        vcs_handler.raise_if_cant_commit()

    version_file = version.VersionFile(args.version_file)

    parsed = semver.parse(args.value)
    current = version.Version(parsed)
    version_file.write(current)

    if args.commit:
        files = {str(file.file) for file in cfg.files}
        files.add(cfg.version_file)
        vcs_handler.add_to_stage(files)
        vcs_handler.create_commit(args.vcs_commit_message % current)

    return {'current_version': current, 'quant': 0, 'commit': args.commit}

# pylint: disable=unused-argument
def command_tag(cfg, args):
    """
    Realize tasks for 'tag' command

    :param cfg:
    :param args:
    :return:
    """
    version_file = version.VersionFile(args.version_file)

    try:
        current = version_file.read()
        vcs_handler = vcs.VCS(args.vcs_engine)
        vcs_handler.create_tag(current, args.vcs_tag_params)
    except FileNotFoundError:
        print('Version file not found', file=sys.stderr)
        sys.exit(1)
    # pylint: disable=bare-except
    except:
        print('Git tag failed, do it yourself')
        if args.verbose:
            traceback.print_exc()
    else:
        print('Git tag created')

    return {'current_version': current, 'quant': 0}

# pylint: disable=unused-argument
def command_default(cfg, args):
    """
    Realize tasks when no command given

    :param cfg:
    :param args:
    :return:
    """
    version_file = version.VersionFile(args.version_file)

    try:
        current = version_file.read()
    except FileNotFoundError:
        print('Version file not found', file=sys.stderr)
        sys.exit(1)

    return {'current_version': current, 'quant': 0}

def main():
    """
    Main script

    :return:
    """

    if pathlib.Path(sys.argv[0]).parts[-1] in ('versionner', 'versionner.py'):
        print("versionner name is deprecated, use \"ver\" now!", file=sys.stderr)

    project_cfg = config.Config()
    args = parse_args(sys.argv[1:], version_file=project_cfg.version_file, date_format=project_cfg.date_format,
        up_part=project_cfg.up_part, vcs_engine=project_cfg.vcs_engine, vcs_tag_params=project_cfg.vcs_tag_params,
        default_init_version=project_cfg.default_init_version, vcs_commit_message=project_cfg.vcs_commit_message)

    commands = {'up': command_up, 'set': command_set, 'init': command_init, 'tag': command_tag}
    result = commands.get(args.get_command(), command_default)(project_cfg, args)
    quant = result['quant']
    current = result['current_version']
    commit = result.get('commit')

    print("Current version: %s" % current)

    if quant:
        print('Changed' + (' and committed' if commit else '') + ' %(files)s files (%(changes)s changes)' % quant)
<commit_msg>removed import from __future__<commit_after>"""
    Main CLI engine for versionner
    Parse input options, and make all dirty jobs.
"""

from versionner import utils

utils.validate_python_version()

import argparse
import pathlib
import re
import shutil
import sys
import tempfile
import time
import traceback

import semver

import versionner
from versionner import config
from versionner import version
from versionner import vcs

def parse_args(args, **defaults):
    """
    Parse input arguments of script.

    :rtype : argparse.Namespace
    :param args:
    :return:
    """
    prog = pathlib.Path(sys.argv[0]).parts[-1].replace('.py', '')
    prog_version = "%%(prog)s %s" % versionner.__version__

    # pylint: disable=invalid-name
    p = argparse.ArgumentParser(prog=prog, description='Helps manipulating version of the project')
    p.add_argument('--file', '-f', dest='version_file', type=str,
        default=defaults.get('version_file'),
        help="path to file where version is saved")
    p.add_argument('--version', '-v', action="version", version=prog_version)
    p.add_argument('--date-format', type=str,
        default=defaults.get('date_format'),
        help="Date format used in project files")
    p.add_argument('--vcs-engine', type=str, default=defaults.get('vcs_engine'),
        help="Select VCS engine (only git is supported currently)", )
    p.add_argument('--vcs-commit-message', '-m', type=str, default=defaults.get('vcs_commit_message'),
        help="Commit message used when committing changes")
    p.add_argument('--verbose', action="store_true", help="Be more verbose if it's possible")

    def get_command_name(name):
        # pylint: disable=missing-docstring
        _name = name

        def _():
            # pylint: disable=missing-docstring
            return _name

        return _

    sub = p.add_subparsers()

    p_init = sub.add_parser('init',
        help="Create new version file")
    p_init.add_argument('value', nargs='?', default=defaults.get('default_init_version'), type=str,
        help="Initial version")
    p_init.add_argument('--commit', '-c', action='store_true',
        help="Commit changes done by `up` command (only if there is no changes in repo before)")
    p_init.set_defaults(get_command=get_command_name('init'))

    p_up = sub.add_parser('up',
        help="Increase version")
    p_up.add_argument('--commit', '-c', action='store_true',
        help="Commit changes done by `up` command (only if there is no changes in repo before)")
    p_up.add_argument('value', nargs='?', type=int,
        help="Increase version by this value (default: 1)")
    p_up.set_defaults(get_command=get_command_name('up'))

    p_up_gr = p_up.add_mutually_exclusive_group()
    up_part = defaults.get('up_part')
    p_up_gr.add_argument('--major', '-j', action="store_true",
        help="increase major part of version" + (" (project default)" if up_part == 'major' else ""))
    p_up_gr.add_argument('--minor', '-n', action="store_true",
        help="increase minor part of version" + (" (project default)" if up_part == 'minor' else ""))
    p_up_gr.add_argument('--patch', '-p', action="store_true",
        help="increase patch part of version" + (" (project default)" if up_part == 'patch' else ""))
    p_up_gr.set_defaults(get_command=get_command_name('up'))

    p_set = sub.add_parser('set',
        help="Set version to specified one")
    p_set.add_argument('--major', '-j', type=int,
        help="set major part of version to MAJOR")
    p_set.add_argument('--minor', '-n', type=int,
        help="set minor part of version to MINOR")
    p_set.add_argument('--patch', '-p', type=int,
        help="set patch part of version to PATCH")
    p_set.add_argument('--prerelease', '-r', type=str,
        help="set prerelease part of version to PRERELEASE")
    p_set.add_argument('--build', '-b', type=str,
        help="set build part of version to BUILD")
    p_set.add_argument('--commit', '-c', action='store_true',
        help="Commit changes done by `set` command (only if there is no changes in repo before)")
    p_set.add_argument('value', nargs='?', type=str,
        help="set version to this value")
    p_set.set_defaults(get_command=get_command_name('set'))

    p_tag = sub.add_parser('tag',
        help="Create VCS tag with current version")
    p_tag.add_argument('--vcs-tag-param', dest='vcs_tag_params', type=str, action="append",
        help="Additional params for VCS for \"tag\" command")
    p_tag.set_defaults(get_command=get_command_name('tag'))

    args = p.parse_args(args)
    args.version_file = pathlib.Path(args.version_file).absolute()

    if not hasattr(args, 'get_command'):
        args.get_command = get_command_name(None)

    if args.get_command() == 'build':
        if not args.version_file.exists():
            p.error("Version file \"%s\" doesn't exists" % args.version_file)

    elif args.get_command() == 'major':
        if not args.version_file.exists():
            p.error("Version file \"%s\" doesn't exists" % args.version_file)

    elif args.get_command() == 'value':
        if args.version_file.exists():
            p.error("Version file \"%s\" already exists" % args.version_file)

    elif args.get_command() == 'tag':
        if not args.vcs_tag_params:
            args.vcs_tag_params = defaults.get('vcs_tag_params', [])

    return args

def update_project_files(args, cfg, proj_version):
    """
    Update version string in project files

    :rtype : dict
    :param args:script arguments
    :param cfg:project configuration
    :param proj_version:current version
    :return:dict :raise ValueError:
    """
    counters = {'files': 0, 'changes': 0}

    for project_file in cfg.files:
        if not project_file.file.exists():
            print("File \"%s\" not found" % project_file.filename, file=sys.stderr)
            continue

        # prepare data
        date_format = project_file.date_format or args.date_format

        rxp = re.compile(project_file.search, project_file.search_flags)
        replace = project_file.replace % {
            "date": time.strftime(date_format),
            "major": proj_version.major,
            "minor": proj_version.minor,
            "patch": proj_version.patch,
            "prerelease": proj_version.prerelease,
            "version": str(proj_version),
            "build": proj_version.build,
        }

        # update project files
        with \
                project_file.file.open(mode="r", encoding=project_file.encoding) as fh_in, \
                tempfile.NamedTemporaryFile(mode="w", encoding=project_file.encoding, delete=False) as fh_out:
            if project_file.match == 'line':
                changes = 0
                for line in fh_in:
                    (line, cnt) = rxp.subn(replace, line)
                    if cnt:
                        changes += cnt
                    fh_out.write(line)

                if changes:
                    counters['files'] += 1
                    counters['changes'] += changes

            elif project_file.match == 'file':
                data = fh_in.read()
                (data, cnt) = rxp.subn(replace, data)
                if cnt:
                    counters['files'] += 1
                    counters['changes'] += cnt
                fh_out.write(data)

            else:
                raise ValueError("Unknown match type: \"%s\"" % project_file.match)

            fh_out.close()

            shutil.copystat(project_file.filename, fh_out.name)
            pathlib.Path(fh_out.name).rename(project_file.filename)

    return counters

def command_up(cfg, args):
    """
    Realize tasks for 'up' command

    :param cfg:
    :param args:
    :return:
    """

    vcs_handler = None
    if args.commit:
        vcs_handler = vcs.VCS(args.vcs_engine)
        vcs_handler.raise_if_cant_commit()

    version_file = version.VersionFile(args.version_file)

    try:
        current = version_file.read()
    except FileNotFoundError:
        print('Version file not found', file=sys.stderr)
        sys.exit(1)

    if args.major:
        new = current.up('major', args.value)
    elif args.minor:
        new = current.up('minor', args.value)
    elif args.patch:
        new = current.up('patch', args.value)
    else:
        new = current.up(cfg.up_part, args.value)

    version_file.write(new)
    current = new

    quant = update_project_files(args, cfg, current)

    if args.commit:
        files = {str(file.file) for file in cfg.files}
        files.add(cfg.version_file)
        vcs_handler.add_to_stage(files)
        vcs_handler.create_commit(args.vcs_commit_message % current)

    return {'current_version': current, 'quant': quant, 'commit': args.commit}

def command_set(cfg, args):
    """
    Realize tasks for 'set' command

    :param cfg:
    :param args:
    :return:
    """

    vcs_handler = None
    if args.commit:
        vcs_handler = vcs.VCS(args.vcs_engine)
        vcs_handler.raise_if_cant_commit()

    version_file = version.VersionFile(args.version_file)

    try:
        current = version_file.read()
    except FileNotFoundError:
        print('Version file not found', file=sys.stderr)
        sys.exit(1)

    if args.value:
        parsed = semver.parse(args.value)
        new = version.Version(parsed)
    else:
        new = version.Version(current)
        for type_ in version.Version.VALID_FIELDS:
            value = getattr(args, type_)
            if value:
                new = new.set(type_, value)

    version_file.write(new)
    current = new

    quant = update_project_files(args, cfg, current)

    if args.commit:
        files = {str(file.file) for file in cfg.files}
        files.add(cfg.version_file)
        vcs_handler.add_to_stage(files)
        vcs_handler.create_commit(args.vcs_commit_message % current)

    return {'current_version': current, 'quant': quant, 'commit': args.commit}

# pylint: disable=unused-argument
def command_init(cfg, args):
    """
    Realize tasks for 'init' command

    :param cfg:
    :param args:
    :return:
    """

    vcs_handler = None
    if args.commit:
        vcs_handler = vcs.VCS(args.vcs_engine)
        vcs_handler.raise_if_cant_commit()

    version_file = version.VersionFile(args.version_file)

    parsed = semver.parse(args.value)
    current = version.Version(parsed)
    version_file.write(current)

    if args.commit:
        files = {str(file.file) for file in cfg.files}
        files.add(cfg.version_file)
        vcs_handler.add_to_stage(files)
        vcs_handler.create_commit(args.vcs_commit_message % current)

    return {'current_version': current, 'quant': 0, 'commit': args.commit}

# pylint: disable=unused-argument
def command_tag(cfg, args):
    """
    Realize tasks for 'tag' command

    :param cfg:
    :param args:
    :return:
    """
    version_file = version.VersionFile(args.version_file)

    try:
        current = version_file.read()
        vcs_handler = vcs.VCS(args.vcs_engine)
        vcs_handler.create_tag(current, args.vcs_tag_params)
    except FileNotFoundError:
        print('Version file not found', file=sys.stderr)
        sys.exit(1)
    # pylint: disable=bare-except
    except:
        print('Git tag failed, do it yourself')
        if args.verbose:
            traceback.print_exc()
    else:
        print('Git tag created')

    return {'current_version': current, 'quant': 0}

# pylint: disable=unused-argument
def command_default(cfg, args):
    """
    Realize tasks when no command given

    :param cfg:
    :param args:
    :return:
    """
    version_file = version.VersionFile(args.version_file)

    try:
        current = version_file.read()
    except FileNotFoundError:
        print('Version file not found', file=sys.stderr)
        sys.exit(1)

    return {'current_version': current, 'quant': 0}

def main():
    """
    Main script

    :return:
    """

    if pathlib.Path(sys.argv[0]).parts[-1] in ('versionner', 'versionner.py'):
        print("versionner name is deprecated, use \"ver\" now!", file=sys.stderr)

    project_cfg = config.Config()
    args = parse_args(sys.argv[1:], version_file=project_cfg.version_file, date_format=project_cfg.date_format,
        up_part=project_cfg.up_part, vcs_engine=project_cfg.vcs_engine, vcs_tag_params=project_cfg.vcs_tag_params,
        default_init_version=project_cfg.default_init_version, vcs_commit_message=project_cfg.vcs_commit_message)

    commands = {'up': command_up, 'set': command_set, 'init': command_init, 'tag': command_tag}
    result = commands.get(args.get_command(), command_default)(project_cfg, args)
    quant = result['quant']
    current = result['current_version']
    commit = result.get('commit')

    print("Current version: %s" % current)

    if quant:
        print('Changed' + (' and committed' if commit else '') + ' %(files)s files (%(changes)s changes)' % quant)
<|endoftext|>


<commit_before>#!/usr/bin/env python

__all__ = ['tumblr_download']

from ..common import *

import re

def tumblr_download(url, output_dir = '.', merge = True, info_only = False, **kwargs):
    html = parse.unquote(get_html(url)).replace('\/', '/')
    feed = r1(r'<meta property="og:type" content="tumblr-feed:(\w+)" />', html)

    if feed == 'audio':
        real_url = r1(r'source src=\\x22([^\\]+)\\', html)
        if not real_url:
            real_url = r1(r'audio_file=([^&]+)&', html) + '?plead=please-dont-download-this-or-our-lawyers-wont-let-us-host-audio'
    elif feed == 'video':
        iframe_url = r1(r'<iframe src=\'([^\']*)\'', html)
        iframe_html = get_html(iframe_url)
        real_url = r1(r'<source src="([^"]*)"', iframe_html)
    else:
        real_url = r1(r'<source src="([^"]*)"', html)

    title = unescape_html(r1(r'<meta property="og:title" content="([^"]*)" />', html) or
        r1(r'<meta property="og:description" content="([^"]*)" />', html) or
        r1(r'<title>([^<\n]*)', html) or url.split("/")[4]).replace('\n', '')

    type, ext, size = url_info(real_url)

    print_info(site_info, title, type, size)
    if not info_only:
        download_urls([real_url], title, ext, size, output_dir, merge = merge)

site_info = "Tumblr.com"
download = tumblr_download
download_playlist = playlist_not_supported('tumblr')
<commit_msg>[tumblr] download photo / photoset<commit_after>#!/usr/bin/env python

__all__ = ['tumblr_download']

from ..common import *

import re

def tumblr_download(url, output_dir='.', merge=True, info_only=False, **kwargs):
    html = parse.unquote(get_html(url)).replace('\/', '/')
    feed = r1(r'<meta property="og:type" content="tumblr-feed:(\w+)" />', html)

    if feed in ['photo', 'photoset'] or feed is None:
        page_title = r1(r'<meta name="description" content="([^"\n]+)', html) or \
                     r1(r'<meta property="og:description" content="([^"\n]+)', html) or \
                     r1(r'<title>([^<\n]*)', html)
        urls = re.findall(r'(https?://[^;"]+/tumblr_[^;"]+_\d+\.jpg)', html) +\
               re.findall(r'(https?://[^;"]+/tumblr_[^;"]+_\d+\.png)', html) +\
               re.findall(r'(https?://[^;"]+/tumblr_[^";]+_\d+\.gif)', html)

        tuggles = {}
        for url in urls:
            filename = parse.unquote(url.split('/')[-1])
            title = '.'.join(filename.split('.')[:-1])
            tumblr_id = r1(r'^tumblr_(.+)_\d+$', title)
            quality = int(r1(r'^tumblr_.+_(\d+)$', title))
            ext = filename.split('.')[-1]
            size = int(get_head(url)['Content-Length'])
            if tumblr_id not in tuggles or tuggles[tumblr_id]['quality'] < quality:
                tuggles[tumblr_id] = {
                    'title': title,
                    'url': url,
                    'quality': quality,
                    'ext': ext,
                    'size': size,
                }

        size = sum([tuggles[t]['size'] for t in tuggles])
        print_info(site_info, page_title, None, size)

        if not info_only:
            for t in tuggles:
                title = tuggles[t]['title']
                ext = tuggles[t]['ext']
                size = tuggles[t]['size']
                url = tuggles[t]['url']
                print_info(site_info, title, ext, size)
                download_urls([url], title, ext, size,
                              output_dir=output_dir)
        return

    elif feed == 'audio':
        real_url = r1(r'source src=\\x22([^\\]+)\\', html)
        if not real_url:
            real_url = r1(r'audio_file=([^&]+)&', html) + '?plead=please-dont-download-this-or-our-lawyers-wont-let-us-host-audio'
    elif feed == 'video':
        iframe_url = r1(r'<iframe src=\'([^\']*)\'', html)
        iframe_html = get_html(iframe_url)
        real_url = r1(r'<source src="([^"]*)"', iframe_html)
    else:
        real_url = r1(r'<source src="([^"]*)"', html)

    title = unescape_html(r1(r'<meta property="og:title" content="([^"]*)" />', html) or
        r1(r'<meta property="og:description" content="([^"]*)" />', html) or
        r1(r'<title>([^<\n]*)', html) or url.split("/")[4]).replace('\n', '')

    type, ext, size = url_info(real_url)

    print_info(site_info, title, type, size)
    if not info_only:
        download_urls([real_url], title, ext, size, output_dir, merge = merge)

site_info = "Tumblr.com"
download = tumblr_download
download_playlist = playlist_not_supported('tumblr')
<|endoftext|>


<commit_before>"""
Main entry point for NPC

Provides the cli method for interpreting commandline arguments.
"""

import argparse
from os import chdir, getcwd, path
from subprocess import run

# local packages
import npc
from npc import commands, util, settings
from npc import gui as gui_core

def start(argv):
    """
    Run the command-line interface

    Args:
        argv (list): Arguments from the command invocation

    Returns:
        Return code indicating success or failure type. See `util.Result` for a
        list of return codes.
    """

    # create parser and parse args
    parser = _make_parser()
    args = parser.parse_args(argv)

    # change to the proper campaign directory if needed
    base = args.campaign
    if base == 'auto':
        base = util.find_campaign_root()

    try:
        chdir(base)
    except OSError as err:
        util.error("{}: '{}'".format(err.strerror, base))
        return 4

    # load settings data
    try:
        prefs = settings.InternalSettings(args.debug)
    except OSError as err:
        util.error(err.strerror)
        return 4

    changeling_errors = settings.lint_changeling_settings(prefs)
    if changeling_errors:
        print("\n".join(changeling_errors))
        return 5

    # show help when no input was given
    if not hasattr(args, 'func'):
        parser.print_help()
        return 0

    # get args as a dict
    full_args = vars(args)
    try:
        # load default character path if search field is at its default
        if full_args['search'] is None:
            full_args['search'] = [prefs.get('paths.characters')]
    except KeyError:
        pass

    # run the command
    try:
        if 'serialize' in full_args:
            serial_args = [full_args[k] for k in full_args['serialize']]
            for k in full_args['serialize']:
                full_args.pop(k)
        else:
            serial_args = []

        result = args.func(*serial_args, **full_args)
    except AttributeError as err:
        if args.debug:
            raise
        util.error(err)
        return 6

    # handle errors
    if not result.success:
        util.error(result)
        return result.errcode

    if not args.batch:
        # print any messages that were returned
        if result.printable:
            print("\n".join(result.printable))

        # open the resulting files, if allowed
        if result.openable:
            run([prefs.get("editor")] + result.openable)

    return 0

def _make_parser():
    """
    Construct the arguments parser

    Returns:
        Complete argparser object
    """
    # This parser stores options shared by all character creation commands. It is never exposed directly.
    character_parser = argparse.ArgumentParser(add_help=False)
    character_parser.add_argument('name', help="Character name", metavar='name')
    character_parser.add_argument('-g', '--groups', default=None, nargs="*", help='Name of a group that counts the character as a member', metavar='group')
    character_parser.add_argument('--dead', default=False, const='', nargs='?', help='Mark that the character has died, with optional notes', metavar='notes')
    character_parser.add_argument('--foreign', default=False, help="Mark that the character is foreign to the main campaign setting, with optional notes on where they're from", metavar='location')
    character_parser.set_defaults(serialize=['name', 'ctype'])

    # Parent parser for shared pathing options
    paths_parser = argparse.ArgumentParser(add_help=False)
    paths_parser.add_argument('--search', nargs="*", default=None, help="Paths to search. Individual files are added verbatim and directories are searched recursively.", metavar="PATH")
    paths_parser.add_argument('--ignore', nargs="*", default=None, help="Paths to skip when searching for character files", metavar="PATH")
    paths_parser.set_defaults(serialize=['search'])

    # This is the main parser which handles program-wide options. These should be kept sparse.
    parser = argparse.ArgumentParser(description='GM helper script to manage game files')
    parser.add_argument('-b', '--batch', action='store_true', default=False, help="Do not open any newly created files")
    parser.add_argument('--campaign', default='auto', help="Use the campaign files in a different directory", metavar='DIR')
    parser.add_argument('--debug', action='store_true', default=False, help="Show all error messages, not just important ones")
    parser.add_argument('--version', action='version', version=npc.VERSION)
    subparsers = parser.add_subparsers(title='Subcommands', description="Commands that can be run on the current campaign. See `%(prog)s <command> -h` to get help with individual commands.")

    # Subcommand to create the basic directories
    parser_init = subparsers.add_parser('init', help="Create the basic directory structure for campaign files")
    parser_init.add_argument('-n', '--name', default=None, help="Name of the campaign. Defaults to the name of the current directory.", dest="campaign_name")
    parser_init.add_argument('-t', '--types', action="store_true", default=False, help="Create directories for all character types", dest="create_types")
    parser_init.add_argument('-a', '--all', action="store_true", default=False, help="Create all optional directories", dest="create_all")
    parser_init.add_argument('-v', '--verbose', action="store_true", default=False, help="Show the changes that are made")
    parser_init.add_argument('--dryrun', action="store_true", default=False, help="Show what would be created, but do not actually change anything")
    parser_init.set_defaults(func=commands.init)

    # Session subcommand
    parser_session = subparsers.add_parser('session', help="Create files for a new game session")
    parser_session.set_defaults(func=commands.session)

    # Create generic character
    parser_generic = subparsers.add_parser('new', parents=[character_parser], help="Create a new character from the named template")
    parser_generic.add_argument('ctype', metavar='template', help="Template to use. Must be configured in settings")
    parser_generic.set_defaults(func=commands.create_simple)

    # These parsers are just named subcommand entry points to create simple characters
    parser_human = subparsers.add_parser('human', aliases=['h'], parents=[character_parser], help="Create a new human character. Alias for `npc new human`")
    parser_human.set_defaults(func=commands.create_simple, ctype="human")
    parser_fetch = subparsers.add_parser('fetch', parents=[character_parser], help="Create a new fetch character. Alias for `npc new fetch`")
    parser_fetch.set_defaults(func=commands.create_simple, ctype="fetch")
    parser_goblin = subparsers.add_parser('goblin', parents=[character_parser], help="Create a new goblin character. Alias for `npc new goblin`")
    parser_goblin.set_defaults(func=commands.create_simple, ctype="goblin")

    # Subcommand for making changelings, with their unique options
    parser_changeling = subparsers.add_parser('changeling', aliases=['c'], parents=[character_parser], help="Create a new changeling character")
    parser_changeling.add_argument('seeming', help="The character's Seeming", metavar='seeming')
    parser_changeling.add_argument('kith', help="The character's Kith", metavar='kith')
    parser_changeling.add_argument('-c', '--court', help="The character's Court", metavar='court')
    parser_changeling.add_argument('-m', '--motley', help="The character's Motley", metavar='motley')
    parser_changeling.set_defaults(func=commands.create_changeling, serialize=['name', 'seeming', 'kith'])

    # Subcommand for linting characer files
    parser_lint = subparsers.add_parser('lint', parents=[paths_parser], help="Check the character files for minimum completeness")
    parser_lint.add_argument('-f', '--fix', action='store_true', default=False, help="Automatically fix certain problems")
    parser_lint.add_argument('--strict', action='store_true', default=False, help="Report all non-critical errors")
    parser_lint.set_defaults(func=commands.lint)

    # Subcommand to list character data in multiple formats
    parser_list = subparsers.add_parser('list', parents=[paths_parser], help="Generate an NPC Listing")
    parser_list.add_argument('-t', '--format', choices=['markdown', 'md', 'json', 'htm', 'html'], default='default', help="Format to use for the listing. Defaults to the list format in settings", dest="fmt")
    parser_list.add_argument('-m', '--metadata', nargs="?", const='default', default=False, help="Add metadata to the output. If the output format supports more than one metadata format, you can specify that format as well.")
    parser_list.add_argument('--title', help="Title to show in the metadata. Overrides the title in settings.", metavar="TITLE")
    parser_list.add_argument('-o', '--outfile', nargs="?", const='-', default=None, help="File where the listing will be saved")
    parser_list.add_argument('--sort', choices=['first', 'last'], default='last', help="The sort order for characters. Defaults to 'last'.")
    parser_list.set_defaults(func=commands.listing)

    # Dump raw character data
    parser_dump = subparsers.add_parser('dump', parents=[paths_parser], help="Export raw json data of all characters")
    parser_dump.add_argument('-s', '--sort', action="store_true", default=False, help="Sort the characters")
    parser_dump.add_argument('-m', '--metadata', action="store_true", default=False, help="Add metadata to the output.")
    parser_dump.add_argument('-o', '--outfile', nargs="?", const='-', default=None, help="File where the listing will be saved")
    parser_dump.set_defaults(func=commands.dump)

    # Reorganize character files subcommand
    parser_reorg = subparsers.add_parser('reorg', parents=[paths_parser], help="Move character files to the most appropriate directories")
    parser_reorg.add_argument('-p', '--purge', action="store_true", default=False, help="After moving all files, remove any empty directories within the base characters path")
    parser_reorg.add_argument('-v', '--verbose', action="store_true", default=False, help="Show the changes that are made")
    parser_reorg.add_argument('-d', '--dryrun', action="store_true", default=False, help="Show the changes that would be made, but don't change anything", dest="dryrun")
    parser_reorg.set_defaults(func=commands.reorg)

    # Open settings files
    parser_settings = subparsers.add_parser('settings', help="Open (and create if needed) a settings file")
    parser_settings.add_argument('location', choices=['user', 'campaign'], help="The settings file to load")
    parser_settings.add_argument('-t', '--type', choices=['base', 'changeling'], help="Open a type-specific settings file", metavar='type', dest='settings_type')
    parser_settings.add_argument('-d', '--defaults', action="store_true", default=False, help="Open the default settings file for easy reference", dest='show_defaults')
    parser_settings.set_defaults(func=commands.open_settings, serialize=['location'])

    # Report on character tags
    parser_report = subparsers.add_parser('report', help="Create a report of the values for one or more tags")
    parser_report.add_argument('tags', nargs="+", help="Tag names to analyze")
    parser_report.add_argument('--search', nargs="*", default=None, help="Paths to search. Individual files are added verbatim and directories are searched recursively.", metavar="PATH")
    parser_report.add_argument('--ignore', nargs="*", default=None, help="Paths to skip when searching for character files", metavar="PATH")
    parser_report.add_argument('-t', '--format', choices=['json', 'htm', 'html', 'md', 'markdown'], default='default', help="Format to use for the tables. Defaults to the table format in settings", dest="fmt")
    parser_report.add_argument('-o', '--outfile', nargs="?", const='-', default=None, help="File where the listing will be saved")
    parser_report.set_defaults(func=commands.report, serialize=['tags'])

    return parser
<commit_msg>Simplify argument serialization<commit_after>"""
Main entry point for NPC

Provides the cli method for interpreting commandline arguments.
"""

import argparse
from os import chdir, getcwd, path
from subprocess import run

# local packages
import npc
from npc import commands, util, settings
from npc import gui as gui_core

def start(argv):
    """
    Run the command-line interface

    Args:
        argv (list): Arguments from the command invocation

    Returns:
        Return code indicating success or failure type. See `util.Result` for a
        list of return codes.
    """

    # create parser and parse args
    parser = _make_parser()
    args = parser.parse_args(argv)

    # change to the proper campaign directory if needed
    base = args.campaign
    if base == 'auto':
        base = util.find_campaign_root()

    try:
        chdir(base)
    except OSError as err:
        util.error("{}: '{}'".format(err.strerror, base))
        return 4

    # load settings data
    try:
        prefs = settings.InternalSettings(args.debug)
    except OSError as err:
        util.error(err.strerror)
        return 4

    changeling_errors = settings.lint_changeling_settings(prefs)
    if changeling_errors:
        print("\n".join(changeling_errors))
        return 5

    # show help when no input was given
    if not hasattr(args, 'func'):
        parser.print_help()
        return 0

    # get args as a dict
    full_args = vars(args)
    try:
        # load default character path if search field is at its default
        if full_args['search'] is None:
            full_args['search'] = [prefs.get('paths.characters')]
    except KeyError:
        pass

    # run the command
    try:
        serial_args = [full_args.pop(k) for k in full_args.get('serialize', [])]

        result = args.func(*serial_args, **full_args)
    except AttributeError as err:
        if args.debug:
            raise
        util.error(err)
        return 6

    # handle errors
    if not result.success:
        util.error(result)
        return result.errcode

    if not args.batch:
        # print any messages that were returned
        if result.printable:
            print("\n".join(result.printable))

        # open the resulting files, if allowed
        if result.openable:
            run([prefs.get("editor")] + result.openable)

    return 0

def _make_parser():
    """
    Construct the arguments parser

    Returns:
        Complete argparser object
    """
    # This parser stores options shared by all character creation commands. It is never exposed directly.
    character_parser = argparse.ArgumentParser(add_help=False)
    character_parser.add_argument('name', help="Character name", metavar='name')
    character_parser.add_argument('-g', '--groups', default=None, nargs="*", help='Name of a group that counts the character as a member', metavar='group')
    character_parser.add_argument('--dead', default=False, const='', nargs='?', help='Mark that the character has died, with optional notes', metavar='notes')
    character_parser.add_argument('--foreign', default=False, help="Mark that the character is foreign to the main campaign setting, with optional notes on where they're from", metavar='location')
    character_parser.set_defaults(serialize=['name', 'ctype'])

    # Parent parser for shared pathing options
    paths_parser = argparse.ArgumentParser(add_help=False)
    paths_parser.add_argument('--search', nargs="*", default=None, help="Paths to search. Individual files are added verbatim and directories are searched recursively.", metavar="PATH")
    paths_parser.add_argument('--ignore', nargs="*", default=None, help="Paths to skip when searching for character files", metavar="PATH")
    paths_parser.set_defaults(serialize=['search'])

    # This is the main parser which handles program-wide options. These should be kept sparse.
    parser = argparse.ArgumentParser(description='GM helper script to manage game files')
    parser.add_argument('-b', '--batch', action='store_true', default=False, help="Do not open any newly created files")
    parser.add_argument('--campaign', default='auto', help="Use the campaign files in a different directory", metavar='DIR')
    parser.add_argument('--debug', action='store_true', default=False, help="Show all error messages, not just important ones")
    parser.add_argument('--version', action='version', version=npc.VERSION)
    subparsers = parser.add_subparsers(title='Subcommands', description="Commands that can be run on the current campaign. See `%(prog)s <command> -h` to get help with individual commands.")

    # Subcommand to create the basic directories
    parser_init = subparsers.add_parser('init', help="Create the basic directory structure for campaign files")
    parser_init.add_argument('-n', '--name', default=None, help="Name of the campaign. Defaults to the name of the current directory.", dest="campaign_name")
    parser_init.add_argument('-t', '--types', action="store_true", default=False, help="Create directories for all character types", dest="create_types")
    parser_init.add_argument('-a', '--all', action="store_true", default=False, help="Create all optional directories", dest="create_all")
    parser_init.add_argument('-v', '--verbose', action="store_true", default=False, help="Show the changes that are made")
    parser_init.add_argument('--dryrun', action="store_true", default=False, help="Show what would be created, but do not actually change anything")
    parser_init.set_defaults(func=commands.init)

    # Session subcommand
    parser_session = subparsers.add_parser('session', help="Create files for a new game session")
    parser_session.set_defaults(func=commands.session)

    # Create generic character
    parser_generic = subparsers.add_parser('new', parents=[character_parser], help="Create a new character from the named template")
    parser_generic.add_argument('ctype', metavar='template', help="Template to use. Must be configured in settings")
    parser_generic.set_defaults(func=commands.create_simple)

    # These parsers are just named subcommand entry points to create simple characters
    parser_human = subparsers.add_parser('human', aliases=['h'], parents=[character_parser], help="Create a new human character. Alias for `npc new human`")
    parser_human.set_defaults(func=commands.create_simple, ctype="human")
    parser_fetch = subparsers.add_parser('fetch', parents=[character_parser], help="Create a new fetch character. Alias for `npc new fetch`")
    parser_fetch.set_defaults(func=commands.create_simple, ctype="fetch")
    parser_goblin = subparsers.add_parser('goblin', parents=[character_parser], help="Create a new goblin character. Alias for `npc new goblin`")
    parser_goblin.set_defaults(func=commands.create_simple, ctype="goblin")

    # Subcommand for making changelings, with their unique options
    parser_changeling = subparsers.add_parser('changeling', aliases=['c'], parents=[character_parser], help="Create a new changeling character")
    parser_changeling.add_argument('seeming', help="The character's Seeming", metavar='seeming')
    parser_changeling.add_argument('kith', help="The character's Kith", metavar='kith')
    parser_changeling.add_argument('-c', '--court', help="The character's Court", metavar='court')
    parser_changeling.add_argument('-m', '--motley', help="The character's Motley", metavar='motley')
    parser_changeling.set_defaults(func=commands.create_changeling, serialize=['name', 'seeming', 'kith'])

    # Subcommand for linting characer files
    parser_lint = subparsers.add_parser('lint', parents=[paths_parser], help="Check the character files for minimum completeness")
    parser_lint.add_argument('-f', '--fix', action='store_true', default=False, help="Automatically fix certain problems")
    parser_lint.add_argument('--strict', action='store_true', default=False, help="Report all non-critical errors")
    parser_lint.set_defaults(func=commands.lint)

    # Subcommand to list character data in multiple formats
    parser_list = subparsers.add_parser('list', parents=[paths_parser], help="Generate an NPC Listing")
    parser_list.add_argument('-t', '--format', choices=['markdown', 'md', 'json', 'htm', 'html'], default='default', help="Format to use for the listing. Defaults to the list format in settings", dest="fmt")
    parser_list.add_argument('-m', '--metadata', nargs="?", const='default', default=False, help="Add metadata to the output. If the output format supports more than one metadata format, you can specify that format as well.")
    parser_list.add_argument('--title', help="Title to show in the metadata. Overrides the title in settings.", metavar="TITLE")
    parser_list.add_argument('-o', '--outfile', nargs="?", const='-', default=None, help="File where the listing will be saved")
    parser_list.add_argument('--sort', choices=['first', 'last'], default='last', help="The sort order for characters. Defaults to 'last'.")
    parser_list.set_defaults(func=commands.listing)

    # Dump raw character data
    parser_dump = subparsers.add_parser('dump', parents=[paths_parser], help="Export raw json data of all characters")
    parser_dump.add_argument('-s', '--sort', action="store_true", default=False, help="Sort the characters")
    parser_dump.add_argument('-m', '--metadata', action="store_true", default=False, help="Add metadata to the output.")
    parser_dump.add_argument('-o', '--outfile', nargs="?", const='-', default=None, help="File where the listing will be saved")
    parser_dump.set_defaults(func=commands.dump)

    # Reorganize character files subcommand
    parser_reorg = subparsers.add_parser('reorg', parents=[paths_parser], help="Move character files to the most appropriate directories")
    parser_reorg.add_argument('-p', '--purge', action="store_true", default=False, help="After moving all files, remove any empty directories within the base characters path")
    parser_reorg.add_argument('-v', '--verbose', action="store_true", default=False, help="Show the changes that are made")
    parser_reorg.add_argument('-d', '--dryrun', action="store_true", default=False, help="Show the changes that would be made, but don't change anything", dest="dryrun")
    parser_reorg.set_defaults(func=commands.reorg)

    # Open settings files
    parser_settings = subparsers.add_parser('settings', help="Open (and create if needed) a settings file")
    parser_settings.add_argument('location', choices=['user', 'campaign'], help="The settings file to load")
    parser_settings.add_argument('-t', '--type', choices=['base', 'changeling'], help="Open a type-specific settings file", metavar='type', dest='settings_type')
    parser_settings.add_argument('-d', '--defaults', action="store_true", default=False, help="Open the default settings file for easy reference", dest='show_defaults')
    parser_settings.set_defaults(func=commands.open_settings, serialize=['location'])

    # Report on character tags
    parser_report = subparsers.add_parser('report', help="Create a report of the values for one or more tags")
    parser_report.add_argument('tags', nargs="+", help="Tag names to analyze")
    parser_report.add_argument('--search', nargs="*", default=None, help="Paths to search. Individual files are added verbatim and directories are searched recursively.", metavar="PATH")
    parser_report.add_argument('--ignore', nargs="*", default=None, help="Paths to skip when searching for character files", metavar="PATH")
    parser_report.add_argument('-t', '--format', choices=['json', 'htm', 'html', 'md', 'markdown'], default='default', help="Format to use for the tables. Defaults to the table format in settings", dest="fmt")
    parser_report.add_argument('-o', '--outfile', nargs="?", const='-', default=None, help="File where the listing will be saved")
    parser_report.set_defaults(func=commands.report, serialize=['tags'])

    return parser
<|endoftext|>


<commit_before><commit_msg>Check Stevedore extensions on import (#840)<commit_after><|endoftext|>


<commit_before>__version__ = '0.4.4'

try:
    __VESPA_SETUP__
except NameError:
    __VESPA_SETUP__ = False

if not __VESPA_SETUP__:

    on_rtd = os.environ.get('READTHEDOCS') == 'True'

    if not on_rtd:

        __all__ = ['FPPCalculation',
                   'EclipsePopulation', 'EBPopulation',
                   'HEBPopulation', 'BEBPopulation',
                   'PlanetPopulation', 'PopulationSet',
                   'StarPopulation', 'MultipleStarPopulation',
                   'ColormatchMultipleStarPopulation',
                   'Spectroscopic_MultipleStarPopulation',
                   'BGStarPopulation', 'BGStarPopulation_TRILEGAL',
                   'BinaryPopulation', 'Simulated_BinaryPopulation',
                   'Raghavan_BinaryPopulation', 'TriplePopulation',
                   'MAInterpolationFunction']

        #StarPopulation & children
        from .stars.populations import StarPopulation
        from .stars.populations import MultipleStarPopulation
        from .stars.populations import ColormatchMultipleStarPopulation
        from .stars.populations import Spectroscopic_MultipleStarPopulation
        from .stars.populations import BGStarPopulation, BGStarPopulation_TRILEGAL
        from .stars.populations import BinaryPopulation
        from .stars.populations import Simulated_BinaryPopulation
        from .stars.populations import Raghavan_BinaryPopulation
        from .stars.populations import TriplePopulation

        from .transit_basic import MAInterpolationFunction

        #EclipsePopulation & children
        from .populations import EclipsePopulation
        from .populations import EBPopulation, HEBPopulation, BEBPopulation
        from .populations import PlanetPopulation
        from .populations import PopulationSet

        #from .populations import calculate_eclipses
        from .transitsignal import TransitSignal

        from .fpp import FPPCalculation

<commit_msg>import os<commit_after>__version__ = '0.4.4'

try:
    __VESPA_SETUP__
except NameError:
    __VESPA_SETUP__ = False

if not __VESPA_SETUP__:

    import os

    on_rtd = os.environ.get('READTHEDOCS') == 'True'

    if not on_rtd:

        __all__ = ['FPPCalculation',
                   'EclipsePopulation', 'EBPopulation',
                   'HEBPopulation', 'BEBPopulation',
                   'PlanetPopulation', 'PopulationSet',
                   'StarPopulation', 'MultipleStarPopulation',
                   'ColormatchMultipleStarPopulation',
                   'Spectroscopic_MultipleStarPopulation',
                   'BGStarPopulation', 'BGStarPopulation_TRILEGAL',
                   'BinaryPopulation', 'Simulated_BinaryPopulation',
                   'Raghavan_BinaryPopulation', 'TriplePopulation',
                   'MAInterpolationFunction']

        #StarPopulation & children
        from .stars.populations import StarPopulation
        from .stars.populations import MultipleStarPopulation
        from .stars.populations import ColormatchMultipleStarPopulation
        from .stars.populations import Spectroscopic_MultipleStarPopulation
        from .stars.populations import BGStarPopulation, BGStarPopulation_TRILEGAL
        from .stars.populations import BinaryPopulation
        from .stars.populations import Simulated_BinaryPopulation
        from .stars.populations import Raghavan_BinaryPopulation
        from .stars.populations import TriplePopulation

        from .transit_basic import MAInterpolationFunction

        #EclipsePopulation & children
        from .populations import EclipsePopulation
        from .populations import EBPopulation, HEBPopulation, BEBPopulation
        from .populations import PlanetPopulation
        from .populations import PopulationSet

        #from .populations import calculate_eclipses
        from .transitsignal import TransitSignal

        from .fpp import FPPCalculation

<|endoftext|>


<commit_before>from cStringIO import StringIO
from zeit.connector.connector import CannonicalId
from zeit.connector.dav.interfaces import DAVNotFoundError
import ZConfig
import ast
import email.utils
import gocept.cache.property
import logging
import lxml.etree
import os
import os.path
import urlparse
import zeit.connector.dav.interfaces
import zeit.connector.interfaces
import zeit.connector.resource
import zope.app.appsetup.product
import zope.app.file.image
import zope.interface

ID_NAMESPACE = u'http://xml.zeit.de/'

log = logging.getLogger(__name__)

class Connector(object):
    """Connect to the CMS backend in read-only mode.

    The current implementation does *not* talk to the CMS backend but to
    a data store in the filesystem, the path to which needs to be configured.

    """

    zope.interface.implements(zeit.connector.interfaces.IConnector)

    resource_class = zeit.connector.resource.CachedResource
    canonicalize_directories = True

    _set_lastmodified_property = False

    property_cache = gocept.cache.property.TransactionBoundCache(
        '_v_property_cache', dict)
    body_cache = gocept.cache.property.TransactionBoundCache(
        '_v_body_cache', dict)
    child_name_cache = gocept.cache.property.TransactionBoundCache(
        '_v_child_name_cache', dict)
    canonical_id_cache = gocept.cache.property.TransactionBoundCache(
        '_v_canonical_id_cache', dict)

    def __init__(self, repository_path):
        self.repository_path = repository_path

    def listCollection(self, id):
        """List the filenames of a collection identified by path. """
        id = self._get_cannonical_id(id)
        try:
            return self.child_name_cache[id]
        except KeyError:
            pass

        path = self._path(id)
        names = self._get_collection_names(path)
        if not names:
            try:
                self[id]
            except KeyError:
                # XXX mimic the real behaviour -- real *should* probably raise
                # KeyError, but doesn't at the moment.
                raise DAVNotFoundError(404, 'Not Found', id, '')

        result = []
        for name in sorted(names):
            name = unicode(name)
            child_id = unicode(
                self._get_cannonical_id(self._make_id(path + (name, ))))
            result.append((name, child_id))
        self.child_name_cache[id] = result
        return result

    def _get_collection_names(self, path):
        absolute_path = self._absolute_path(path)
        names = (set(os.listdir(absolute_path))
                 if os.path.isdir(absolute_path) else set())
        for x in names.copy():
            if x.endswith('.meta') and x[:-5] in names:
                names.remove(x)
            if x.startswith('.'):
                names.remove(x)
        return names

    def getResourceType(self, id):
        id = self._get_cannonical_id(id)
        __traceback_info__ = id

        properties = self._get_properties(id)
        if properties:
            type = properties.get(
                zeit.connector.interfaces.RESOURCE_TYPE_PROPERTY)
            if type:
                return type

        path = self._absolute_path(self._path(id))
        if os.path.isdir(path):
            return 'collection'

        f = self._get_file(id)
        data = f.read(200)
        f.close()
        content_type, width, height = zope.app.file.image.getImageInfo(data)
        if content_type:
            return 'image'
        return 'unknown'

    def __getitem__(self, id):
        id = self._get_cannonical_id(id)
        properties = self._get_properties(id)
        type = self.getResourceType(id)
        properties[zeit.connector.interfaces.RESOURCE_TYPE_PROPERTY] = type
        path = self._path(id)
        name = path[-1] if path else ''
        return self.resource_class(
            unicode(id), name, type,
            lambda: self._get_properties(id),
            lambda: self._get_body(id),
            content_type=self._get_content_type(id, type))

    def _get_body(self, id):
        try:
            return StringIO(self.body_cache[id])
        except KeyError:
            pass
        try:
            f = self._get_file(id)
            data = f.read()
            f.close()
        except:
            data = ''
        self.body_cache[id] = data
        return StringIO(data)

    def _get_content_type(self, id, type):
        return 'httpd/unix-directory' if type == 'collection' else ''

    def __setitem__(self, id, object):
        raise NotImplementedError()

    def __delitem__(self, id):
        raise NotImplementedError()

    def __contains__(self, id):
        try:
            self[id]
        except KeyError:
            return False
        return True

    def add(self, object, verify_etag=True):
        raise NotImplementedError()

    def copy(self, old_id, new_id):
        raise NotImplementedError()

    def move(self, old_id, new_id):
        raise NotImplementedError()

    def changeProperties(self, id, properties):
        raise NotImplementedError()

    def lock(self, id, principal, until):
        raise NotImplementedError()

    def unlock(self, id, locktoken=None):
        raise NotImplementedError()

    def locked(self, id):
        raise NotImplementedError()

    def search(self, attributes, expression):
        raise NotImplementedError()

    # internal helpers

    def _get_cannonical_id(self, id):
        """Add / for collections if not appended yet."""
        if isinstance(id, CannonicalId):
            return id
        if id == ID_NAMESPACE:
            return CannonicalId(id)

        input = id
        result = id
        try:
            return self.canonical_id_cache[input]
        except KeyError:
            pass

        if result.endswith('/'):
            result = result[:-1]
        if self.canonicalize_directories:
            path = self._absolute_path(self._path(result))
            if os.path.isdir(path):
                result = CannonicalId(result + '/')
        else:
            result = CannonicalId(result)
        self.canonical_id_cache[input] = result
        return result

    def _absolute_path(self, path):
        if not path:
            return self.repository_path
        return os.path.join(self.repository_path, os.path.join(*path))

    def _path(self, id):
        if not id.startswith(ID_NAMESPACE):
            raise ValueError("The id %r is invalid." % id)
        id = id.replace(ID_NAMESPACE, '', 1)
        if not id:
            return ()
        result = tuple(id.split('/'))
        if result[-1] == '':
            result = result[:-1]
        return result

    def _get_file(self, id):
        filename = self._absolute_path(self._path(id))
        __traceback_info__ = (id, filename)
        try:
            return file(filename, 'rb')
        except IOError:
            if os.path.isdir(filename):
                raise ValueError(
                    'The path %r points to a directory.' % filename)
            raise KeyError("The resource '%s' does not exist." % id)

    def _get_metadata_file(self, id):
        filename = self._absolute_path(self._path(id)) + '.meta'
        __traceback_info__ = (id, filename)
        try:
            return file(filename, 'rb')
        except IOError:
            return self._get_file(id)

    def _make_id(self, path):
        return urlparse.urljoin(ID_NAMESPACE, '/'.join(
            element for element in path if element))

    def _get_properties(self, id):
        try:
            return self.property_cache[id]
        except KeyError:
            pass
        properties = {}
        if self._set_lastmodified_property:
            modified = self._get_lastmodified(id)
            if modified:
                properties[('getlastmodified', 'DAV:')] = modified

        metadata_parse_error = False
        try:
            data = self._get_metadata_file(id)
            xml = lxml.etree.parse(data)
            data.close()
        except ValueError:
            # Performance optimization: We know this error happens only for
            # directories, so we can determine the resource type here instead
            # of waiting for getResourceType() doing the isdir check _again_.
            properties[zeit.connector.interfaces.RESOURCE_TYPE_PROPERTY] = (
                'collection')
            metadata_parse_error = True
        except lxml.etree.LxmlError:
            metadata_parse_error = True
        if metadata_parse_error:
            self.property_cache[id] = properties
            return properties

        nodes = xml.xpath('//head/attribute')
        for node in nodes:
            properties[node.get('name'), node.get('ns')] = node.text or ''

        # XXX workaround for zeit.frontend, can probably go away
        # once the filesystem connector is finished.
        tags = xml.xpath('//head/rankedTags')
        if tags:
            value = (
                '<tag:rankedTags xmlns:tag="http://namespaces.zeit.de'
                '/CMS/tagging">')
            value += lxml.etree.tostring(tags[0])
            value += '</tag:rankedTags>'
            properties[(
                'rankedTags',
                'http://namespaces.zeit.de/CMS/tagging')] = value

        self.property_cache[id] = properties
        return properties

    def _get_lastmodified(self, id):
        filename = self._absolute_path(self._path(id))
        try:
            mtime = os.stat(filename).st_mtime
        except OSError:
            return None
        return email.utils.formatdate(mtime, usegmt=True)

def connector_factory():
    config = zope.app.appsetup.product.getProductConfiguration(
        'zeit.connector') or {}
    repository_path = config.get('repository-path')
    if not repository_path:
        raise ZConfig.ConfigurationError(
            "Filesystem connector not configured properly.")
    connector = Connector(repository_path)
    canonicalize = config.get('canonicalize-directories', None)
    if canonicalize is not None:
        connector.canonicalize_directories = ast.literal_eval(canonicalize)
    return connector
<commit_msg>Clarify comment<commit_after>from cStringIO import StringIO
from zeit.connector.connector import CannonicalId
from zeit.connector.dav.interfaces import DAVNotFoundError
import ZConfig
import ast
import email.utils
import gocept.cache.property
import logging
import lxml.etree
import os
import os.path
import urlparse
import zeit.connector.dav.interfaces
import zeit.connector.interfaces
import zeit.connector.resource
import zope.app.appsetup.product
import zope.app.file.image
import zope.interface

ID_NAMESPACE = u'http://xml.zeit.de/'

log = logging.getLogger(__name__)

class Connector(object):
    """Connect to the CMS backend in read-only mode.

    The current implementation does *not* talk to the CMS backend but to
    a data store in the filesystem, the path to which needs to be configured.

    """

    zope.interface.implements(zeit.connector.interfaces.IConnector)

    resource_class = zeit.connector.resource.CachedResource
    canonicalize_directories = True

    _set_lastmodified_property = False

    property_cache = gocept.cache.property.TransactionBoundCache(
        '_v_property_cache', dict)
    body_cache = gocept.cache.property.TransactionBoundCache(
        '_v_body_cache', dict)
    child_name_cache = gocept.cache.property.TransactionBoundCache(
        '_v_child_name_cache', dict)
    canonical_id_cache = gocept.cache.property.TransactionBoundCache(
        '_v_canonical_id_cache', dict)

    def __init__(self, repository_path):
        self.repository_path = repository_path

    def listCollection(self, id):
        """List the filenames of a collection identified by path. """
        id = self._get_cannonical_id(id)
        try:
            return self.child_name_cache[id]
        except KeyError:
            pass

        path = self._path(id)
        names = self._get_collection_names(path)
        if not names:
            try:
                self[id]
            except KeyError:
                # XXX mimic the real behaviour -- real *should* probably raise
                # KeyError, but doesn't at the moment.
                raise DAVNotFoundError(404, 'Not Found', id, '')

        result = []
        for name in sorted(names):
            name = unicode(name)
            child_id = unicode(
                self._get_cannonical_id(self._make_id(path + (name, ))))
            result.append((name, child_id))
        self.child_name_cache[id] = result
        return result

    def _get_collection_names(self, path):
        absolute_path = self._absolute_path(path)
        names = (set(os.listdir(absolute_path))
                 if os.path.isdir(absolute_path) else set())
        for x in names.copy():
            if x.endswith('.meta') and x[:-5] in names:
                names.remove(x)
            if x.startswith('.'):
                names.remove(x)
        return names

    def getResourceType(self, id):
        id = self._get_cannonical_id(id)
        __traceback_info__ = id

        properties = self._get_properties(id)
        if properties:
            type = properties.get(
                zeit.connector.interfaces.RESOURCE_TYPE_PROPERTY)
            if type:
                return type

        path = self._absolute_path(self._path(id))
        if os.path.isdir(path):
            return 'collection'

        f = self._get_file(id)
        data = f.read(200)
        f.close()
        content_type, width, height = zope.app.file.image.getImageInfo(data)
        if content_type:
            return 'image'
        return 'unknown'

    def __getitem__(self, id):
        id = self._get_cannonical_id(id)
        properties = self._get_properties(id)
        type = self.getResourceType(id)
        properties[zeit.connector.interfaces.RESOURCE_TYPE_PROPERTY] = type
        path = self._path(id)
        name = path[-1] if path else ''
        return self.resource_class(
            unicode(id), name, type,
            lambda: self._get_properties(id),
            lambda: self._get_body(id),
            content_type=self._get_content_type(id, type))

    def _get_body(self, id):
        try:
            return StringIO(self.body_cache[id])
        except KeyError:
            pass
        try:
            f = self._get_file(id)
            data = f.read()
            f.close()
        except:
            data = ''
        self.body_cache[id] = data
        return StringIO(data)

    def _get_content_type(self, id, type):
        return 'httpd/unix-directory' if type == 'collection' else ''

    def __setitem__(self, id, object):
        raise NotImplementedError()

    def __delitem__(self, id):
        raise NotImplementedError()

    def __contains__(self, id):
        try:
            self[id]
        except KeyError:
            return False
        return True

    def add(self, object, verify_etag=True):
        raise NotImplementedError()

    def copy(self, old_id, new_id):
        raise NotImplementedError()

    def move(self, old_id, new_id):
        raise NotImplementedError()

    def changeProperties(self, id, properties):
        raise NotImplementedError()

    def lock(self, id, principal, until):
        raise NotImplementedError()

    def unlock(self, id, locktoken=None):
        raise NotImplementedError()

    def locked(self, id):
        raise NotImplementedError()

    def search(self, attributes, expression):
        raise NotImplementedError()

    # internal helpers

    def _get_cannonical_id(self, id):
        """Add / for collections if not appended yet."""
        if isinstance(id, CannonicalId):
            return id
        if id == ID_NAMESPACE:
            return CannonicalId(id)

        input = id
        result = id
        try:
            return self.canonical_id_cache[input]
        except KeyError:
            pass

        if result.endswith('/'):
            result = result[:-1]
        if self.canonicalize_directories:
            path = self._absolute_path(self._path(result))
            if os.path.isdir(path):
                result = CannonicalId(result + '/')
        else:
            result = CannonicalId(result)
        self.canonical_id_cache[input] = result
        return result

    def _absolute_path(self, path):
        if not path:
            return self.repository_path
        return os.path.join(self.repository_path, os.path.join(*path))

    def _path(self, id):
        if not id.startswith(ID_NAMESPACE):
            raise ValueError("The id %r is invalid." % id)
        id = id.replace(ID_NAMESPACE, '', 1)
        if not id:
            return ()
        result = tuple(id.split('/'))
        if result[-1] == '':
            result = result[:-1]
        return result

    def _get_file(self, id):
        filename = self._absolute_path(self._path(id))
        __traceback_info__ = (id, filename)
        try:
            return file(filename, 'rb')
        except IOError:
            if os.path.isdir(filename):
                raise ValueError(
                    'The path %r points to a directory.' % filename)
            raise KeyError("The resource '%s' does not exist." % id)

    def _get_metadata_file(self, id):
        filename = self._absolute_path(self._path(id)) + '.meta'
        __traceback_info__ = (id, filename)
        try:
            return file(filename, 'rb')
        except IOError:
            return self._get_file(id)

    def _make_id(self, path):
        return urlparse.urljoin(ID_NAMESPACE, '/'.join(
            element for element in path if element))

    def _get_properties(self, id):
        try:
            return self.property_cache[id]
        except KeyError:
            pass
        properties = {}
        if self._set_lastmodified_property:
            modified = self._get_lastmodified(id)
            if modified:
                properties[('getlastmodified', 'DAV:')] = modified

        metadata_parse_error = False
        try:
            data = self._get_metadata_file(id)
            xml = lxml.etree.parse(data)
            data.close()
        except ValueError:
            # Performance optimization: We know this error happens only for
            # directories, so we can determine the resource type here instead
            # of waiting for getResourceType() doing the isdir check _again_.
            properties[zeit.connector.interfaces.RESOURCE_TYPE_PROPERTY] = (
                'collection')
            metadata_parse_error = True
        except lxml.etree.LxmlError:
            metadata_parse_error = True
        if metadata_parse_error:
            self.property_cache[id] = properties
            return properties

        nodes = xml.xpath('//head/attribute')
        for node in nodes:
            properties[node.get('name'), node.get('ns')] = node.text or ''

        # rankedTags are serialized like all other attributes in .meta files,
        # but in the "fallback to content file" case we need to recognize a
        # different format:
        tags = xml.xpath('//head/rankedTags')
        if tags:
            value = (
                '<tag:rankedTags xmlns:tag="http://namespaces.zeit.de'
                '/CMS/tagging">')
            value += lxml.etree.tostring(tags[0])
            value += '</tag:rankedTags>'
            properties[(
                'rankedTags',
                'http://namespaces.zeit.de/CMS/tagging')] = value

        self.property_cache[id] = properties
        return properties

    def _get_lastmodified(self, id):
        filename = self._absolute_path(self._path(id))
        try:
            mtime = os.stat(filename).st_mtime
        except OSError:
            return None
        return email.utils.formatdate(mtime, usegmt=True)

def connector_factory():
    config = zope.app.appsetup.product.getProductConfiguration(
        'zeit.connector') or {}
    repository_path = config.get('repository-path')
    if not repository_path:
        raise ZConfig.ConfigurationError(
            "Filesystem connector not configured properly.")
    connector = Connector(repository_path)
    canonicalize = config.get('canonicalize-directories', None)
    if canonicalize is not None:
        connector.canonicalize_directories = ast.literal_eval(canonicalize)
    return connector
<|endoftext|>