<commit_before>#
# Copyright (c) 2017 by QA Cafe.
# All Rights Reserved.
#

"""Module for accessing CDRouter Packages."""

import collections

from marshmallow import Schema, fields, post_load
from .cdr_error import CDRouterError
from .testsuites import TestSchema
from .cdr_datetime import DateTime
from .filters import Field as field

class Analysis(object):
    """Model for CDRouter Package Analysis.

    :param total_count: (optional) Total count as an int.
    :param run_count: (optional) Run count as an int.
    :param skipped_count: (optional) Skipped test count as an int.
    :param skipped_tests: (optional) Skipped tests as a string list.
    """
    def __init__(self, **kwargs):
        self.total_count = kwargs.get('total_count', None)
        self.run_count = kwargs.get('run_count', None)
        self.skipped_count = kwargs.get('skipped_count', None)
        self.skipped_tests = kwargs.get('skipped_tests', None)

class AnalysisSchema(Schema):
    total_count = fields.Int()
    run_count = fields.Int()
    skipped_count = fields.Int()
    skipped_tests = fields.Nested(TestSchema, many=True)

    @post_load
    def post_load(self, data):
        return Analysis(**data)

class Options(object):
    """Model for CDRouter Package Options.

    :param forever: (optional) Bool `True` if package looped forver.
    :param loop: (optional) Loop count as an int.
    :param repeat: (optional) Repeat count as an int.
    :param maxfail: (optional) Max fail count as an int.
    :param duration: (optional) Max testing time duration as an int.
    :param wait: (optional) Wait between tests duration as an int.
    :param pause: (optional) Bool `True` is pausing between tests.
    :param shuffle: (optional) Bool `True` if testlist is shuffled.
    :param seed: (optional) Shuffle seed as an int.
    :param retry: (optional) Retry count as an int.
    :param rdelay: (optional) Retry delay as an int.
    """
    def __init__(self, **kwargs):
        self.forever = kwargs.get('forever', None)
        self.loop = kwargs.get('loop', None)
        self.repeat = kwargs.get('repeat', None)
        self.maxfail = kwargs.get('maxfail', None)
        self.duration = kwargs.get('duration', None)
        self.wait = kwargs.get('wait', None)
        self.pause = kwargs.get('pause', None)
        self.shuffle = kwargs.get('shuffle', None)
        self.seed = kwargs.get('seed', None)
        self.retry = kwargs.get('retry', None)
        self.rdelay = kwargs.get('rdelay', None)

class OptionsSchema(Schema):
    forever = fields.Bool()
    loop = fields.Int(as_string=True)
    repeat = fields.Int(as_string=True)
    maxfail = fields.Int(as_string=True)
    duration = fields.Int(as_string=True)
    wait = fields.Int(as_string=True)
    pause = fields.Bool()
    shuffle = fields.Bool()
    seed = fields.Int(as_string=True)
    retry = fields.Int(as_string=True)
    rdelay = fields.Int(as_string=True)

    @post_load
    def post_load(self, data):
        return Options(**data)

class Schedule(object):
    """Model for CDRouter Package Schedule.

    :param enabled: (optional) Bool `True` if schedule is enabled.
    :param spec: (optional) Cron spec schedule as a string.
    """
    def __init__(self, **kwargs):
        self.enabled = kwargs.get('enabled', None)
        self.spec = kwargs.get('spec', None)

class ScheduleSchema(Schema):
    enabled = fields.Bool()
    spec = fields.Str()

    @post_load
    def post_load(self, data):
        return Schedule(**data)

class Package(object):
    """Model for CDRouter Packages.

    :param id: (optional) Package ID as an int.
    :param name: (optional) Name as a string.
    :param description: (optional) Description as a string.
    :param created: (optional) Creation time as `DateTime`.
    :param updated: (optional) Last-updated time as `DateTime`.
    :param test_count: (optional) Test count as an int.
    :param testlist: (optional) Testlist as a string list.
    :param extra_cli_args: (optional) Extra CLI args as a string.
    :param user_id: (optional) User ID as an int.
    :param agent_id: (optional) Agent ID as an int.
    :param config_id: (optional) Config ID as an int.
    :param result_id: (optional) Result ID as an int (if a package snapshot).
    :param device_id: (optional) Device ID as an int.
    :param options: (optional) :class:`packages.Options <packages.Options>` object
    :param tags: (optional) Tags as a string list.
    :param use_as_testlist: (optional) Bool `True` if package is used as a testlist.
    :param note: (optional) Note as a string.
    """
    def __init__(self, **kwargs):
        self.id = kwargs.get('id', None)
        self.name = kwargs.get('name', None)
        self.description = kwargs.get('description', None)
        self.created = kwargs.get('created', None)
        self.updated = kwargs.get('updated', None)
        self.test_count = kwargs.get('test_count', None)
        self.testlist = kwargs.get('testlist', None)
        self.extra_cli_args = kwargs.get('extra_cli_args', None)
        self.user_id = kwargs.get('user_id', None)
        self.agent_id = kwargs.get('agent_id', None)
        self.config_id = kwargs.get('config_id', None)
        self.result_id = kwargs.get('result_id', None)
        self.device_id = kwargs.get('device_id', None)
        self.options = kwargs.get('options', None)
        self.tags = kwargs.get('tags', None)
        self.use_as_testlist = kwargs.get('use_as_testlist', None)
        self.note = kwargs.get('note', None)
        self.schedule = kwargs.get('schedule', None)

class PackageSchema(Schema):
    id = fields.Int(as_string=True)
    name = fields.Str()
    description = fields.Str()
    created = DateTime()
    updated = DateTime()
    test_count = fields.Int(as_string=True)
    testlist = fields.List(fields.Str())
    extra_cli_args = fields.Str()
    user_id = fields.Int(as_string=True)
    agent_id = fields.Int(as_string=True)
    config_id = fields.Int(as_string=True)
    result_id = fields.Int(as_string=True, missing=None)
    device_id = fields.Int(as_string=True)
    options = fields.Nested(OptionsSchema)
    tags = fields.List(fields.Str())
    use_as_testlist = fields.Bool()
    note = fields.Str(missing=None)
    schedule = fields.Nested(ScheduleSchema)

    @post_load
    def post_load(self, data):
        return Package(**data)

class Page(collections.namedtuple('Page', ['data', 'links'])):
    """Named tuple for a page of list response data.

    :param data: :class:`packages.Package <packages.Package>` list
    :param links: :class:`cdrouter.Links <cdrouter.Links>` object
    """

class PackagesService(object):
    """Service for accessing CDRouter Packages."""

    RESOURCE = 'packages'
    BASE = RESOURCE + '/'

    def __init__(self, service):
        self.service = service
        self.base = self.BASE

    def list(self, filter=None, type=None, sort=None, limit=None, page=None): # pylint: disable=redefined-builtin
        """Get a list of packages.

        :param filter: (optional) Filters to apply as a string list.
        :param type: (optional) `union` or `inter` as string.
        :param sort: (optional) Sort fields to apply as string list.
        :param limit: (optional) Limit returned list length.
        :param page: (optional) Page to return.
        :return: :class:`packages.Page <packages.Page>` object
        """
        schema = PackageSchema(exclude=('testlist', 'extra_cli_args', 'agent_id', 'options', 'note'))
        resp = self.service.list(self.base, filter, type, sort, limit, page)
        ps, l = self.service.decode(schema, resp, many=True, links=True)
        return Page(ps, l)

    def iter_list(self, *args, **kwargs):
        """Get a list of packages.  Whereas ``list`` fetches a single page of
        packages according to its ``limit`` and ``page`` arguments,
        ``iter_list`` returns all packages by internally making
        successive calls to ``list``.

        :param args: Arguments that ``list`` takes.
        :param kwargs: Optional arguments that ``list`` takes.
        :return: :class:`packages.Package <packages.Package>` list

        """
        return self.service.iter_list(self.list, *args, **kwargs)

    def get(self, id): # pylint: disable=invalid-name,redefined-builtin
        """Get a package.

        :param id: Package ID as an int.
        :return: :class:`packages.Package <packages.Package>` object
        :rtype: packages.Package
        """
        schema = PackageSchema()
        resp = self.service.get_id(self.base, id)
        return self.service.decode(schema, resp)

    def get_by_name(self, name): # pylint: disable=invalid-name,redefined-builtin
        """Get a package by name.

        :param name: Package name as string.
        :return: :class:`packages.Package <packages.Package>` object
        :rtype: packages.Package
        """
        rs, _ = self.list(filter=field('name').eq(name), limit=1)
        if len(rs) is 0:
            raise CDRouterError('no such package')
        return rs[0]

    def create(self, resource):
        """Create a new package.

        :param resource: :class:`packages.Package <packages.Package>` object
        :return: :class:`packages.Package <packages.Package>` object
        :rtype: packages.Package
        """
        schema = PackageSchema(exclude=('id', 'created', 'updated', 'test_count', 'agent_id', 'result_id'))
        json = self.service.encode(schema, resource)

        schema = PackageSchema()
        resp = self.service.create(self.base, json)
        return self.service.decode(schema, resp)

    def edit(self, resource):
        """Edit a package.

        :param resource: :class:`packages.Package <packages.Package>` object
        :return: :class:`packages.Package <packages.Package>` object
        :rtype: packages.Package
        """
        schema = PackageSchema(exclude=('id', 'created', 'updated', 'test_count', 'agent_id', 'result_id'))
        json = self.service.encode(schema, resource)

        schema = PackageSchema()
        resp = self.service.edit(self.base, resource.id, json)
        return self.service.decode(schema, resp)

    def delete(self, id): # pylint: disable=invalid-name,redefined-builtin
        """Delete a package.

        :param id: Package ID as an int.
        """
        return self.service.delete_id(self.base, id)

    def get_shares(self, id): # pylint: disable=invalid-name,redefined-builtin
        """Get shares for a package.

        :param id: Package ID as an int.
        :return: :class:`cdrouter.Share <cdrouter.Share>` list
        """
        return self.service.get_shares(self.base, id)

    def edit_shares(self, id, user_ids): # pylint: disable=invalid-name,redefined-builtin
        """Edit shares for a package.

        :param id: Package ID as an int.
        :param user_ids: User IDs as int list.
        :return: :class:`cdrouter.Share <cdrouter.Share>` list
        """
        return self.service.edit_shares(self.base, id, user_ids)

    def export(self, id): # pylint: disable=invalid-name,redefined-builtin
        """Export a package.

        :param id: Package ID as an int.
        :rtype: tuple `(io.BytesIO, 'filename')`
        """
        return self.service.export(self.base, id)

    def analyze(self, id): # pylint: disable=invalid-name,redefined-builtin
        """Get a list of tests that will be skipped for a package.

        :param id: Package ID as an int.
        :return: :class:`packages.Analysis <packages.Analysis>` object
        :rtype: packages.Analysis
        """
        schema = AnalysisSchema()
        resp = self.service.post(self.base+str(id)+'/', params={'process': 'analyze'})
        return self.service.decode(schema, resp)

    def bulk_export(self, ids):
        """Bulk export a set of packages.

        :param ids: Int list of package IDs.
        :rtype: tuple `(io.BytesIO, 'filename')`
        """
        return self.service.bulk_export(self.base, ids)

    def bulk_copy(self, ids):
        """Bulk copy a set of packages.

        :param ids: Int list of package IDs.
        :return: :class:`packages.Package <packages.Package>` list
        """
        schema = PackageSchema()
        return self.service.bulk_copy(self.base, self.RESOURCE, ids, schema)

    def bulk_edit(self, _fields, ids=None, filter=None, type=None, all=False): # pylint: disable=redefined-builtin
        """Bulk edit a set of packages.

        :param _fields: :class:`packages.Package <packages.Package>` object
        :param ids: (optional) Int list of package IDs.
        :param filter: (optional) String list of filters.
        :param type: (optional) `union` or `inter` as string.
        :param all: (optional) Apply to all if bool `True`.
        """
        schema = PackageSchema(exclude=('id', 'created', 'updated', 'test_count', 'agent_id', 'result_id'))
        _fields = self.service.encode(schema, _fields, skip_none=True)
        return self.service.bulk_edit(self.base, self.RESOURCE,
                                      _fields, ids=ids, filter=filter, type=type, all=all)

    def bulk_delete(self, ids=None, filter=None, type=None, all=False): # pylint: disable=redefined-builtin
        """Bulk delete a set of packages.

        :param ids: (optional) Int list of package IDs.
        :param filter: (optional) String list of filters.
        :param type: (optional) `union` or `inter` as string.
        :param all: (optional) Apply to all if bool `True`.
        """
        return self.service.bulk_delete(self.base, self.RESOURCE, ids=ids, filter=filter, type=type, all=all)
<commit_msg>Add missing schedule field to Package docstring<commit_after>#
# Copyright (c) 2017 by QA Cafe.
# All Rights Reserved.
#

"""Module for accessing CDRouter Packages."""

import collections

from marshmallow import Schema, fields, post_load
from .cdr_error import CDRouterError
from .testsuites import TestSchema
from .cdr_datetime import DateTime
from .filters import Field as field

class Analysis(object):
    """Model for CDRouter Package Analysis.

    :param total_count: (optional) Total count as an int.
    :param run_count: (optional) Run count as an int.
    :param skipped_count: (optional) Skipped test count as an int.
    :param skipped_tests: (optional) Skipped tests as a string list.
    """
    def __init__(self, **kwargs):
        self.total_count = kwargs.get('total_count', None)
        self.run_count = kwargs.get('run_count', None)
        self.skipped_count = kwargs.get('skipped_count', None)
        self.skipped_tests = kwargs.get('skipped_tests', None)

class AnalysisSchema(Schema):
    total_count = fields.Int()
    run_count = fields.Int()
    skipped_count = fields.Int()
    skipped_tests = fields.Nested(TestSchema, many=True)

    @post_load
    def post_load(self, data):
        return Analysis(**data)

class Options(object):
    """Model for CDRouter Package Options.

    :param forever: (optional) Bool `True` if package looped forver.
    :param loop: (optional) Loop count as an int.
    :param repeat: (optional) Repeat count as an int.
    :param maxfail: (optional) Max fail count as an int.
    :param duration: (optional) Max testing time duration as an int.
    :param wait: (optional) Wait between tests duration as an int.
    :param pause: (optional) Bool `True` is pausing between tests.
    :param shuffle: (optional) Bool `True` if testlist is shuffled.
    :param seed: (optional) Shuffle seed as an int.
    :param retry: (optional) Retry count as an int.
    :param rdelay: (optional) Retry delay as an int.
    """
    def __init__(self, **kwargs):
        self.forever = kwargs.get('forever', None)
        self.loop = kwargs.get('loop', None)
        self.repeat = kwargs.get('repeat', None)
        self.maxfail = kwargs.get('maxfail', None)
        self.duration = kwargs.get('duration', None)
        self.wait = kwargs.get('wait', None)
        self.pause = kwargs.get('pause', None)
        self.shuffle = kwargs.get('shuffle', None)
        self.seed = kwargs.get('seed', None)
        self.retry = kwargs.get('retry', None)
        self.rdelay = kwargs.get('rdelay', None)

class OptionsSchema(Schema):
    forever = fields.Bool()
    loop = fields.Int(as_string=True)
    repeat = fields.Int(as_string=True)
    maxfail = fields.Int(as_string=True)
    duration = fields.Int(as_string=True)
    wait = fields.Int(as_string=True)
    pause = fields.Bool()
    shuffle = fields.Bool()
    seed = fields.Int(as_string=True)
    retry = fields.Int(as_string=True)
    rdelay = fields.Int(as_string=True)

    @post_load
    def post_load(self, data):
        return Options(**data)

class Schedule(object):
    """Model for CDRouter Package Schedule.

    :param enabled: (optional) Bool `True` if schedule is enabled.
    :param spec: (optional) Cron spec schedule as a string.
    """
    def __init__(self, **kwargs):
        self.enabled = kwargs.get('enabled', None)
        self.spec = kwargs.get('spec', None)

class ScheduleSchema(Schema):
    enabled = fields.Bool()
    spec = fields.Str()

    @post_load
    def post_load(self, data):
        return Schedule(**data)

class Package(object):
    """Model for CDRouter Packages.

    :param id: (optional) Package ID as an int.
    :param name: (optional) Name as a string.
    :param description: (optional) Description as a string.
    :param created: (optional) Creation time as `DateTime`.
    :param updated: (optional) Last-updated time as `DateTime`.
    :param test_count: (optional) Test count as an int.
    :param testlist: (optional) Testlist as a string list.
    :param extra_cli_args: (optional) Extra CLI args as a string.
    :param user_id: (optional) User ID as an int.
    :param agent_id: (optional) Agent ID as an int.
    :param config_id: (optional) Config ID as an int.
    :param result_id: (optional) Result ID as an int (if a package snapshot).
    :param device_id: (optional) Device ID as an int.
    :param options: (optional) :class:`packages.Options <packages.Options>` object
    :param tags: (optional) Tags as a string list.
    :param use_as_testlist: (optional) Bool `True` if package is used as a testlist.
    :param note: (optional) Note as a string.
    :param schedule: (optional) :class:`packages.Schedule <packages.Schedule>` object
    """
    def __init__(self, **kwargs):
        self.id = kwargs.get('id', None)
        self.name = kwargs.get('name', None)
        self.description = kwargs.get('description', None)
        self.created = kwargs.get('created', None)
        self.updated = kwargs.get('updated', None)
        self.test_count = kwargs.get('test_count', None)
        self.testlist = kwargs.get('testlist', None)
        self.extra_cli_args = kwargs.get('extra_cli_args', None)
        self.user_id = kwargs.get('user_id', None)
        self.agent_id = kwargs.get('agent_id', None)
        self.config_id = kwargs.get('config_id', None)
        self.result_id = kwargs.get('result_id', None)
        self.device_id = kwargs.get('device_id', None)
        self.options = kwargs.get('options', None)
        self.tags = kwargs.get('tags', None)
        self.use_as_testlist = kwargs.get('use_as_testlist', None)
        self.note = kwargs.get('note', None)
        self.schedule = kwargs.get('schedule', None)

class PackageSchema(Schema):
    id = fields.Int(as_string=True)
    name = fields.Str()
    description = fields.Str()
    created = DateTime()
    updated = DateTime()
    test_count = fields.Int(as_string=True)
    testlist = fields.List(fields.Str())
    extra_cli_args = fields.Str()
    user_id = fields.Int(as_string=True)
    agent_id = fields.Int(as_string=True)
    config_id = fields.Int(as_string=True)
    result_id = fields.Int(as_string=True, missing=None)
    device_id = fields.Int(as_string=True)
    options = fields.Nested(OptionsSchema)
    tags = fields.List(fields.Str())
    use_as_testlist = fields.Bool()
    note = fields.Str(missing=None)
    schedule = fields.Nested(ScheduleSchema)

    @post_load
    def post_load(self, data):
        return Package(**data)

class Page(collections.namedtuple('Page', ['data', 'links'])):
    """Named tuple for a page of list response data.

    :param data: :class:`packages.Package <packages.Package>` list
    :param links: :class:`cdrouter.Links <cdrouter.Links>` object
    """

class PackagesService(object):
    """Service for accessing CDRouter Packages."""

    RESOURCE = 'packages'
    BASE = RESOURCE + '/'

    def __init__(self, service):
        self.service = service
        self.base = self.BASE

    def list(self, filter=None, type=None, sort=None, limit=None, page=None): # pylint: disable=redefined-builtin
        """Get a list of packages.

        :param filter: (optional) Filters to apply as a string list.
        :param type: (optional) `union` or `inter` as string.
        :param sort: (optional) Sort fields to apply as string list.
        :param limit: (optional) Limit returned list length.
        :param page: (optional) Page to return.
        :return: :class:`packages.Page <packages.Page>` object
        """
        schema = PackageSchema(exclude=('testlist', 'extra_cli_args', 'agent_id', 'options', 'note'))
        resp = self.service.list(self.base, filter, type, sort, limit, page)
        ps, l = self.service.decode(schema, resp, many=True, links=True)
        return Page(ps, l)

    def iter_list(self, *args, **kwargs):
        """Get a list of packages.  Whereas ``list`` fetches a single page of
        packages according to its ``limit`` and ``page`` arguments,
        ``iter_list`` returns all packages by internally making
        successive calls to ``list``.

        :param args: Arguments that ``list`` takes.
        :param kwargs: Optional arguments that ``list`` takes.
        :return: :class:`packages.Package <packages.Package>` list

        """
        return self.service.iter_list(self.list, *args, **kwargs)

    def get(self, id): # pylint: disable=invalid-name,redefined-builtin
        """Get a package.

        :param id: Package ID as an int.
        :return: :class:`packages.Package <packages.Package>` object
        :rtype: packages.Package
        """
        schema = PackageSchema()
        resp = self.service.get_id(self.base, id)
        return self.service.decode(schema, resp)

    def get_by_name(self, name): # pylint: disable=invalid-name,redefined-builtin
        """Get a package by name.

        :param name: Package name as string.
        :return: :class:`packages.Package <packages.Package>` object
        :rtype: packages.Package
        """
        rs, _ = self.list(filter=field('name').eq(name), limit=1)
        if len(rs) is 0:
            raise CDRouterError('no such package')
        return rs[0]

    def create(self, resource):
        """Create a new package.

        :param resource: :class:`packages.Package <packages.Package>` object
        :return: :class:`packages.Package <packages.Package>` object
        :rtype: packages.Package
        """
        schema = PackageSchema(exclude=('id', 'created', 'updated', 'test_count', 'agent_id', 'result_id'))
        json = self.service.encode(schema, resource)

        schema = PackageSchema()
        resp = self.service.create(self.base, json)
        return self.service.decode(schema, resp)

    def edit(self, resource):
        """Edit a package.

        :param resource: :class:`packages.Package <packages.Package>` object
        :return: :class:`packages.Package <packages.Package>` object
        :rtype: packages.Package
        """
        schema = PackageSchema(exclude=('id', 'created', 'updated', 'test_count', 'agent_id', 'result_id'))
        json = self.service.encode(schema, resource)

        schema = PackageSchema()
        resp = self.service.edit(self.base, resource.id, json)
        return self.service.decode(schema, resp)

    def delete(self, id): # pylint: disable=invalid-name,redefined-builtin
        """Delete a package.

        :param id: Package ID as an int.
        """
        return self.service.delete_id(self.base, id)

    def get_shares(self, id): # pylint: disable=invalid-name,redefined-builtin
        """Get shares for a package.

        :param id: Package ID as an int.
        :return: :class:`cdrouter.Share <cdrouter.Share>` list
        """
        return self.service.get_shares(self.base, id)

    def edit_shares(self, id, user_ids): # pylint: disable=invalid-name,redefined-builtin
        """Edit shares for a package.

        :param id: Package ID as an int.
        :param user_ids: User IDs as int list.
        :return: :class:`cdrouter.Share <cdrouter.Share>` list
        """
        return self.service.edit_shares(self.base, id, user_ids)

    def export(self, id): # pylint: disable=invalid-name,redefined-builtin
        """Export a package.

        :param id: Package ID as an int.
        :rtype: tuple `(io.BytesIO, 'filename')`
        """
        return self.service.export(self.base, id)

    def analyze(self, id): # pylint: disable=invalid-name,redefined-builtin
        """Get a list of tests that will be skipped for a package.

        :param id: Package ID as an int.
        :return: :class:`packages.Analysis <packages.Analysis>` object
        :rtype: packages.Analysis
        """
        schema = AnalysisSchema()
        resp = self.service.post(self.base+str(id)+'/', params={'process': 'analyze'})
        return self.service.decode(schema, resp)

    def bulk_export(self, ids):
        """Bulk export a set of packages.

        :param ids: Int list of package IDs.
        :rtype: tuple `(io.BytesIO, 'filename')`
        """
        return self.service.bulk_export(self.base, ids)

    def bulk_copy(self, ids):
        """Bulk copy a set of packages.

        :param ids: Int list of package IDs.
        :return: :class:`packages.Package <packages.Package>` list
        """
        schema = PackageSchema()
        return self.service.bulk_copy(self.base, self.RESOURCE, ids, schema)

    def bulk_edit(self, _fields, ids=None, filter=None, type=None, all=False): # pylint: disable=redefined-builtin
        """Bulk edit a set of packages.

        :param _fields: :class:`packages.Package <packages.Package>` object
        :param ids: (optional) Int list of package IDs.
        :param filter: (optional) String list of filters.
        :param type: (optional) `union` or `inter` as string.
        :param all: (optional) Apply to all if bool `True`.
        """
        schema = PackageSchema(exclude=('id', 'created', 'updated', 'test_count', 'agent_id', 'result_id'))
        _fields = self.service.encode(schema, _fields, skip_none=True)
        return self.service.bulk_edit(self.base, self.RESOURCE,
                                      _fields, ids=ids, filter=filter, type=type, all=all)

    def bulk_delete(self, ids=None, filter=None, type=None, all=False): # pylint: disable=redefined-builtin
        """Bulk delete a set of packages.

        :param ids: (optional) Int list of package IDs.
        :param filter: (optional) String list of filters.
        :param type: (optional) `union` or `inter` as string.
        :param all: (optional) Apply to all if bool `True`.
        """
        return self.service.bulk_delete(self.base, self.RESOURCE, ids=ids, filter=filter, type=type, all=all)
<|endoftext|>


<commit_before>"""a work in progress"""
import datetime
from glob import glob
from json import dumps
from subprocess import call
import csv

from django.core.management.base import BaseCommand, CommandError
from paying_for_college.models import *
# from paying_for_college.disclosures.scripts import ipeds, prep_ipeds_csv
from django.conf import settings

DUMP = ['python',
        'manage.py',
        'dumpdata',
        '--indent',
        '4',
        'paying_for_college',
        '>',
        'paying_for_college/fixtures/colleges.json']
# DATA_ROOT = "%s/data/ipeds" % settings.PROJECT_ROOT

class Command(BaseCommand):
    args = '<path>'
    help = 'Load the specified CSV into the schools database'

    def handle(self, *args, **options):
        print('dumping backup fixture before starting ...')
        call(DUMP)
        # print('dump finished; now downloading IPEDS data')
        # ipeds.download_tuition(DATA_ROOT)
        # print('download finished; now prepping csv')
        # csvfile = prep_ipeds_csv()
        # print("prepped {0}".format(csvfile))
        starter = datetime.datetime.now()
        count = 0
        newcount = 0
        newaliascount = 0
        for path_spec in args:
            for path in glob(path_spec):
                print "loading %s" % path
                with open(path) as csv_file:
                    reader = csv.DictReader(csv_file)
                    for record in reader:
                        count += 1
                        if count % 250 == 0:
                            print count  # progress feedback
                        primary_name = record.get("SCHOOL").strip()
                        school_id = int(record['SCHOOL_ID'])
                        city = record.get('CITY')
                        state = record.get('STATE')
                        school, cr = School.objects.get_or_create(pk=school_id)
                        if created:
                            newcount += 1
                        aliases = set()
                        alias_pack = record.get('ALIAS')  # pipe delimited
                        if alias_pack:
                            aliases = aliases.union([ali.strip()
                                                     for ali in
                                                     alias_pack.split('|')])
                        old_aliases = school.alias_set.all()
                        for new_alias in aliases:
                            Alias.objects.get_or_create(alias=new_alias,
                                                        institution=school)
                            if _[1]:
                                newaliascount += 1
                                print 'created new alias %s' % new_alias
                        old_primary = old_aliases.get(is_primary=True)
                        if old_primary.alias != primary_name:
                            print 'new name for %s -- %s?' % (school,
                                                              primary_name)
                        data_json = dumps(record)
                        school.data_json = data_json
                        school.save()
        print "created %s schools and updated %s" % (newcount,
                                                     (count - newcount))
        print "script run time: %s" % (datetime.datetime.now() - starter)
<commit_msg>fix redirection to output<commit_after>"""a work in progress"""
import datetime
from glob import glob
from json import dumps
from subprocess import call
import csv

from django.core.management.base import BaseCommand, CommandError
from paying_for_college.models import *
# from paying_for_college.disclosures.scripts import ipeds, prep_ipeds_csv
from django.conf import settings

DUMP = ['python',
        'manage.py',
        'dumpdata',
        '--indent',
        '4',
        'paying_for_college']

class Command(BaseCommand):
    args = '<path>'
    help = 'Load the specified CSV into the schools database'

    def handle(self, *args, **options):
        print('dumping backup fixture before starting ...')
        with open('paying_for_college/fixtures/colleges_backup.json', 'w') as f:
            call(DUMP, stdout=f)
        # print('dump finished; now downloading IPEDS data')
        # ipeds.download_tuition(DATA_ROOT)
        # print('download finished; now prepping csv')
        # csvfile = prep_ipeds_csv()
        # print("prepped {0}".format(csvfile))
        starter = datetime.datetime.now()
        count = 0
        newcount = 0
        newaliascount = 0
        for path_spec in args:
            for path in glob(path_spec):
                print "loading %s" % path
                with open(path) as csv_file:
                    reader = csv.DictReader(csv_file)
                    for record in reader:
                        count += 1
                        if count % 250 == 0:
                            print count  # progress feedback
                        primary_name = record.get("SCHOOL").strip()
                        school_id = int(record['SCHOOL_ID'])
                        city = record.get('CITY')
                        state = record.get('STATE')
                        school, cr = School.objects.get_or_create(pk=school_id)
                        if created:
                            newcount += 1
                        aliases = set()
                        alias_pack = record.get('ALIAS')  # pipe delimited
                        if alias_pack:
                            aliases = aliases.union([ali.strip()
                                                     for ali in
                                                     alias_pack.split('|')])
                        old_aliases = school.alias_set.all()
                        for new_alias in aliases:
                            Alias.objects.get_or_create(alias=new_alias,
                                                        institution=school)
                            if _[1]:
                                newaliascount += 1
                                print 'created new alias %s' % new_alias
                        old_primary = old_aliases.get(is_primary=True)
                        if old_primary.alias != primary_name:
                            print 'new name for %s -- %s?' % (school,
                                                              primary_name)
                        data_json = dumps(record)
                        school.data_json = data_json
                        school.save()
        print "created %s schools and updated %s" % (newcount,
                                                     (count - newcount))
        print "script run time: %s" % (datetime.datetime.now() - starter)
<|endoftext|>


<commit_before>from contextlib import contextmanager
from os.path import join, exists, dirname, relpath, isdir, realpath
from os import environ, makedirs
from tempfile import mkdtemp
from time import sleep, time
import fcntl, errno

from git import Repo, Actor, GitCommandError
from slugify import slugify

from ..repo_functions import MergeConflict
from ..constants import WORKING_STATE_PUBLISHED, WORKING_STATE_DELETED, USERTASK_DIRECTORY_PATTERN

def _calculate_dirname(actor, origin):
    ''' Prepare a consistent directory for this user and this repository.
    '''
    first_commit = list(origin.iter_commits())[-1].hexsha
    return USERTASK_DIRECTORY_PATTERN.format(sha=first_commit[:8], email=slugify(actor.email))

@contextmanager
def get_usertask(*args, **kwargs):
    task = UserTask(*args, **kwargs)
    yield task
    task.cleanup()

class UserTaskPublished(RuntimeError): pass
class UserTaskDeleted(RuntimeError): pass
class UserTaskUnpushable(RuntimeError): pass

class UserTask():
    actor = None
    commit_sha = None
    _writeable = True
    _committed = False
    _pushed = False

    def __init__(self, actor, task_id, origin_dirname, working_dirname, start_point=None):
        '''

            start_point: task ID or commit SHA.
        '''
        self.actor = actor
        self.task_id = task_id

        # Prepare a clone directory.
        origin = Repo(origin_dirname)
        user_dirname = _calculate_dirname(self.actor, origin)
        clone_dirname = join(realpath(working_dirname), user_dirname)

        self._lock(clone_dirname)

        if isdir(clone_dirname):
            self.repo = Repo(clone_dirname)
            self.repo.git.checkout('zelig')
            self.repo.git.reset('origin/master', hard=True)
        else:
            # Clone origin to local checkout.
            self.repo = origin.clone(clone_dirname)
            self.repo.git.checkout('zelig', orphan=True)
            self.repo.git.reset('origin/master', hard=True)

        # Fetch all branches from origin.
        self.repo.git.fetch('origin')

        # Determine if a start-point was passed or needs to be inferred.
        if start_point is None:
            start_point = task_id
            self._writeable = False

        # Figure out what start_point is.
        if 'origin/{}'.format(start_point) in self.repo.refs:
            branch = self.repo.refs['origin/{}'.format(start_point)]
            self.commit_sha = branch.commit.hexsha
        else:
            self.commit_sha = start_point

        # Point local zelig to start_point.
        self.repo.git.reset(self.commit_sha, hard=True)

    def __repr__(self):
        return '<UserTask {} in {}>'.format(self.actor.email, self.repo.working_dir)

    def _lock(self, clone_dirname):
        self._lockfile = open('{}.lock'.format(clone_dirname), 'a')
        timeout = time() + 1.

        while True:
            try:
                fcntl.flock(self._lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
                # Lock acquired!
                return
            except IOError, ex:
                if ex.errno != errno.EAGAIN:  # Resource temporarily unavailable
                    raise
                elif time() > timeout:
                    # Exceeded the timeout.
                    raise

            sleep(.01)

    def _unlock(self):
        fcntl.flock(self._lockfile, fcntl.LOCK_UN)

    def _open(self, path, *args, **kwargs):
        return open(join(self.repo.working_dir, path), *args, **kwargs)

    def read(self, filename):
        with self._open(filename, 'r') as file:
            return file.read()

    def write(self, filename, content):
        assert self._writeable and not (self._committed or self._pushed)

        with self._open(filename, 'w') as file:
            file.write(content)
        self.repo.git.add(filename)

    def move(self, old_path, new_path):
        assert self._writeable and not (self._committed or self._pushed)

        #
        # Make sure we're not trying to move a directory inside itself.
        # This behavior is pretty old, and it's unclear if we want to
        # keep it but for now we just wants the existings test to pass.
        #
        old_dirname, new_dirname = dirname(old_path), dirname(new_path)

        if old_dirname:
            if not relpath(new_dirname, old_dirname).startswith('..'):
                raise ValueError(u'I cannot move a directory inside itself!', u'warning')

        dir_path = join(self.repo.working_dir, dirname(new_path))

        if not exists(dir_path):
            makedirs(dir_path)

        self.repo.git.mv(old_path, new_path)

    def commit(self, message):
        assert self._writeable and not (self._committed or self._pushed)
        self._committed = True

        # Commit to local zelig, push to origin task ID.
        self._set_author_env()
        dirty = self.repo.is_dirty()
        if dirty:
            self.repo.git.commit(m=message, a=True)
        return dirty

    def is_pushable(self):
        ''' Return pushable status: True, False, or a working state constant.
        '''
        if not self._writeable:
            return False

        if self._pushed:
            return False

        if not self._committed:
            return False

        if self.task_id in self.repo.tags:
            return WORKING_STATE_PUBLISHED

        if 'origin/{}'.format(self.task_id) not in self.repo.refs:
            return WORKING_STATE_DELETED

        return True

    def push(self):
        assert self._committed and not self._pushed
        pushable = self.is_pushable()

        if pushable is True:
            self._pushed = True
        if pushable is WORKING_STATE_PUBLISHED:
            raise UserTaskPublished()
        elif pushable is WORKING_STATE_DELETED:
            raise UserTaskDeleted()
        elif pushable is False:
            raise UserTaskUnpushable()

        # See if we are behind the origin branch, for example because we are
        # using the back button for editing, and starting from an older commit.
        task_sha = self._get_task_sha()

        # Rebase if necessary.
        if task_sha != self.commit_sha:
            self._rebase_with_author_check(task_sha)

        try:
            # Push to origin; we think this is safe to do.
            self.repo.git.push('origin', 'zelig:{}'.format(self.task_id))

        except GitCommandError:
            # Push failed, possibly because origin has
            # been modified due to rapid concurrent editing.
            self.repo.git.fetch('origin')
            self._rebase_with_author_check(self._get_task_sha())

        # Re-point self.commit_sha to the new one.
        self.commit_sha = self._get_task_sha()

    def ref_info(self, ref=None):
        ''' Return dict with author email and a relative date for a given reference.
        '''
        if ref is None:
            ref = self.commit_sha

        elif ref in self.repo.tags:
            # De-reference the tag. Sometimes showing a tag shows more than
            # just the commit, prefixing output with "tag <tag name>" if
            # there are notes attached.
            ref = self.repo.tags[ref].commit.hexsha

        raw = self.repo.git.show('--format=%ae %ar', ref)
        email, date = raw.split('\n')[0].split(' ', 1)

        return dict(published_date=date, published_by=email)

    def _set_author_env(self):
        '''
        '''
        environ['GIT_AUTHOR_NAME'] = self.actor.name
        environ['GIT_COMMITTER_NAME'] = self.actor.name
        environ['GIT_AUTHOR_EMAIL'] = self.actor.email
        environ['GIT_COMMITTER_EMAIL'] = self.actor.email

    def _is_interloped(self, ending_sha):
        '''
        '''
        rev_list = '{}..{}'.format(self.commit_sha, ending_sha)
        actors = [commit.author for commit in self.repo.iter_commits(rev_list)]

        for commit in self.repo.iter_commits(rev_list):
            if commit.author != self.actor:
                return True

        return False

    def _get_task_sha(self):
        ''' Get local commit SHA for a given task ID.
        '''
        branch = self.repo.refs['origin/{}'.format(self.task_id)]
        return branch.commit.hexsha

    def _rebase_with_author_check(self, task_sha):
        ''' Rebase onto given SHA, checking for interloping authors.

            If no interlopers exist, use an aggressive merge strategy
            to clobber possible conflicts. If any interloper exists,
            use a more timid strategy and possibly raise a MergeConflict.
        '''
        if self._is_interloped(task_sha):
            try:
                # Do a timid rebase since someone else has been here.
                local_commit = self.repo.commit()
                self.repo.git.rebase(task_sha)
            except GitCommandError:
                # Raise a MergeConflict.
                remote_commit = self.repo.commit(task_sha)
                raise MergeConflict(remote_commit, local_commit)
        else:
            # Do an aggressive rebase since no one else has been here.
            self.repo.git.rebase(task_sha, X='theirs')

    def cleanup(self):
        # once we have locking, we will unlock here
        self._unlock()

    def __del__(self):
        self.cleanup()
<commit_msg>point commit_sha to the publish commit<commit_after>from contextlib import contextmanager
from os.path import join, exists, dirname, relpath, isdir, realpath
from os import environ, makedirs
from tempfile import mkdtemp
from time import sleep, time
import fcntl, errno

from git import Repo, Actor, GitCommandError
from slugify import slugify

from ..repo_functions import MergeConflict
from ..constants import WORKING_STATE_PUBLISHED, WORKING_STATE_DELETED, USERTASK_DIRECTORY_PATTERN

def _calculate_dirname(actor, origin):
    ''' Prepare a consistent directory for this user and this repository.
    '''
    first_commit = list(origin.iter_commits())[-1].hexsha
    return USERTASK_DIRECTORY_PATTERN.format(sha=first_commit[:8], email=slugify(actor.email))

@contextmanager
def get_usertask(*args, **kwargs):
    task = UserTask(*args, **kwargs)
    yield task
    task.cleanup()

class UserTaskPublished(RuntimeError): pass
class UserTaskDeleted(RuntimeError): pass
class UserTaskUnpushable(RuntimeError): pass

class UserTask():
    actor = None
    commit_sha = None
    _writeable = True
    _committed = False
    _pushed = False

    def __init__(self, actor, task_id, origin_dirname, working_dirname, start_point=None):
        '''

            start_point: task ID or commit SHA.
        '''
        self.actor = actor
        self.task_id = task_id

        # Prepare a clone directory.
        origin = Repo(origin_dirname)
        user_dirname = _calculate_dirname(self.actor, origin)
        clone_dirname = join(realpath(working_dirname), user_dirname)

        self._lock(clone_dirname)

        if isdir(clone_dirname):
            self.repo = Repo(clone_dirname)
            self.repo.git.checkout('zelig')
            self.repo.git.reset('origin/master', hard=True)
        else:
            # Clone origin to local checkout.
            self.repo = origin.clone(clone_dirname)
            self.repo.git.checkout('zelig', orphan=True)
            self.repo.git.reset('origin/master', hard=True)

        # Fetch all branches from origin.
        self.repo.git.fetch('origin')

        # Determine if a start-point was passed or needs to be inferred.
        if start_point is None:
            start_point = task_id
            self._writeable = False

        # Figure out what start_point is.
        if 'origin/{}'.format(start_point) in self.repo.refs:
            branch = self.repo.refs['origin/{}'.format(start_point)]
            self.commit_sha = branch.commit.hexsha
        else:
            self.commit_sha = start_point

        # Point local zelig to start_point.
        self.repo.git.reset(self.commit_sha, hard=True)

    def __repr__(self):
        return '<UserTask {} in {}>'.format(self.actor.email, self.repo.working_dir)

    def _lock(self, clone_dirname):
        self._lockfile = open('{}.lock'.format(clone_dirname), 'a')
        timeout = time() + 1.

        while True:
            try:
                fcntl.flock(self._lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
                # Lock acquired!
                return
            except IOError, ex:
                if ex.errno != errno.EAGAIN:  # Resource temporarily unavailable
                    raise
                elif time() > timeout:
                    # Exceeded the timeout.
                    raise

            sleep(.01)

    def _unlock(self):
        fcntl.flock(self._lockfile, fcntl.LOCK_UN)

    def _open(self, path, *args, **kwargs):
        return open(join(self.repo.working_dir, path), *args, **kwargs)

    def read(self, filename):
        with self._open(filename, 'r') as file:
            return file.read()

    def write(self, filename, content):
        assert self._writeable and not (self._committed or self._pushed)

        with self._open(filename, 'w') as file:
            file.write(content)
        self.repo.git.add(filename)

    def move(self, old_path, new_path):
        assert self._writeable and not (self._committed or self._pushed)

        #
        # Make sure we're not trying to move a directory inside itself.
        # This behavior is pretty old, and it's unclear if we want to
        # keep it but for now we just wants the existings test to pass.
        #
        old_dirname, new_dirname = dirname(old_path), dirname(new_path)

        if old_dirname:
            if not relpath(new_dirname, old_dirname).startswith('..'):
                raise ValueError(u'I cannot move a directory inside itself!', u'warning')

        dir_path = join(self.repo.working_dir, dirname(new_path))

        if not exists(dir_path):
            makedirs(dir_path)

        self.repo.git.mv(old_path, new_path)

    def commit(self, message):
        assert self._writeable and not (self._committed or self._pushed)
        self._committed = True

        # Commit to local zelig, push to origin task ID.
        self._set_author_env()
        dirty = self.repo.is_dirty()
        if dirty:
            self.repo.git.commit(m=message, a=True)
        return dirty

    def is_pushable(self):
        ''' Return pushable status: True, False, or a working state constant.
        '''
        if not self._writeable:
            return False

        if self._pushed:
            return False

        if not self._committed:
            return False

        if self.task_id in self.repo.tags:
            return WORKING_STATE_PUBLISHED

        if 'origin/{}'.format(self.task_id) not in self.repo.refs:
            return WORKING_STATE_DELETED

        return True

    def push(self):
        assert self._committed and not self._pushed
        pushable = self.is_pushable()

        if pushable is True:
            self._pushed = True
        if pushable is WORKING_STATE_PUBLISHED:
            # point self.commit_sha to the publish commit
            self.commit_sha = self.repo.tags[self.task_id].commit.hexsha
            raise UserTaskPublished()
        elif pushable is WORKING_STATE_DELETED:
            raise UserTaskDeleted()
        elif pushable is False:
            raise UserTaskUnpushable()

        # See if we are behind the origin branch, for example because we are
        # using the back button for editing, and starting from an older commit.
        task_sha = self._get_task_sha()

        # Rebase if necessary.
        if task_sha != self.commit_sha:
            self._rebase_with_author_check(task_sha)

        try:
            # Push to origin; we think this is safe to do.
            self.repo.git.push('origin', 'zelig:{}'.format(self.task_id))

        except GitCommandError:
            # Push failed, possibly because origin has
            # been modified due to rapid concurrent editing.
            self.repo.git.fetch('origin')
            self._rebase_with_author_check(self._get_task_sha())

        # Re-point self.commit_sha to the new one.
        self.commit_sha = self._get_task_sha()

    def ref_info(self, ref=None):
        ''' Return dict with author email and a relative date for a given reference.
        '''
        if ref is None:
            ref = self.commit_sha

        elif ref in self.repo.tags:
            # De-reference the tag. Sometimes showing a tag shows more than
            # just the commit, prefixing output with "tag <tag name>" if
            # there are notes attached.
            ref = self.repo.tags[ref].commit.hexsha

        raw = self.repo.git.show('--format=%ae %ar', ref)
        email, date = raw.split('\n')[0].split(' ', 1)

        return dict(published_date=date, published_by=email)

    def _set_author_env(self):
        '''
        '''
        environ['GIT_AUTHOR_NAME'] = self.actor.name
        environ['GIT_COMMITTER_NAME'] = self.actor.name
        environ['GIT_AUTHOR_EMAIL'] = self.actor.email
        environ['GIT_COMMITTER_EMAIL'] = self.actor.email

    def _is_interloped(self, ending_sha):
        '''
        '''
        rev_list = '{}..{}'.format(self.commit_sha, ending_sha)
        actors = [commit.author for commit in self.repo.iter_commits(rev_list)]

        for commit in self.repo.iter_commits(rev_list):
            if commit.author != self.actor:
                return True

        return False

    def _get_task_sha(self):
        ''' Get local commit SHA for a given task ID.
        '''
        branch = self.repo.refs['origin/{}'.format(self.task_id)]
        return branch.commit.hexsha

    def _rebase_with_author_check(self, task_sha):
        ''' Rebase onto given SHA, checking for interloping authors.

            If no interlopers exist, use an aggressive merge strategy
            to clobber possible conflicts. If any interloper exists,
            use a more timid strategy and possibly raise a MergeConflict.
        '''
        if self._is_interloped(task_sha):
            try:
                # Do a timid rebase since someone else has been here.
                local_commit = self.repo.commit()
                self.repo.git.rebase(task_sha)
            except GitCommandError:
                # Raise a MergeConflict.
                remote_commit = self.repo.commit(task_sha)
                raise MergeConflict(remote_commit, local_commit)
        else:
            # Do an aggressive rebase since no one else has been here.
            self.repo.git.rebase(task_sha, X='theirs')

    def cleanup(self):
        # once we have locking, we will unlock here
        self._unlock()

    def __del__(self):
        self.cleanup()
<|endoftext|>


<commit_before># Module:   errors
# Date:     11th February 2009
# Author:   James Mills, prologic at shortcircuit dot net dot au

"""Errors

This module implements a set of standard HTTP Errors.
"""

from cgi import escape as _escape
from urlparse import urljoin as _urljoin

import utils
from constants import DEFAULT_ERROR_MESSAGE, RESPONSES, SERVER_VERSION

class BaseError(object):

    def __init__(self, request, response):
        super(BaseError, self).__init__()

        self.request = request
        self.response = response

    def __nonzero__(self):
        return False

class HTTPError(BaseError):

    def __init__(self, request, response, status, message=None, error=None):
        super(HTTPError, self).__init__(request, response)

        short, long = RESPONSES.get(status, ("???", "???",))
        message = message or short

        s = DEFAULT_ERROR_MESSAGE % {
            "status": "%s %s" % (status, short),
            "message": _escape(message),
            "traceback": error or "",
            "version": SERVER_VERSION}

        response.clear()
        response.body = s
        response.status = "%s %s" % (status, short)
        response.headers.add_header("Connection", "close")

class Forbidden(HTTPError):

    def __init__(self, request, response, message=None):
        super(Forbidden, self).__init__(request, response, 403, message)

class NotFound(HTTPError):

    def __init__(self, request, response, message=None):
        super(NotFound, self).__init__(request, response, 404, message)

class Redirect(BaseError):

    def __init__(self, request, response, urls, status=None):
        super(Redirect, self).__init__(request, response)

        if isinstance(urls, basestring):
            urls = [urls]

        abs_urls = []
        for url in urls:
            # Note that urljoin will "do the right thing" whether url is:
            #  1. a complete URL with host (e.g. "http://www.example.com/test")
            #  2. a URL relative to root (e.g. "/dummy")
            #  3. a URL relative to the current path
            # Note that any query string in request is discarded.
            url = _urljoin(utils.url(request), url)
            abs_urls.append(url)
        urls = abs_urls

        # RFC 2616 indicates a 301 response code fits our goal; however,
        # browser support for 301 is quite messy. Do 302/303 instead. See
        # http://ppewww.ph.gla.ac.uk/~flavell/www/post-redirect.html
        if status is None:
            if request.protocol >= (1, 1):
                status = 303
            else:
                status = 302
        else:
            status = int(status)
            if status < 300 or status > 399:
                raise ValueError("status must be between 300 and 399.")

        response.status = status

        if status in (300, 301, 302, 303, 307):
            response.headers["Content-Type"] = "text/html"
            # "The ... URI SHOULD be given by the Location field
            # in the response."
            response.headers["Location"] = urls[0]

            # "Unless the request method was HEAD, the entity of the response
            # SHOULD contain a short hypertext note with a hyperlink to the
            # new URI(s)."
            msg = {300: "This resource can be found at %s.",
                   301: "This resource has permanently moved to %s.",
                   302: "This resource resides temporarily at %s.",
                   303: "This resource can be found at %s.",
                   307: "This resource has moved temporarily to %s.",
                   }[status]
            response.body = "
\n".join([msg % (u, u) for u in urls])
            # Previous code may have set C-L, so we have to reset it
            # (allow finalize to set it).
            response.headers.pop("Content-Length", None)
        elif status == 304:
            # Not Modified.
            # "The response MUST include the following header fields:
            # Date, unless its omission is required by section 14.18.1"
            # The "Date" header should have been set in Response.__init__

            # "...the response SHOULD NOT include other entity-headers."
            for key in ("Allow", "Content-Encoding", "Content-Language",
                        "Content-Length", "Content-Location", "Content-MD5",
                        "Content-Range", "Content-Type", "Expires",
                        "Last-Modified"):
                if key in response.headers:
                    del response.headers[key]

            # "The 304 response MUST NOT contain a message-body."
            response.body = None
            # Previous code may have set C-L, so we have to reset it.
            response.headers.pop("Content-Length", None)
        elif status == 305:
            # Use Proxy.
            # urls[0] should be the URI of the proxy.
            response.headers["Location"] = urls[0]
            response.body = None
            # Previous code may have set C-L, so we have to reset it.
            response.headers.pop("Content-Length", None)
        else:
            raise ValueError("The %s status code is unknown." % status)
<commit_msg>lib.web.errors: Return True when evaluated as a bool (__nonzero__)<commit_after># Module:   errors
# Date:     11th February 2009
# Author:   James Mills, prologic at shortcircuit dot net dot au

"""Errors

This module implements a set of standard HTTP Errors.
"""

from cgi import escape as _escape
from urlparse import urljoin as _urljoin

import utils
from constants import DEFAULT_ERROR_MESSAGE, RESPONSES, SERVER_VERSION

class BaseError(object):

    def __init__(self, request, response):
        super(BaseError, self).__init__()

        self.request = request
        self.response = response

    def __nonzero__(self):
        return True

class HTTPError(BaseError):

    def __init__(self, request, response, status, message=None, error=None):
        super(HTTPError, self).__init__(request, response)

        short, long = RESPONSES.get(status, ("???", "???",))
        message = message or short

        s = DEFAULT_ERROR_MESSAGE % {
            "status": "%s %s" % (status, short),
            "message": _escape(message),
            "traceback": error or "",
            "version": SERVER_VERSION}

        response.clear()
        response.body = s
        response.status = "%s %s" % (status, short)
        response.headers.add_header("Connection", "close")

class Forbidden(HTTPError):

    def __init__(self, request, response, message=None):
        super(Forbidden, self).__init__(request, response, 403, message)

class NotFound(HTTPError):

    def __init__(self, request, response, message=None):
        super(NotFound, self).__init__(request, response, 404, message)

class Redirect(BaseError):

    def __init__(self, request, response, urls, status=None):
        super(Redirect, self).__init__(request, response)

        if isinstance(urls, basestring):
            urls = [urls]

        abs_urls = []
        for url in urls:
            # Note that urljoin will "do the right thing" whether url is:
            #  1. a complete URL with host (e.g. "http://www.example.com/test")
            #  2. a URL relative to root (e.g. "/dummy")
            #  3. a URL relative to the current path
            # Note that any query string in request is discarded.
            url = _urljoin(utils.url(request), url)
            abs_urls.append(url)
        urls = abs_urls

        # RFC 2616 indicates a 301 response code fits our goal; however,
        # browser support for 301 is quite messy. Do 302/303 instead. See
        # http://ppewww.ph.gla.ac.uk/~flavell/www/post-redirect.html
        if status is None:
            if request.protocol >= (1, 1):
                status = 303
            else:
                status = 302
        else:
            status = int(status)
            if status < 300 or status > 399:
                raise ValueError("status must be between 300 and 399.")

        response.status = status

        if status in (300, 301, 302, 303, 307):
            response.headers["Content-Type"] = "text/html"
            # "The ... URI SHOULD be given by the Location field
            # in the response."
            response.headers["Location"] = urls[0]

            # "Unless the request method was HEAD, the entity of the response
            # SHOULD contain a short hypertext note with a hyperlink to the
            # new URI(s)."
            msg = {300: "This resource can be found at %s.",
                   301: "This resource has permanently moved to %s.",
                   302: "This resource resides temporarily at %s.",
                   303: "This resource can be found at %s.",
                   307: "This resource has moved temporarily to %s.",
                   }[status]
            response.body = "
\n".join([msg % (u, u) for u in urls])
            # Previous code may have set C-L, so we have to reset it
            # (allow finalize to set it).
            response.headers.pop("Content-Length", None)
        elif status == 304:
            # Not Modified.
            # "The response MUST include the following header fields:
            # Date, unless its omission is required by section 14.18.1"
            # The "Date" header should have been set in Response.__init__

            # "...the response SHOULD NOT include other entity-headers."
            for key in ("Allow", "Content-Encoding", "Content-Language",
                        "Content-Length", "Content-Location", "Content-MD5",
                        "Content-Range", "Content-Type", "Expires",
                        "Last-Modified"):
                if key in response.headers:
                    del response.headers[key]

            # "The 304 response MUST NOT contain a message-body."
            response.body = None
            # Previous code may have set C-L, so we have to reset it.
            response.headers.pop("Content-Length", None)
        elif status == 305:
            # Use Proxy.
            # urls[0] should be the URI of the proxy.
            response.headers["Location"] = urls[0]
            response.body = None
            # Previous code may have set C-L, so we have to reset it.
            response.headers.pop("Content-Length", None)
        else:
            raise ValueError("The %s status code is unknown." % status)
<|endoftext|>


<commit_before># Copyright 2015 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Implements TestRunner class.

The TestRunner class is used to control the overall execution and reporting
of tests. It is a unittest.TestRunner that will setup and tear down the global
test environment, and delegate to another unittest.TestRunner to run the
actual tests. This class wraps the delegate by setting up the bindings and
reporting scribe while letting the injected TestRunner perform the actual
running and standard reporting hooks used by other tools.
"""

# Standard python modules.
import argparse
import ast
import datetime
import logging
import logging.config
import os.path
import sys
import unittest

# Our modules.
from . import args_util
from . import html_scribe
from .scribe import Doodle

# If a -log_config is not provided, then use this.
_DEFAULT_LOG_CONFIG = """{
 'version':1,
 'disable_existing_loggers':True,
 'formatters':{
 'timestamped':{
   'format':'%(asctime)s %(message)s',
   'datefmt':'%H:%M:%S'
  }
 },
 'handlers':{
   'console':{
   'level':'WARNING',
   'class':'logging.StreamHandler',
   'formatter':'timestamped'
  },
  'file':{
   'level':'DEBUG',
   'class':'logging.FileHandler',
   'formatter':'timestamped',
   'filename':'$LOG_DIR/$LOG_FILENAME',
   'mode':'w'
  }
 },
  'loggers':{
  '': {
   'level':'DEBUG',
   'handlers':['console', 'file']
  }
 }
}
"""

class TestRunner(object):
  """Provides additional reporting for existing TestRunners.

  The TestRunner delegates to an existing injected unittest TestRunner
  (e.g. TextTestRunner) to run tests.

  It is assumed that where effects are not desired to be shared, the test
  will either undo the effects as part of the test or contribute an additional
  test that will undo the effects and have that test execute before other
  tests that do not want the side effects.

  The runner maintains a map of bindings to different parameters available to
  tests. The bindings dictionary is exposed for direct injection of hardcoded
  values without overrides.

  The runner configures logging using the LOG_CONFIG key to get the config
  filename, if any. If no LOG_CONFIG is provided, the a default will be used.
  The LOG_CONFIG can reference additional |$KEY| variables, which will be
  resolved using the binding for |KEY|.
  """

  __global_runner = None

  @property
  def options(self):
    """Returns a ArgumentParserNamespace with the commandline option values.

    The intention is for the bindings to be a more complete collection, but
    options are here for convienence in controlled circumstances
    (e.g. 'private' options).
    """
    return self.__options

  @property
  def bindings(self):
    """Returns a dictionary with name/value bindings.

    The keys in the binding dictionary are upper case by convention to help
    distinguish them. The default bindings are derived from the "options",
    however the program is free to add additional bindings.
    """
    return self.__bindings

  @property
  def default_binding_overrides(self):
    """A dictionary keyed by the binding key used to initialize options.

    The purpose of this dictionary is to provide default values when adding
    argumentParser arguments. This dictionary is passed to the
    initArgumentParser method in the BaseTestCase when initializing the
    ArgumentParser. Programs can use this to inject the default submodule
    values they'd like to override.
    """
    return self.__default_binding_overrides

  @property
  def report_scribe(self):
    """Returns a scribe used to render the generated report."""
    return self.__report_scribe

  @staticmethod
  def global_runner():
    """Returns the TestRunner instance.

    Presumably there is only one.
    """
    if TestRunner.__global_runner is None:
      raise BaseException('TestRunner not yet instantiated')
    return TestRunner.__global_runner

  @classmethod
  def main(cls, runner=None,
           default_binding_overrides=None,
           test_case_list=None):
    """Implements a main method for running tests.

    This main will instantiate an instance of the class |cls| then
    pass control to the _do_main method on the instantiated runner.

    Args:
      runner: If provided, then delegate to this runner to run the tests.
      default_binding_overrides: Provides a means to inject default values
          to use for bindings.
      test_case_list: If provided, a list of test cases to run.
    """
    runner = cls(runner=runner)
    runner.set_default_binding_overrides(default_binding_overrides)
    return runner._do_main(test_case_list=test_case_list)

  def set_default_binding_overrides(self, overrides):
    """Provides a means for setting the default_binding_overrides attribute.

    This is intentionall not an assignment because it is not intended to be
    called, but is here in case it is no possible to use the "main()" method.
    """
    self.__default_binding_overrides = overrides or {}

  def _do_main(self, default_binding_overrides=None, test_case_list=None):
    """Helper function used by main() once a TestRunner instance exists."""
    # pylint: disable=unused-argument
    # default_binding_overrides is declared as a prototype for derived classes
    # but not actually used here.
    logger = logging.getLogger(__name__)
    logger.info('Building test suite')
    suite = self.build_suite(test_case_list)

    # Create some separation in logs
    logger.info('Finished Setup. Start Tests\n'
                + ' ' * (8 + 1)  # for leading timestamp prefix
                + '---------------------------\n')
    result = self.run(suite)
    return len(result.failures) + len(result.errors)

  def __init__(self, runner=None):
    TestRunner.__global_runner = self
    self.__delegate = runner or unittest.TextTestRunner(verbosity=2)
    self.__options = None
    self.__bindings = {}
    self.__default_binding_overrides = {}
    self.__report_scribe = None
    self.__report_file = None

  def run(self, obj_or_suite):
    """Run tests.

    Args:
      obj_or_suite: The TestCase or TestSuite to run.
    """
    self._prepare()

    logger = logging.getLogger(__name__)
    logger.info('Running tests')

    try:
      result = self.__delegate.run(obj_or_suite)
    finally:
      if sys.exc_info()[0] != None:
        sys.stderr.write('Terminated early due to an exception\n')
      self._cleanup()

    return result

  def initArgumentParser(self, parser, defaults=None):
    """Adds arguments introduced by the TestRunner module.

    Args:
      parser: argparse.ArgumentParser instance to add to.
    """
    # Normally we want the log file name to reflect the name of the program
    # we are running, but we might not be running one (e.g. in interpreter).
    try:
      basename = os.path.basename(sys.argv[0])
      main_filename = os.path.splitext(basename)[0] + '.log'
    except IndexError:
      main_filename = 'debug.log'

    defaults = defaults or {}
    parser.add_argument('--log_dir', default=defaults.get('LOG_DIR', '.'))
    parser.add_argument('--log_filename',
                        default=defaults.get('LOG_FILENAME', main_filename))
    parser.add_argument(
        '--log_config', default=defaults.get('LOG_CONFIG', ''),
        help='Path to text file containing custom logging configuration. The'
        ' contents of this path can contain variable references in the form'
        ' $KEY where --KEY is a command-line argument that whose value should'
        ' be substituted. Otherwise this is a standard python logging'
        ' configuration schema as described in'
        ' https://docs.python.org/2/library/logging.config.html'
        '#logging-config-dictschema')

  def start_logging(self):
    """Setup default logging from the --log_config parameter."""
    text = _DEFAULT_LOG_CONFIG
    path = self.bindings.get('LOG_CONFIG', None)
    if path:
      try:
        with open(path, 'r') as f:
          text = f.read()
      except Exception as ex:
        print 'ERROR reading LOGGING_CONFIG from {0}: {1}'.format(path, ex)
        raise
    config = ast.literal_eval(args_util.replace(text, self.bindings))
    logging.config.dictConfig(config)
    path = os.path.join(
        self.bindings['LOG_DIR'], self.bindings['LOG_FILENAME'])
    os.chmod(path, 0600)

  def start_report_scribe(self):
    """Sets up report_scribe and output file for high level reporting."""
    if self.__report_scribe is not None:
      raise ValueError('report_scribe already started.')

    dirname = self.bindings.get('LOG_DIR', '.')
    filename = os.path.basename(self.bindings.get('LOG_FILENAME'))
    filename = os.path.splitext(filename)[0] + '.html'
    title = '{program} at {time}'.format(
        program=os.path.basename(sys.argv[0]),
        time=datetime.datetime.now().strftime("%d/%m/%Y %H:%M:%S"))

    self.__report_scribe = html_scribe.HtmlScribe()
    out = Doodle(self.__report_scribe)
    self.__report_scribe.write_begin_html_document(out, title)
    out.write(
        '
{title}
\n'.format(title=title))
    self.__report_scribe.write_key_html(out)
    self.__report_file = open('{0}/{1}'.format(dirname, filename), 'w')
    os.fchmod(self.__report_file.fileno(), 0600)  # Protect sensitive data.
    self.__report_file.write(str(out))

  def report(self, obj):
    """Add object to report.

    Args:
      obj: The object to write into the report.
    """
    self.__report_file.write(self.__report_scribe.render_to_string(obj))

  def finish_report_scribe(self):
    """Finish the reporting scribe and close the file."""
    if self.__report_scribe is None:
      return
    out = Doodle(self.__report_scribe)
    self.__report_scribe.write_end_html_document(out)
    self.__report_file.write(str(out))
    self.__report_file.close()
    self.__report_scribe = None
    self.__report_file = None

  def build_suite(self, test_case_list):
    """Build the TestSuite of tests to run."""
    if not test_case_list:
      raise ValueError('No test cases provided.')

    loader = unittest.TestLoader()

    # TODO(ewiseblatt): 20150521
    # This doesnt seem to take effect. The intent here is to not sort the order
    # of tests. But it still is. So I've renamed the tests to lexographically
    # sort in place. Leaving this around anyway in hopes to eventually figure
    # out why it doesnt work.
    loader.sortTestMethodsUsing = None

    suite = unittest.TestSuite()
    for test in test_case_list:
      suite.addTests(loader.loadTestsFromTestCase(test))
    return suite

  def _prepare(self):
    """Suite helper function finishes initialization of global context.

    This includes processing command-line arguments to set the bindings in
    the runner, and initializing the reporting scribe.
    """
    # Customize commandline arguments
    parser = argparse.ArgumentParser()
    self.initArgumentParser(parser, defaults=self.default_binding_overrides)
    self.__options = parser.parse_args()
    self.__bindings.update(args_util.parser_args_to_bindings(self.__options))

    self.start_logging()
    self.start_report_scribe()

  def _cleanup(self):
    """Helper function when running a suite for cleaning up the global context.

    This incudes closing out the report.
    """
    self.finish_report_scribe()
<commit_msg>Integrate Journal/snapshotting with base test runner.<commit_after># Copyright 2015 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Implements TestRunner class.

The TestRunner class is used to control the overall execution and reporting
of tests. It is a unittest.TestRunner that will setup and tear down the global
test environment, and delegate to another unittest.TestRunner to run the
actual tests. This class wraps the delegate by setting up the bindings and
reporting scribe while letting the injected TestRunner perform the actual
running and standard reporting hooks used by other tools.
"""

# Standard python modules.
import argparse
import ast
import datetime
import logging
import logging.config
import os.path
import sys
import unittest

# Our modules.
from . import args_util
from . import html_scribe
from .scribe import Doodle
from . import (Journal, JsonSnapshotable)

# If a -log_config is not provided, then use this.
_DEFAULT_LOG_CONFIG = """{
 'version':1,
 'disable_existing_loggers':True,
 'formatters':{
 'timestamped':{
   'format':'%(asctime)s %(message)s',
   'datefmt':'%H:%M:%S'
  }
 },
 'handlers':{
   'console':{
   'level':'WARNING',
   'class':'logging.StreamHandler',
   'formatter':'timestamped'
  },
  'file':{
   'level':'DEBUG',
   'class':'logging.FileHandler',
   'formatter':'timestamped',
   'filename':'$LOG_DIR/$LOG_FILENAME',
   'mode':'w'
  }
 },
  'loggers':{
  '': {
   'level':'DEBUG',
   'handlers':['console', 'file']
  }
 }
}
"""

class TestRunner(object):
  """Provides additional reporting for existing TestRunners.

  The TestRunner delegates to an existing injected unittest TestRunner
  (e.g. TextTestRunner) to run tests.

  It is assumed that where effects are not desired to be shared, the test
  will either undo the effects as part of the test or contribute an additional
  test that will undo the effects and have that test execute before other
  tests that do not want the side effects.

  The runner maintains a map of bindings to different parameters available to
  tests. The bindings dictionary is exposed for direct injection of hardcoded
  values without overrides.

  The runner configures logging using the LOG_CONFIG key to get the config
  filename, if any. If no LOG_CONFIG is provided, the a default will be used.
  The LOG_CONFIG can reference additional |$KEY| variables, which will be
  resolved using the binding for |KEY|.
  """

  __global_runner = None

  @property
  def options(self):
    """Returns a ArgumentParserNamespace with the commandline option values.

    The intention is for the bindings to be a more complete collection, but
    options are here for convienence in controlled circumstances
    (e.g. 'private' options).
    """
    return self.__options

  @property
  def bindings(self):
    """Returns a dictionary with name/value bindings.

    The keys in the binding dictionary are upper case by convention to help
    distinguish them. The default bindings are derived from the "options",
    however the program is free to add additional bindings.
    """
    return self.__bindings

  @property
  def default_binding_overrides(self):
    """A dictionary keyed by the binding key used to initialize options.

    The purpose of this dictionary is to provide default values when adding
    argumentParser arguments. This dictionary is passed to the
    initArgumentParser method in the BaseTestCase when initializing the
    ArgumentParser. Programs can use this to inject the default submodule
    values they'd like to override.
    """
    return self.__default_binding_overrides

  @property
  def report_scribe(self):
    """Returns a scribe used to render the generated report."""
    return self.__report_scribe

  @staticmethod
  def global_runner():
    """Returns the TestRunner instance.

    Presumably there is only one.
    """
    if TestRunner.__global_runner is None:
      raise BaseException('TestRunner not yet instantiated')
    return TestRunner.__global_runner

  @classmethod
  def main(cls, runner=None,
           default_binding_overrides=None,
           test_case_list=None):
    """Implements a main method for running tests.

    This main will instantiate an instance of the class |cls| then
    pass control to the _do_main method on the instantiated runner.

    Args:
      runner: If provided, then delegate to this runner to run the tests.
      default_binding_overrides: Provides a means to inject default values
          to use for bindings.
      test_case_list: If provided, a list of test cases to run.
    """
    runner = cls(runner=runner)
    runner.set_default_binding_overrides(default_binding_overrides)
    return runner._do_main(test_case_list=test_case_list)

  def set_default_binding_overrides(self, overrides):
    """Provides a means for setting the default_binding_overrides attribute.

    This is intentionall not an assignment because it is not intended to be
    called, but is here in case it is no possible to use the "main()" method.
    """
    self.__default_binding_overrides = overrides or {}

  def _do_main(self, default_binding_overrides=None, test_case_list=None):
    """Helper function used by main() once a TestRunner instance exists."""
    # pylint: disable=unused-argument
    # default_binding_overrides is declared as a prototype for derived classes
    # but not actually used here.
    logger = logging.getLogger(__name__)
    logger.info('Building test suite')
    suite = self.build_suite(test_case_list)

    # Create some separation in logs
    logger.info('Finished Setup. Start Tests\n'
                + ' ' * (8 + 1)  # for leading timestamp prefix
                + '---------------------------\n')
    result = self.run(suite)
    return len(result.failures) + len(result.errors)

  def __init__(self, runner=None):
    TestRunner.__global_runner = self
    self.__delegate = runner or unittest.TextTestRunner(verbosity=2)
    self.__options = None
    self.__bindings = {}
    self.__default_binding_overrides = {}
    self.__journal = None
    self.__report_scribe = None
    self.__report_file = None

  def run(self, obj_or_suite):
    """Run tests.

    Args:
      obj_or_suite: The TestCase or TestSuite to run.
    """
    self._prepare()

    logger = logging.getLogger(__name__)
    logger.info('Running tests')

    try:
      result = self.__delegate.run(obj_or_suite)
    finally:
      if sys.exc_info()[0] != None:
        sys.stderr.write('Terminated early due to an exception\n')
      self._cleanup()

    return result

  def initArgumentParser(self, parser, defaults=None):
    """Adds arguments introduced by the TestRunner module.

    Args:
      parser: argparse.ArgumentParser instance to add to.
    """
    # Normally we want the log file name to reflect the name of the program
    # we are running, but we might not be running one (e.g. in interpreter).
    try:
      basename = os.path.basename(sys.argv[0])
      main_filename = os.path.splitext(basename)[0] + '.log'
    except IndexError:
      main_filename = 'debug.log'

    defaults = defaults or {}
    parser.add_argument('--log_dir', default=defaults.get('LOG_DIR', '.'))
    parser.add_argument('--log_filename',
                        default=defaults.get('LOG_FILENAME', main_filename))
    parser.add_argument(
        '--log_config', default=defaults.get('LOG_CONFIG', ''),
        help='Path to text file containing custom logging configuration. The'
        ' contents of this path can contain variable references in the form'
        ' $KEY where --KEY is a command-line argument that whose value should'
        ' be substituted. Otherwise this is a standard python logging'
        ' configuration schema as described in'
        ' https://docs.python.org/2/library/logging.config.html'
        '#logging-config-dictschema')

  def start_logging(self):
    """Setup default logging from the --log_config parameter."""
    text = _DEFAULT_LOG_CONFIG
    path = self.bindings.get('LOG_CONFIG', None)
    if path:
      try:
        with open(path, 'r') as f:
          text = f.read()
      except Exception as ex:
        print 'ERROR reading LOGGING_CONFIG from {0}: {1}'.format(path, ex)
        raise
    config = ast.literal_eval(args_util.replace(text, self.bindings))
    logging.config.dictConfig(config)
    path = os.path.join(
        self.bindings['LOG_DIR'], self.bindings['LOG_FILENAME'])
    os.chmod(path, 0600)

  def start_report_scribe(self):
    """Sets up report_scribe and output file for high level reporting."""
    if self.__report_scribe is not None:
      raise ValueError('report_scribe already started.')

    dirname = self.bindings.get('LOG_DIR', '.')
    filename = os.path.basename(self.bindings.get('LOG_FILENAME'))
    filename = os.path.splitext(filename)[0] + '.html'
    title = '{program} at {time}'.format(
        program=os.path.basename(sys.argv[0]),
        time=datetime.datetime.now().strftime("%d/%m/%Y %H:%M:%S"))

    self.__report_scribe = html_scribe.HtmlScribe()
    out = Doodle(self.__report_scribe)
    self.__report_scribe.write_begin_html_document(out, title)
    out.write(
        '
{title}
\n'.format(title=title))
    self.__report_scribe.write_key_html(out)
    self.__report_file = open('{0}/{1}'.format(dirname, filename), 'w')
    os.fchmod(self.__report_file.fileno(), 0600)  # Protect sensitive data.
    self.__report_file.write(str(out))

    filename = os.path.splitext(filename)[0] + '.journal.json'
    journal_file = open(os.path.join(dirname, filename), 'w')
    os.fchmod(journal_file.fileno(), 0600)  # Protect sensitive data.
    self.__journal = Journal()
    self.__journal.open_with_file(journal_file)

  def report(self, obj):
    """Add object to report.

    Args:
      obj: The object to write into the report.
    """
    if isinstance(obj, JsonSnapshotable):
      self.__journal.store(obj)
    else:
      self.__report_file.write(self.__report_scribe.render_to_string(obj))

  def finish_report_scribe(self):
    """Finish the reporting scribe and close the file."""
    if self.__report_scribe is None:
      return
    out = Doodle(self.__report_scribe)
    self.__report_scribe.write_end_html_document(out)
    self.__report_file.write(str(out))
    self.__report_file.close()
    self.__report_scribe = None
    self.__report_file = None
    self.__journal.terminate()
    self.__journal = None

  def build_suite(self, test_case_list):
    """Build the TestSuite of tests to run."""
    if not test_case_list:
      raise ValueError('No test cases provided.')

    loader = unittest.TestLoader()

    # TODO(ewiseblatt): 20150521
    # This doesnt seem to take effect. The intent here is to not sort the order
    # of tests. But it still is. So I've renamed the tests to lexographically
    # sort in place. Leaving this around anyway in hopes to eventually figure
    # out why it doesnt work.
    loader.sortTestMethodsUsing = None

    suite = unittest.TestSuite()
    for test in test_case_list:
      suite.addTests(loader.loadTestsFromTestCase(test))
    return suite

  def _prepare(self):
    """Suite helper function finishes initialization of global context.

    This includes processing command-line arguments to set the bindings in
    the runner, and initializing the reporting scribe.
    """
    # Customize commandline arguments
    parser = argparse.ArgumentParser()
    self.initArgumentParser(parser, defaults=self.default_binding_overrides)
    self.__options = parser.parse_args()
    self.__bindings.update(args_util.parser_args_to_bindings(self.__options))

    self.start_logging()
    self.start_report_scribe()

  def _cleanup(self):
    """Helper function when running a suite for cleaning up the global context.

    This incudes closing out the report.
    """
    self.finish_report_scribe()
<|endoftext|>