<commit_before># -*- coding: utf-8 -*-
# Copyright (c) 2012 Fabian Barkhau <fabian.barkhau@gmail.com>
# License: MIT (see LICENSE.TXT file)

import datetime
from django.core.exceptions import PermissionDenied
from django.utils.translation import ugettext as _
from django.dispatch import receiver
from apps.common.shortcuts import send_mail
from apps.borrow.models import Borrow
from apps.borrow.models import Rating
from apps.borrow.models import Log
from apps.borrow import signals
from apps.team import control as team_control
from apps.account import control as account_control

def _remove_from_borrow_chain(borrow):
    """ Pass the borrow being removed from the chain.
        Ensure that borrow chain src and dest stations always match.

        Before                      After
          x>1 - 1>B>2 - 2>x           x>1 -   -   - 1>x
        1>b>2 - 2>B>3 - 3>x         1>b>2 -   -   - 2>x
          x>1 - 1>B>2 - 2>b>3         x>1 -   -   - 1>b>3
        1>b>2 - 2>B>3 - 3>b>4       1>b>2 -   -   - 2>b>4
    """
    next_borrow = get_next_borrow(borrow.bike, borrow.finish)
    if next_borrow and next_borrow.src != borrow.src:
         next_borrow.src = borrow.src
         next_borrow.save()
         log(None, next_borrow, "", "EDIT")

def _insert_into_borrow_chain(borrow, bike, account=None, note=""):
    """ Pass the borrow being inserted into the chain.
        Ensure that borrow chain src and dest stations always match.

         jul     aug     sep         jul     aug     sep
        Before                      After
          x>1 -   -   - 1>x           x>1 - 1>B>1 - 1>x
        1>b>2 -   -   - 2>x         1>b>2 - 2>B>2 - 2>x
          x>1 -   -   - 1>b>3         x>1 - 1>B>1 - 1>b>3
        1>b>2 -   -   - 2>b>4       1>b>2 - 2>B>2 - 2>b>4
    """
    if borrow.active: # fix borrow chains
        _remove_from_borrow_chain(borrow) # remove from previous chain
        prev_borrow = get_prev_borrow(bike, borrow.start)
        borrow.src = prev_borrow and prev_borrow.dest or bike.station
        borrow.dest = borrow.src
    borrow.bike = bike
    borrow.save()
    log(account, borrow, note, "EDIT")

def log(account, borrow, note, action):
    if not account: # get site account if available
        account = account_control.get_site_account()
    l = Log()
    l.borrow = borrow
    l.initiator = account
    l.action = action
    l.note = note
    l.save()
    signals.borrow_log_created.send(sender=log, log=l)
    return l

##########
# EMAILS #
##########

def _get_email_templates(party, action):
    action = action.lower()
    party = party.lower()
    template = "borrow/email/notify_{party}_{action}_{part}.txt"
    return (
        template.format(party=party, action=action, part="subject"),
        template.format(party=party, action=action, part="message"),
    )

@receiver(signals.borrow_log_created)
def borrow_log_created_borrower_callback(sender, **kwargs):
    log = kwargs["log"]
    if log.borrow.borrower == log.initiator:
        return # no need to notify user of there own actions
    if log.action in ["FINISHED", "LENDER_RATE"]:
        return # no one cares, dont spam
    email = account_control.get_email_or_404(log.borrow.borrower)
    subject, message = _get_email_templates("borrower", log.action)
    send_mail([email], subject, message, kwargs)

@receiver(signals.borrow_log_created)
def borrow_log_created_lender_callback(sender, **kwargs):
    log = kwargs["log"]
    if log.action in ["FINISHED", "BORROWER_RATE"]:
        return # no one cares, dont spam
    if team_control.is_member(log.initiator, log.borrow.team):
        return # not need to notify team of its own actions
    emails = team_control.get_team_emails(log.borrow.team)
    subject, message = _get_email_templates("lender", log.action)
    send_mail(emails, subject, message, kwargs)

###########
# COMMENT #
###########

def can_comment(account, borrow):
    if account == borrow.borrower:
        return True
    if team_control.is_member(account, borrow.team):
        return True
    return False

def comment(account, borrow, note):
    if not can_comment(account, borrow):
        raise PermissionDenied
    log(account, borrow, note, "COMMENT")

##########
# QUERRY #
##########

def get_next_borrow(bike, finish):
    if not bike:
        return None
    qs = Borrow.objects.filter(active=True, bike=bike, start__gt=finish)
    borrows = list(qs.order_by("start")[:1]) # order and limit
    return borrows and borrows[0] or None

def get_prev_borrow(bike, start):
    if not bike:
        return None
    qs = Borrow.objects.filter(active=True, bike=bike, finish__lt=start)
    borrows = list(qs.order_by("-finish")[:1]) # order and limit
    return borrows and borrows[0] or None

def active_borrows_in_timeframe(bike, start, finish, exclude=None):
    """ Returns borrows in the given timeframe. finish is inclusive! """
    if not bike:
        raise Exception("BIKE REQUIRED!")
    qs = Borrow.objects.filter(bike=bike, active=True)
    qs = qs.exclude(start__gt=finish)
    qs = qs.exclude(finish__lt=start)
    if exclude:
        qs = qs.exclude(id=exclude.id)
    return list(qs)

def to_list_data(borrows, team_link=False):
    def borrow2entrie(borrow):
        base_url = team_link and ("/%s" % borrow.team.link) or ""
        src = borrow.src and borrow.src.street or None
        dest = borrow.dest and borrow.dest.street or None
        bike_name = borrow.bike and borrow.bike.name or _("DELETED")
        return {
            "labels" : [
                borrow.borrower, bike_name, borrow.start, borrow.finish,
                src, dest, borrow.state
            ],
            "url" : "%s/borrow/view/%s" % (base_url, borrow.id)
        }
    return {
        "columns" : [
            _("BORROWER"), _("BIKE"), _("DATE_FROM"), _("DATE_TO"),
            _("STATION_FROM"), _("STATION_TO"), _("BORROW_STATE"),
        ],
        "entries" : map(borrow2entrie, borrows)
    }

def arrivals(account):
    today = datetime.datetime.now().date()
    return Borrow.objects.filter(active=True, finish__gte=today,
                                 dest__responsible=account)

def departures(account):
    today = datetime.datetime.now().date()
    return Borrow.objects.filter(active=True, start__gte=today,
                                 src__responsible=account)

##############
# RESPONDING #
##############

def can_respond(account, borrow):
    if borrow.state != "REQUEST":
        return False # borrow must be in request state
    if not team_control.is_member(account, borrow.team):
        return False # not a member
    return True

def response_is_allowed(account, borrow, state):
    if not can_respond(account, borrow):
        return False
    if state == "REJECTED":
        return True
    today = datetime.datetime.now().date()
    if borrow.finish <= today:
        return False # to late
    if not can_borrow(borrow.bike):
        return False
    if active_borrows_in_timeframe(borrow.bike, borrow.start, borrow.finish):
        return False
    return True

def respond(account, borrow, state, note):
    if not response_is_allowed(account, borrow, state):
        raise PermissionDenied
    borrow.state = state
    borrow.active = state != "REJECTED"
    if state != "REJECTED": # set stations
        prev_borrow = get_prev_borrow(borrow.bike, borrow.start)
        borrow.src = prev_borrow and prev_borrow.dest or borrow.bike.station
        borrow.dest = borrow.src
    borrow.save()
    log(account, borrow, note, "RESPOND")
    return borrow

#############
# CANCELING #
#############

def can_cancel(account, borrow):
    today = datetime.datetime.now().date()
    borrow_started = borrow.start <= datetime.datetime.now().date()
    borrow_ended = borrow.finish  < today
    is_lender = team_control.is_member(account, borrow.team)
    is_borrower = account == borrow.borrower
    lender_state = borrow.state in ["ACCEPTED"]
    borrower_state = borrow.state in ["REQUEST", "ACCEPTED"]
    return (is_borrower and borrower_state and not borrow_ended or
            is_lender and lender_state and not borrow_started)

def cancel(account, borrow, note):
    if not can_cancel(account, borrow):
        raise PermissionDenied
    borrow.state = "CANCELED"
    if borrow.active: # ensure borrow chain unbroken
        _remove_from_borrow_chain(borrow)
        borrow.src = None
        borrow.dest = None
    borrow.active = False
    borrow.save()
    log(account, borrow, note, "CANCEL")
    return borrow

#################
# CREATE BORROW #
#################

def can_borrow(bike):
    return (bike and bike.active and not bike.reserve and
            bike.station and bike.station.active)

def creation_is_allowed(bike, start, finish, exclude=None):
    if not can_borrow(bike):
        return False
    # check timeframe
    today = datetime.datetime.now().date()
    if start <= today:
        return False
    if finish < start:
        return False
    if len(active_borrows_in_timeframe(bike, start, finish, exclude=exclude)):
        return False
    return True

def create(account, bike, start, finish, note):
    if not account_control.has_required_info(account):
        raise PermissionDenied
    if not creation_is_allowed(bike, start, finish):
        raise PermissionDenied
    borrow = Borrow()
    borrow.bike = bike
    borrow.team = bike.team
    borrow.borrower = account
    borrow.start = start
    borrow.finish = finish
    borrow.active = False
    borrow.state = "REQUEST"
    borrow.save()
    log(account, borrow, note, "CREATE")
    return borrow

##########
# RATING #
##########

def _finish(borrow):
    if len(Rating.objects.filter(borrow=borrow)) != 2:
        return borrow # only finish when borrower and lender have rated
    borrow.state = "FINISHED"
    borrow.save()
    log(None, borrow, "", "FINISHED")
    return borrow

def lender_can_rate(account, borrow):
    today = datetime.datetime.now().date()
    if not today > borrow.finish:
        return False # to soon
    if borrow.state not in ["ACCEPTED"]:
        return False # wrong state
    if not team_control.is_member(account, borrow.team):
        return False # only members
    if len(Rating.objects.filter(borrow=borrow, originator='LENDER')):
        return False # already rated
    return True

def borrower_can_rate(account, borrow):
    today = datetime.datetime.now().date()
    if not today >= borrow.start:
        return False # to soon
    if borrow.state not in ["ACCEPTED"]:
        return False # wrong state
    if account != borrow.borrower:
        return False # only borrower
    if len(Rating.objects.filter(borrow=borrow, originator='BORROWER')):
        return False # already rated
    return True

def lender_rate(account, borrow, rating_value, note):
    if not lender_can_rate(account, borrow):
        raise PermissionDenied
    rating = Rating()
    rating.borrow = borrow
    rating.rating = rating_value
    rating.account = account
    rating.originator = "LENDER"
    rating.save()
    log(account, borrow, note, "LENDER_RATE")
    return _finish(borrow)

def borrower_rate(account, borrow, rating_value, note):
    if not borrower_can_rate(account, borrow):
        raise PermissionDenied
    rating = Rating()
    rating.borrow = borrow
    rating.rating = rating_value
    rating.account = account
    rating.originator = "BORROWER"
    rating.save()
    log(account, borrow, note, "BORROWER_RATE")
    return _finish(borrow)

###########
# EDITING #
###########

def lender_can_edit(account, borrow):
    today = datetime.datetime.now().date()
    if borrow.state not in ["REQUEST", "ACCEPTED"]:
        return False
    if borrow.start <= today:
        return False
    if account not in borrow.team.members.all():
        return False
    return True

def lender_can_edit_dest(account, borrow):
    if not lender_can_edit(account, borrow):
        return False
    if not borrow.active:
        return False
    return True

def borrower_can_edit(account, borrow):
    today = datetime.datetime.now().date()
    if borrow.state not in ["REQUEST", "ACCEPTED"]:
        return False
    if borrow.start <= today:
        return False
    if account != borrow.borrower:
        return False
    return True

def borrower_edit_is_allowed(account, borrow, start, finish, bike):
    if not borrower_can_edit(account, borrow):
        return False
    if not creation_is_allowed(bike, start, finish, exclude=borrow):
        return False
    if bike.team != borrow.team or not bike.active:
        return False
    return True

def lender_edit_bike_is_allowed(account, borrow, bike):
    if not lender_can_edit(account, borrow):
        return False
    if bike.team != borrow.team or not bike.active:
        return False
    if active_borrows_in_timeframe(bike, borrow.start, borrow.finish):
        return False
    return True

def lender_edit_dest_is_allowed(account, borrow, dest):
    if not lender_can_edit_dest(account, borrow):
        return False
    if dest.team != borrow.team or not dest.active:
        return False
    return True

def borrower_edit(account, borrow, start, finish, bike, note):
    if (borrow.start == start and borrow.finish == finish
            and borrow.bike == bike):
        return # nothing changed TODO throw error here, should never get this far!
    if not borrower_edit_is_allowed(account, borrow, start, finish, bike):
        raise PermissionDenied
    if borrow.active: # ensure borrow chain unbroken
        _remove_from_borrow_chain(borrow)
    borrow.start = start
    borrow.finish = finish
    borrow.bike = bike
    borrow.state = "REQUEST" # borrower edits require confirmation
    borrow.active = False
    borrow.save()
    log(account, borrow, note, "EDIT")

def lender_edit_dest(account, borrow, dest, note):
    if borrow.dest == dest:
        return # nothing changed TODO throw error here, should never get this far!
    if not lender_edit_dest_is_allowed(account, borrow, dest):
        raise PermissionDenied
    next_borrow = get_next_borrow(borrow.bike, borrow.finish)
    if next_borrow:
        next_borrow.src = dest
        next_borrow.save()
        log(None, borrow, "", "EDIT")
    borrow.dest = dest
    borrow.save()
    log(account, borrow, note, "EDIT")

def lender_edit_bike(account, borrow, bike, note):
    if borrow.bike == bike:
        return # nothing changed TODO throw error here, should never get this far!
    if not lender_edit_bike_is_allowed(account, borrow, bike):
        raise PermissionDenied
    _insert_into_borrow_chain(borrow, bike, account=account, note=note)

<commit_msg>order arrival and departure lists<commit_after># -*- coding: utf-8 -*-
# Copyright (c) 2012 Fabian Barkhau <fabian.barkhau@gmail.com>
# License: MIT (see LICENSE.TXT file)

import datetime
from django.core.exceptions import PermissionDenied
from django.utils.translation import ugettext as _
from django.dispatch import receiver
from apps.common.shortcuts import send_mail
from apps.borrow.models import Borrow
from apps.borrow.models import Rating
from apps.borrow.models import Log
from apps.borrow import signals
from apps.team import control as team_control
from apps.account import control as account_control

def _remove_from_borrow_chain(borrow):
    """ Pass the borrow being removed from the chain.
        Ensure that borrow chain src and dest stations always match.

        Before                      After
          x>1 - 1>B>2 - 2>x           x>1 -   -   - 1>x
        1>b>2 - 2>B>3 - 3>x         1>b>2 -   -   - 2>x
          x>1 - 1>B>2 - 2>b>3         x>1 -   -   - 1>b>3
        1>b>2 - 2>B>3 - 3>b>4       1>b>2 -   -   - 2>b>4
    """
    next_borrow = get_next_borrow(borrow.bike, borrow.finish)
    if next_borrow and next_borrow.src != borrow.src:
         next_borrow.src = borrow.src
         next_borrow.save()
         log(None, next_borrow, "", "EDIT")

def _insert_into_borrow_chain(borrow, bike, account=None, note=""):
    """ Pass the borrow being inserted into the chain.
        Ensure that borrow chain src and dest stations always match.

         jul     aug     sep         jul     aug     sep
        Before                      After
          x>1 -   -   - 1>x           x>1 - 1>B>1 - 1>x
        1>b>2 -   -   - 2>x         1>b>2 - 2>B>2 - 2>x
          x>1 -   -   - 1>b>3         x>1 - 1>B>1 - 1>b>3
        1>b>2 -   -   - 2>b>4       1>b>2 - 2>B>2 - 2>b>4
    """
    if borrow.active: # fix borrow chains
        _remove_from_borrow_chain(borrow) # remove from previous chain
        prev_borrow = get_prev_borrow(bike, borrow.start)
        borrow.src = prev_borrow and prev_borrow.dest or bike.station
        borrow.dest = borrow.src
    borrow.bike = bike
    borrow.save()
    log(account, borrow, note, "EDIT")

def log(account, borrow, note, action):
    if not account: # get site account if available
        account = account_control.get_site_account()
    l = Log()
    l.borrow = borrow
    l.initiator = account
    l.action = action
    l.note = note
    l.save()
    signals.borrow_log_created.send(sender=log, log=l)
    return l

##########
# EMAILS #
##########

def _get_email_templates(party, action):
    action = action.lower()
    party = party.lower()
    template = "borrow/email/notify_{party}_{action}_{part}.txt"
    return (
        template.format(party=party, action=action, part="subject"),
        template.format(party=party, action=action, part="message"),
    )

@receiver(signals.borrow_log_created)
def borrow_log_created_borrower_callback(sender, **kwargs):
    log = kwargs["log"]
    if log.borrow.borrower == log.initiator:
        return # no need to notify user of there own actions
    if log.action in ["FINISHED", "LENDER_RATE"]:
        return # no one cares, dont spam
    email = account_control.get_email_or_404(log.borrow.borrower)
    subject, message = _get_email_templates("borrower", log.action)
    send_mail([email], subject, message, kwargs)

@receiver(signals.borrow_log_created)
def borrow_log_created_lender_callback(sender, **kwargs):
    log = kwargs["log"]
    if log.action in ["FINISHED", "BORROWER_RATE"]:
        return # no one cares, dont spam
    if team_control.is_member(log.initiator, log.borrow.team):
        return # not need to notify team of its own actions
    emails = team_control.get_team_emails(log.borrow.team)
    subject, message = _get_email_templates("lender", log.action)
    send_mail(emails, subject, message, kwargs)

###########
# COMMENT #
###########

def can_comment(account, borrow):
    if account == borrow.borrower:
        return True
    if team_control.is_member(account, borrow.team):
        return True
    return False

def comment(account, borrow, note):
    if not can_comment(account, borrow):
        raise PermissionDenied
    log(account, borrow, note, "COMMENT")

##########
# QUERRY #
##########

def get_next_borrow(bike, finish):
    if not bike:
        return None
    qs = Borrow.objects.filter(active=True, bike=bike, start__gt=finish)
    borrows = list(qs.order_by("start")[:1]) # order and limit
    return borrows and borrows[0] or None

def get_prev_borrow(bike, start):
    if not bike:
        return None
    qs = Borrow.objects.filter(active=True, bike=bike, finish__lt=start)
    borrows = list(qs.order_by("-finish")[:1]) # order and limit
    return borrows and borrows[0] or None

def active_borrows_in_timeframe(bike, start, finish, exclude=None):
    """ Returns borrows in the given timeframe. finish is inclusive! """
    if not bike:
        raise Exception("BIKE REQUIRED!")
    qs = Borrow.objects.filter(bike=bike, active=True)
    qs = qs.exclude(start__gt=finish)
    qs = qs.exclude(finish__lt=start)
    if exclude:
        qs = qs.exclude(id=exclude.id)
    return list(qs)

def to_list_data(borrows, team_link=False):
    def borrow2entrie(borrow):
        base_url = team_link and ("/%s" % borrow.team.link) or ""
        src = borrow.src and borrow.src.street or None
        dest = borrow.dest and borrow.dest.street or None
        bike_name = borrow.bike and borrow.bike.name or _("DELETED")
        return {
            "labels" : [
                borrow.borrower, bike_name, borrow.start, borrow.finish,
                src, dest, borrow.state
            ],
            "url" : "%s/borrow/view/%s" % (base_url, borrow.id)
        }
    return {
        "columns" : [
            _("BORROWER"), _("BIKE"), _("DATE_FROM"), _("DATE_TO"),
            _("STATION_FROM"), _("STATION_TO"), _("BORROW_STATE"),
        ],
        "entries" : map(borrow2entrie, borrows)
    }

def arrivals(account):
    today = datetime.datetime.now().date()
    borrows = Borrow.objects.filter(
            active=True, finish__gte=today, dest__responsible=account
    )
    borrows = borrows.order_by("finish")
    return borrows

def departures(account):
    today = datetime.datetime.now().date()
    borrows = Borrow.objects.filter(
            active=True, start__gte=today, src__responsible=account
    )
    borrows = borrows.order_by("start")
    return borrows

##############
# RESPONDING #
##############

def can_respond(account, borrow):
    if borrow.state != "REQUEST":
        return False # borrow must be in request state
    if not team_control.is_member(account, borrow.team):
        return False # not a member
    return True

def response_is_allowed(account, borrow, state):
    if not can_respond(account, borrow):
        return False
    if state == "REJECTED":
        return True
    today = datetime.datetime.now().date()
    if borrow.finish <= today:
        return False # to late
    if not can_borrow(borrow.bike):
        return False
    if active_borrows_in_timeframe(borrow.bike, borrow.start, borrow.finish):
        return False
    return True

def respond(account, borrow, state, note):
    if not response_is_allowed(account, borrow, state):
        raise PermissionDenied
    borrow.state = state
    borrow.active = state != "REJECTED"
    if state != "REJECTED": # set stations
        prev_borrow = get_prev_borrow(borrow.bike, borrow.start)
        borrow.src = prev_borrow and prev_borrow.dest or borrow.bike.station
        borrow.dest = borrow.src
    borrow.save()
    log(account, borrow, note, "RESPOND")
    return borrow

#############
# CANCELING #
#############

def can_cancel(account, borrow):
    today = datetime.datetime.now().date()
    borrow_started = borrow.start <= datetime.datetime.now().date()
    borrow_ended = borrow.finish  < today
    is_lender = team_control.is_member(account, borrow.team)
    is_borrower = account == borrow.borrower
    lender_state = borrow.state in ["ACCEPTED"]
    borrower_state = borrow.state in ["REQUEST", "ACCEPTED"]
    return (is_borrower and borrower_state and not borrow_ended or
            is_lender and lender_state and not borrow_started)

def cancel(account, borrow, note):
    if not can_cancel(account, borrow):
        raise PermissionDenied
    borrow.state = "CANCELED"
    if borrow.active: # ensure borrow chain unbroken
        _remove_from_borrow_chain(borrow)
        borrow.src = None
        borrow.dest = None
    borrow.active = False
    borrow.save()
    log(account, borrow, note, "CANCEL")
    return borrow

#################
# CREATE BORROW #
#################

def can_borrow(bike):
    return (bike and bike.active and not bike.reserve and
            bike.station and bike.station.active)

def creation_is_allowed(bike, start, finish, exclude=None):
    if not can_borrow(bike):
        return False
    # check timeframe
    today = datetime.datetime.now().date()
    if start <= today:
        return False
    if finish < start:
        return False
    if len(active_borrows_in_timeframe(bike, start, finish, exclude=exclude)):
        return False
    return True

def create(account, bike, start, finish, note):
    if not account_control.has_required_info(account):
        raise PermissionDenied
    if not creation_is_allowed(bike, start, finish):
        raise PermissionDenied
    borrow = Borrow()
    borrow.bike = bike
    borrow.team = bike.team
    borrow.borrower = account
    borrow.start = start
    borrow.finish = finish
    borrow.active = False
    borrow.state = "REQUEST"
    borrow.save()
    log(account, borrow, note, "CREATE")
    return borrow

##########
# RATING #
##########

def _finish(borrow):
    if len(Rating.objects.filter(borrow=borrow)) != 2:
        return borrow # only finish when borrower and lender have rated
    borrow.state = "FINISHED"
    borrow.save()
    log(None, borrow, "", "FINISHED")
    return borrow

def lender_can_rate(account, borrow):
    today = datetime.datetime.now().date()
    if not today > borrow.finish:
        return False # to soon
    if borrow.state not in ["ACCEPTED"]:
        return False # wrong state
    if not team_control.is_member(account, borrow.team):
        return False # only members
    if len(Rating.objects.filter(borrow=borrow, originator='LENDER')):
        return False # already rated
    return True

def borrower_can_rate(account, borrow):
    today = datetime.datetime.now().date()
    if not today >= borrow.start:
        return False # to soon
    if borrow.state not in ["ACCEPTED"]:
        return False # wrong state
    if account != borrow.borrower:
        return False # only borrower
    if len(Rating.objects.filter(borrow=borrow, originator='BORROWER')):
        return False # already rated
    return True

def lender_rate(account, borrow, rating_value, note):
    if not lender_can_rate(account, borrow):
        raise PermissionDenied
    rating = Rating()
    rating.borrow = borrow
    rating.rating = rating_value
    rating.account = account
    rating.originator = "LENDER"
    rating.save()
    log(account, borrow, note, "LENDER_RATE")
    return _finish(borrow)

def borrower_rate(account, borrow, rating_value, note):
    if not borrower_can_rate(account, borrow):
        raise PermissionDenied
    rating = Rating()
    rating.borrow = borrow
    rating.rating = rating_value
    rating.account = account
    rating.originator = "BORROWER"
    rating.save()
    log(account, borrow, note, "BORROWER_RATE")
    return _finish(borrow)

###########
# EDITING #
###########

def lender_can_edit(account, borrow):
    today = datetime.datetime.now().date()
    if borrow.state not in ["REQUEST", "ACCEPTED"]:
        return False
    if borrow.start <= today:
        return False
    if account not in borrow.team.members.all():
        return False
    return True

def lender_can_edit_dest(account, borrow):
    if not lender_can_edit(account, borrow):
        return False
    if not borrow.active:
        return False
    return True

def borrower_can_edit(account, borrow):
    today = datetime.datetime.now().date()
    if borrow.state not in ["REQUEST", "ACCEPTED"]:
        return False
    if borrow.start <= today:
        return False
    if account != borrow.borrower:
        return False
    return True

def borrower_edit_is_allowed(account, borrow, start, finish, bike):
    if not borrower_can_edit(account, borrow):
        return False
    if not creation_is_allowed(bike, start, finish, exclude=borrow):
        return False
    if bike.team != borrow.team or not bike.active:
        return False
    return True

def lender_edit_bike_is_allowed(account, borrow, bike):
    if not lender_can_edit(account, borrow):
        return False
    if bike.team != borrow.team or not bike.active:
        return False
    if active_borrows_in_timeframe(bike, borrow.start, borrow.finish):
        return False
    return True

def lender_edit_dest_is_allowed(account, borrow, dest):
    if not lender_can_edit_dest(account, borrow):
        return False
    if dest.team != borrow.team or not dest.active:
        return False
    return True

def borrower_edit(account, borrow, start, finish, bike, note):
    if (borrow.start == start and borrow.finish == finish
            and borrow.bike == bike):
        return # nothing changed TODO throw error here, should never get this far!
    if not borrower_edit_is_allowed(account, borrow, start, finish, bike):
        raise PermissionDenied
    if borrow.active: # ensure borrow chain unbroken
        _remove_from_borrow_chain(borrow)
    borrow.start = start
    borrow.finish = finish
    borrow.bike = bike
    borrow.state = "REQUEST" # borrower edits require confirmation
    borrow.active = False
    borrow.save()
    log(account, borrow, note, "EDIT")

def lender_edit_dest(account, borrow, dest, note):
    if borrow.dest == dest:
        return # nothing changed TODO throw error here, should never get this far!
    if not lender_edit_dest_is_allowed(account, borrow, dest):
        raise PermissionDenied
    next_borrow = get_next_borrow(borrow.bike, borrow.finish)
    if next_borrow:
        next_borrow.src = dest
        next_borrow.save()
        log(None, borrow, "", "EDIT")
    borrow.dest = dest
    borrow.save()
    log(account, borrow, note, "EDIT")

def lender_edit_bike(account, borrow, bike, note):
    if borrow.bike == bike:
        return # nothing changed TODO throw error here, should never get this far!
    if not lender_edit_bike_is_allowed(account, borrow, bike):
        raise PermissionDenied
    _insert_into_borrow_chain(borrow, bike, account=account, note=note)

<|endoftext|>


<commit_before><commit_msg>removing id_token not needed<commit_after><|endoftext|>


<commit_before>"""
A Spawner for JupyterHub that runs each user's server in a separate docker container
"""
import itertools
import os
import string
from textwrap import dedent
from concurrent.futures import ThreadPoolExecutor
from pprint import pformat

import docker
from docker.errors import APIError
from docker.utils import kwargs_from_env
from tornado import gen

from escapism import escape
from jupyterhub.spawner import Spawner
from traitlets import (
    Dict,
    Unicode,
    Bool,
)

class UnicodeOrFalse(Unicode):
    info_text = 'a unicode string or False'
    def validate(self, obj, value):
        if value is False:
            return value
        return super(UnicodeOrFalse, self).validate(obj, value)

class DockerSpawner(Spawner):

    _executor = None
    @property
    def executor(self):
        """single global executor"""
        cls = self.__class__
        if cls._executor is None:
            cls._executor = ThreadPoolExecutor(1)
        return cls._executor

    _client = None
    @property
    def client(self):
        """single global client instance"""
        cls = self.__class__
        if cls._client is None:
            if self.use_docker_client_env:
                kwargs = kwargs_from_env(
                    assert_hostname=self.tls_assert_hostname
                )
                client = docker.Client(version='auto', **kwargs)
            else:
                if self.tls:
                    tls_config = True
                elif self.tls_verify or self.tls_ca or self.tls_client:
                    tls_config = docker.tls.TLSConfig(
                        client_cert=self.tls_client,
                        ca_cert=self.tls_ca,
                        verify=self.tls_verify,
                        assert_hostname = self.tls_assert_hostname)
                else:
                    tls_config = None

                docker_host = os.environ.get('DOCKER_HOST', 'unix://var/run/docker.sock')
                client = docker.Client(base_url=docker_host, tls=tls_config, version='auto')
            cls._client = client
        return cls._client

    container_id = Unicode()
    container_ip = Unicode('127.0.0.1', config=True)
    container_image = Unicode("jupyter/singleuser", config=True)
    container_prefix = Unicode(
        "jupyter",
        config=True,
        help=dedent(
            """
            Prefix for container names. The full container name for a particular
            user will be <prefix>-<username>.
            """
        )
    )

    volumes = Dict(
        config=True,
        help=dedent(
            """
            Map from host file/directory to container file/directory.
            Volumes specified here will be read/write in the container.
            If you use {username} in the host file / directory path, it will be
            replaced with the current user's name.
            """
        )
    )
    read_only_volumes = Dict(
        config=True,
        help=dedent(
            """
            Map from host file/directory to container file/directory.
            Volumes specified here will be read-only in the container.
            If you use {username} in the host file / directory path, it will be
            replaced with the current user's name.
            """
        )
    )

    use_docker_client_env = Bool(False, config=True, help="If True, will use Docker client env variable (boot2docker friendly)")
    tls = Bool(False, config=True, help="If True, connect to docker with --tls")
    tls_verify = Bool(False, config=True, help="If True, connect to docker with --tlsverify")
    tls_ca = Unicode("", config=True, help="Path to CA certificate for docker TLS")
    tls_cert = Unicode("", config=True, help="Path to client certificate for docker TLS")
    tls_key = Unicode("", config=True, help="Path to client key for docker TLS")
    tls_assert_hostname = UnicodeOrFalse(default_value=None, allow_none=True,
        config=True,
        help="If False, do not verify hostname of docker daemon",
    )

    remove_containers = Bool(False, config=True, help="If True, delete containers after they are stopped.")
    extra_create_kwargs = Dict(config=True, help="Additional args to pass for container create")
    extra_start_kwargs = Dict(config=True, help="Additional args to pass for container start")
    extra_host_config = Dict(config=True, help="Additional args to create_host_config for container create")

    _container_safe_chars = set(string.ascii_letters + string.digits + '-')
    _container_escape_char = '_'

    hub_ip_connect = Unicode(
        "",
        config=True,
        help=dedent(
            """
            If set, DockerSpawner will configure the containers to use
            the specified IP to connect the hub api.  This is useful
            when the hub_api is bound to listen on all ports or is
            running inside of a container.
            """
        )
    )

    use_internal_ip = Bool(
        False,
        config=True,
        help=dedent(
            """
            Enable the usage of the internal docker ip. This is useful if you are running
            jupyterhub (as a container) and the user containers within the same docker engine.
            E.g. by mounting the docker socket of the host into the jupyterhub container.
            """
        )
    )

    network_name = Unicode(
        "bridge",
        config=True,
        help=dedent(
            """
            The name of the docker network from which to retrieve the internal IP address. Defaults to the default
            docker network 'bridge'. You need to set this if you run your jupyterhub containers in a
            non-standard network. Only has an effect if use_internal_ip=True.
            """
        )
    )

    @property
    def tls_client(self):
        """A tuple consisting of the TLS client certificate and key if they
        have been provided, otherwise None.

        """
        if self.tls_cert and self.tls_key:
            return (self.tls_cert, self.tls_key)
        return None

    @property
    def volume_mount_points(self):
        """
        Volumes are declared in docker-py in two stages.  First, you declare
        all the locations where you're going to mount volumes when you call
        create_container.

        Returns a list of all the values in self.volumes or
        self.read_only_volumes.
        """
        return list(
            itertools.chain(
                self.volumes.values(),
                self.read_only_volumes.values(),
            )
        )

    @property
    def volume_binds(self):
        """
        The second half of declaring a volume with docker-py happens when you
        actually call start().  The required format is a dict of dicts that
        looks like:

        {
            host_location: {'bind': container_location, 'ro': True}
        }
        """
        volumes = {
            key.format(username=self.user.name): {'bind': value.format(username=self.user.name), 'ro': False}
            for key, value in self.volumes.items()
        }
        ro_volumes = {
            key.format(username=self.user.name): {'bind': value.format(username=self.user.name), 'ro': True}
            for key, value in self.read_only_volumes.items()
        }
        volumes.update(ro_volumes)
        return volumes

    _escaped_name = None
    @property
    def escaped_name(self):
        if self._escaped_name is None:
            self._escaped_name = escape(self.user.name,
                safe=self._container_safe_chars,
                escape_char=self._container_escape_char,
            )
        return self._escaped_name

    @property
    def container_name(self):
        return "{}-{}".format(self.container_prefix, self.escaped_name)

    def load_state(self, state):
        super(DockerSpawner, self).load_state(state)
        self.container_id = state.get('container_id', '')

    def get_state(self):
        state = super(DockerSpawner, self).get_state()
        if self.container_id:
            state['container_id'] = self.container_id
        return state

    def _public_hub_api_url(self):
        proto, path = self.hub.api_url.split('://', 1)
        ip, rest = path.split(':', 1)
        return '{proto}://{ip}:{rest}'.format(
            proto = proto,
            ip = self.hub_ip_connect,
            rest = rest
        )

    def _env_keep_default(self):
        """Don't inherit any env from the parent process"""
        return []

    def _env_default(self):
        env = super(DockerSpawner, self)._env_default()
        env.update(dict(
            JPY_USER=self.user.name,
            JPY_COOKIE_NAME=self.user.server.cookie_name,
            JPY_BASE_URL=self.user.server.base_url,
            JPY_HUB_PREFIX=self.hub.server.base_url
        ))

        if self.hub_ip_connect:
           hub_api_url = self._public_hub_api_url()
        else:
           hub_api_url = self.hub.api_url
        env['JPY_HUB_API_URL'] = hub_api_url

        return env

    def _docker(self, method, *args, **kwargs):
        """wrapper for calling docker methods

        to be passed to ThreadPoolExecutor
        """
        m = getattr(self.client, method)
        return m(*args, **kwargs)

    def docker(self, method, *args, **kwargs):
        """Call a docker method in a background thread

        returns a Future
        """
        return self.executor.submit(self._docker, method, *args, **kwargs)

    @gen.coroutine
    def poll(self):
        """Check for my id in `docker ps`"""
        container = yield self.get_container()
        if not container:
            self.log.warn("container not found")
            return ""

        container_state = container['State']
        self.log.debug(
            "Container %s status: %s",
            self.container_id[:7],
            pformat(container_state),
        )

        if container_state["Running"]:
            return None
        else:
            return (
                "ExitCode={ExitCode}, "
                "Error='{Error}', "
                "FinishedAt={FinishedAt}".format(**container_state)
            )

    @gen.coroutine
    def get_container(self):
        self.log.debug("Getting container '%s'", self.container_name)
        try:
            container = yield self.docker(
                'inspect_container', self.container_name
            )
            self.container_id = container['Id']
        except APIError as e:
            if e.response.status_code == 404:
                self.log.info("Container '%s' is gone", self.container_name)
                container = None
                # my container is gone, forget my id
                self.container_id = ''
            else:
                raise
        return container

    @gen.coroutine
    def start(self, image=None, extra_create_kwargs=None,
        extra_start_kwargs=None, extra_host_config=None):
        """Start the single-user server in a docker container. You can override
        the default parameters passed to `create_container` through the
        `extra_create_kwargs` dictionary and passed to `start` through the
        `extra_start_kwargs` dictionary.  You can also override the
        'host_config' parameter passed to `create_container` through the
        `extra_host_config` dictionary.

        Per-instance `extra_create_kwargs`, `extra_start_kwargs`, and
        `extra_host_config` take precedence over their global counterparts.

        """
        container = yield self.get_container()
        if container is None:
            image = image or self.container_image

            # build the dictionary of keyword arguments for create_container
            create_kwargs = dict(
                image=image,
                environment=self.env,
                volumes=self.volume_mount_points,
                name=self.container_name)
            create_kwargs.update(self.extra_create_kwargs)
            if extra_create_kwargs:
                create_kwargs.update(extra_create_kwargs)

            # build the dictionary of keyword arguments for host_config
            host_config = dict(binds=self.volume_binds)
            if not self.use_internal_ip:
                host_config['port_bindings'] = {8888: (self.container_ip,)}
            host_config.update(self.extra_host_config)
            if extra_host_config:
                host_config.update(extra_host_config)

            host_config = self.client.create_host_config(**host_config)
            create_kwargs.setdefault('host_config', {}).update(host_config)

            # create the container
            resp = yield self.docker('create_container', **create_kwargs)
            self.container_id = resp['Id']
            self.log.info(
                "Created container '%s' (id: %s) from image %s",
                self.container_name, self.container_id[:7], image)

        else:
            self.log.info(
                "Found existing container '%s' (id: %s)",
                self.container_name, self.container_id[:7])

        # TODO: handle unpause
        self.log.info(
            "Starting container '%s' (id: %s)",
            self.container_name, self.container_id[:7])

        # build the dictionary of keyword arguments for start
        start_kwargs = {}
        start_kwargs.update(self.extra_start_kwargs)
        if extra_start_kwargs:
            start_kwargs.update(extra_start_kwargs)

        # start the container
        yield self.docker('start', self.container_id, **start_kwargs)

        ip, port = yield from self.get_ip_and_port()
        self.user.server.ip = ip
        self.user.server.port = port

    def get_ip_and_port(self):
        if self.use_internal_ip:
            resp = yield self.docker('inspect_container', self.container_id)
            network_settings = resp['NetworkSettings']
            if 'Networks' in network_settings:
                ip = self.get_network_ip(network_settings)
            else:  # Fallback for old versions of docker (<1.9) without network management
                ip = network_settings['IPAddress']
            port = 8888
        else:
            resp = yield self.docker('port', self.container_id, 8888)
            ip = self.container_ip
            port = resp[0]['HostPort']
        return ip, port

    def get_network_ip(self, network_settings):
        networks = network_settings['Networks']
        if self.network_name not in networks:
            raise Exception(
                "Unknown docker network '{network}'. Did you create it with 'docker network create <name>' and "
                "did you pass network_mode=<name> in extra_kwargs?".format(
                    network=self.network_name
                )
            )
        network = networks[self.network_name]
        ip = network['IPAddress']
        return ip

    @gen.coroutine
    def stop(self, now=False):
        """Stop the container

        Consider using pause/unpause when docker-py adds support
        """
        self.log.info(
            "Stopping container %s (id: %s)",
            self.container_name, self.container_id[:7])
        yield self.docker('stop', self.container_id)

        if self.remove_containers:
            self.log.info(
                "Removing container %s (id: %s)",
                self.container_name, self.container_id[:7])
            # remove the container, as well as any associated volumes
            yield self.docker('remove_container', self.container_id, v=True)

        self.clear_state()
<commit_msg>Possibility of docker linking. Made some changes to how the config is build, because the dict update method does not work on nested dicts, but only updates the root level.<commit_after>"""
A Spawner for JupyterHub that runs each user's server in a separate docker container
"""
import itertools
import os
import string
from textwrap import dedent
from concurrent.futures import ThreadPoolExecutor
from pprint import pformat

import docker
from docker.errors import APIError
from docker.utils import kwargs_from_env
from tornado import gen

from escapism import escape
from jupyterhub.spawner import Spawner
from traitlets import (
    Dict,
    Unicode,
    Bool,
)

class UnicodeOrFalse(Unicode):
    info_text = 'a unicode string or False'
    def validate(self, obj, value):
        if value is False:
            return value
        return super(UnicodeOrFalse, self).validate(obj, value)

class DockerSpawner(Spawner):

    _executor = None
    @property
    def executor(self):
        """single global executor"""
        cls = self.__class__
        if cls._executor is None:
            cls._executor = ThreadPoolExecutor(1)
        return cls._executor

    _client = None
    @property
    def client(self):
        """single global client instance"""
        cls = self.__class__
        if cls._client is None:
            if self.use_docker_client_env:
                kwargs = kwargs_from_env(
                    assert_hostname=self.tls_assert_hostname
                )
                client = docker.Client(version='auto', **kwargs)
            else:
                if self.tls:
                    tls_config = True
                elif self.tls_verify or self.tls_ca or self.tls_client:
                    tls_config = docker.tls.TLSConfig(
                        client_cert=self.tls_client,
                        ca_cert=self.tls_ca,
                        verify=self.tls_verify,
                        assert_hostname = self.tls_assert_hostname)
                else:
                    tls_config = None

                docker_host = os.environ.get('DOCKER_HOST', 'unix://var/run/docker.sock')
                client = docker.Client(base_url=docker_host, tls=tls_config, version='auto')
            cls._client = client
        return cls._client

    container_id = Unicode()
    container_ip = Unicode('127.0.0.1', config=True)
    container_image = Unicode("jupyter/singleuser", config=True)
    container_prefix = Unicode(
        "jupyter",
        config=True,
        help=dedent(
            """
            Prefix for container names. The full container name for a particular
            user will be <prefix>-<username>.
            """
        )
    )

    volumes = Dict(
        config=True,
        help=dedent(
            """
            Map from host file/directory to container file/directory.
            Volumes specified here will be read/write in the container.
            If you use {username} in the host file / directory path, it will be
            replaced with the current user's name.
            """
        )
    )
    read_only_volumes = Dict(
        config=True,
        help=dedent(
            """
            Map from host file/directory to container file/directory.
            Volumes specified here will be read-only in the container.
            If you use {username} in the host file / directory path, it will be
            replaced with the current user's name.
            """
        )
    )

    use_docker_client_env = Bool(False, config=True, help="If True, will use Docker client env variable (boot2docker friendly)")
    tls = Bool(False, config=True, help="If True, connect to docker with --tls")
    tls_verify = Bool(False, config=True, help="If True, connect to docker with --tlsverify")
    tls_ca = Unicode("", config=True, help="Path to CA certificate for docker TLS")
    tls_cert = Unicode("", config=True, help="Path to client certificate for docker TLS")
    tls_key = Unicode("", config=True, help="Path to client key for docker TLS")
    tls_assert_hostname = UnicodeOrFalse(default_value=None, allow_none=True,
        config=True,
        help="If False, do not verify hostname of docker daemon",
    )

    remove_containers = Bool(False, config=True, help="If True, delete containers after they are stopped.")
    extra_create_kwargs = Dict(config=True, help="Additional args to pass for container create")
    extra_start_kwargs = Dict(config=True, help="Additional args to pass for container start")
    extra_host_config = Dict(config=True, help="Additional args to create_host_config for container create")

    _container_safe_chars = set(string.ascii_letters + string.digits + '-')
    _container_escape_char = '_'

    hub_ip_connect = Unicode(
        "",
        config=True,
        help=dedent(
            """
            If set, DockerSpawner will configure the containers to use
            the specified IP to connect the hub api.  This is useful
            when the hub_api is bound to listen on all ports or is
            running inside of a container.
            """
        )
    )

    use_internal_ip = Bool(
        False,
        config=True,
        help=dedent(
            """
            Enable the usage of the internal docker ip. This is useful if you are running
            jupyterhub (as a container) and the user containers within the same docker engine.
            E.g. by mounting the docker socket of the host into the jupyterhub container.
            """
        )
    )

    use_docker_links = Bool(
        False,
        config=True,
        help=dedent(
            """
            If the Jupyterhub is running in a Docker container, this can be used
            to connect using a Docker link, this simplifies the routing because
            all traffic will be using docker hostnames.
            """
        )
    )

    network_name = Unicode(
        "bridge",
        config=True,
        help=dedent(
            """
            The name of the docker network from which to retrieve the internal IP address. Defaults to the default
            docker network 'bridge'. You need to set this if you run your jupyterhub containers in a
            non-standard network. Only has an effect if use_internal_ip=True.
            """
        )
    )

    @property
    def tls_client(self):
        """A tuple consisting of the TLS client certificate and key if they
        have been provided, otherwise None.

        """
        if self.tls_cert and self.tls_key:
            return (self.tls_cert, self.tls_key)
        return None

    @property
    def volume_mount_points(self):
        """
        Volumes are declared in docker-py in two stages.  First, you declare
        all the locations where you're going to mount volumes when you call
        create_container.

        Returns a list of all the values in self.volumes or
        self.read_only_volumes.
        """
        return list(
            itertools.chain(
                self.volumes.values(),
                self.read_only_volumes.values(),
            )
        )

    @property
    def volume_binds(self):
        """
        The second half of declaring a volume with docker-py happens when you
        actually call start().  The required format is a dict of dicts that
        looks like:

        {
            host_location: {'bind': container_location, 'ro': True}
        }
        """
        volumes = {
            key.format(username=self.user.name): {'bind': value.format(username=self.user.name), 'ro': False}
            for key, value in self.volumes.items()
        }
        ro_volumes = {
            key.format(username=self.user.name): {'bind': value.format(username=self.user.name), 'ro': True}
            for key, value in self.read_only_volumes.items()
        }
        volumes.update(ro_volumes)
        return volumes

    _escaped_name = None
    @property
    def escaped_name(self):
        if self._escaped_name is None:
            self._escaped_name = escape(self.user.name,
                safe=self._container_safe_chars,
                escape_char=self._container_escape_char,
            )
        return self._escaped_name

    @property
    def container_name(self):
        return "{}-{}".format(self.container_prefix, self.escaped_name)

    def load_state(self, state):
        super(DockerSpawner, self).load_state(state)
        self.container_id = state.get('container_id', '')

    def get_state(self):
        state = super(DockerSpawner, self).get_state()
        if self.container_id:
            state['container_id'] = self.container_id
        return state

    def _public_hub_api_url(self):
        proto, path = self.hub.api_url.split('://', 1)
        ip, rest = path.split(':', 1)
        return '{proto}://{ip}:{rest}'.format(
            proto = proto,
            ip = self.hub_ip_connect,
            rest = rest
        )

    def _env_keep_default(self):
        """Don't inherit any env from the parent process"""
        return []

    def _env_default(self):
        env = super(DockerSpawner, self)._env_default()
        env.update(dict(
            JPY_USER=self.user.name,
            JPY_COOKIE_NAME=self.user.server.cookie_name,
            JPY_BASE_URL=self.user.server.base_url,
            JPY_HUB_PREFIX=self.hub.server.base_url
        ))

        if self.hub_ip_connect:
           hub_api_url = self._public_hub_api_url()
        else:
           hub_api_url = self.hub.api_url
        env['JPY_HUB_API_URL'] = hub_api_url

        return env

    def _docker(self, method, *args, **kwargs):
        """wrapper for calling docker methods

        to be passed to ThreadPoolExecutor
        """
        m = getattr(self.client, method)
        return m(*args, **kwargs)

    def docker(self, method, *args, **kwargs):
        """Call a docker method in a background thread

        returns a Future
        """
        return self.executor.submit(self._docker, method, *args, **kwargs)

    @gen.coroutine
    def poll(self):
        """Check for my id in `docker ps`"""
        container = yield self.get_container()
        if not container:
            self.log.warn("container not found")
            return ""

        container_state = container['State']
        self.log.debug(
            "Container %s status: %s",
            self.container_id[:7],
            pformat(container_state),
        )

        if container_state["Running"]:
            return None
        else:
            return (
                "ExitCode={ExitCode}, "
                "Error='{Error}', "
                "FinishedAt={FinishedAt}".format(**container_state)
            )

    @gen.coroutine
    def get_container(self):
        self.log.debug("Getting container '%s'", self.container_name)
        try:
            container = yield self.docker(
                'inspect_container', self.container_name
            )
            self.container_id = container['Id']
        except APIError as e:
            if e.response.status_code == 404:
                self.log.info("Container '%s' is gone", self.container_name)
                container = None
                # my container is gone, forget my id
                self.container_id = ''
            else:
                raise
        return container

    @gen.coroutine
    def start(self, image=None, extra_create_kwargs=None,
        extra_start_kwargs=None, extra_host_config=None):
        """Start the single-user server in a docker container. You can override
        the default parameters passed to `create_container` through the
        `extra_create_kwargs` dictionary and passed to `start` through the
        `extra_start_kwargs` dictionary.  You can also override the
        'host_config' parameter passed to `create_container` through the
        `extra_host_config` dictionary.

        Per-instance `extra_create_kwargs`, `extra_start_kwargs`, and
        `extra_host_config` take precedence over their global counterparts.

        """
        container = yield self.get_container()
        if container is None:
            image = image or self.container_image

            # build the dictionary of keyword arguments for create_container
            create_kwargs = dict(
                image=image,
                environment=self.env,
                volumes=self.volume_mount_points,
                name=self.container_name)
            create_kwargs.update(self.extra_create_kwargs)
            if extra_create_kwargs:
                create_kwargs.update(extra_create_kwargs)

            # build the dictionary of keyword arguments for host_config
            host_config = dict(binds=self.volume_binds, links={})

            if not self.use_internal_ip:
                host_config['port_bindings'] = {8888: (self.container_ip,)}

            host_config.update(self.extra_host_config)

            if extra_host_config:
                host_config.update(extra_host_config)

            if self.use_docker_links:
                containers = self.client.containers(quiet=True, filters={'com.jupyter.service': 'jupyterhub'})

                if len(containers) > 0:
                    container_id = containers[0]['Id']
                    host_config['links'].update({container_id: 'jupyterhub'})
                    self.log.info("Linked to container id %s", container_id)
                else:
                    self.log.warn("Could not find parent container id")

            self.log.debug("Starting host with config: %s", host_config)

            host_config = self.client.create_host_config(**host_config)
            create_kwargs.setdefault('host_config', {}).update(host_config)

            # create the container
            resp = yield self.docker('create_container', **create_kwargs)
            self.container_id = resp['Id']
            self.log.info(
                "Created container '%s' (id: %s) from image %s",
                self.container_name, self.container_id[:7], image)

        else:
            self.log.info(
                "Found existing container '%s' (id: %s)",
                self.container_name, self.container_id[:7])

        # TODO: handle unpause
        self.log.info(
            "Starting container '%s' (id: %s)",
            self.container_name, self.container_id[:7])

        # build the dictionary of keyword arguments for start
        start_kwargs = {}
        start_kwargs.update(self.extra_start_kwargs)
        if extra_start_kwargs:
            start_kwargs.update(extra_start_kwargs)

        # start the container
        yield self.docker('start', self.container_id, **start_kwargs)

        ip, port = yield from self.get_ip_and_port()
        self.user.server.ip = ip
        self.user.server.port = port

    def get_ip_and_port(self):
        if self.use_internal_ip:
            resp = yield self.docker('inspect_container', self.container_id)
            network_settings = resp['NetworkSettings']
            if 'Networks' in network_settings:
                ip = self.get_network_ip(network_settings)
            else:  # Fallback for old versions of docker (<1.9) without network management
                ip = network_settings['IPAddress']
            port = 8888
        else:
            resp = yield self.docker('port', self.container_id, 8888)
            ip = self.container_ip
            port = resp[0]['HostPort']
        return ip, port

    def get_network_ip(self, network_settings):
        networks = network_settings['Networks']
        if self.network_name not in networks:
            raise Exception(
                "Unknown docker network '{network}'. Did you create it with 'docker network create <name>' and "
                "did you pass network_mode=<name> in extra_kwargs?".format(
                    network=self.network_name
                )
            )
        network = networks[self.network_name]
        ip = network['IPAddress']
        return ip

    @gen.coroutine
    def stop(self, now=False):
        """Stop the container

        Consider using pause/unpause when docker-py adds support
        """
        self.log.info(
            "Stopping container %s (id: %s)",
            self.container_name, self.container_id[:7])
        yield self.docker('stop', self.container_id)

        if self.remove_containers:
            self.log.info(
                "Removing container %s (id: %s)",
                self.container_name, self.container_id[:7])
            # remove the container, as well as any associated volumes
            yield self.docker('remove_container', self.container_id, v=True)

        self.clear_state()
<|endoftext|>


<commit_before># Copyright (c) 2009-2010 Simon van Heeringen <s.vanheeringen@ncmls.ru.nl>
#
# This module is free software. You can redistribute it and/or modify it under
# the terms of the MIT License, see the file COPYING included with this
# distribution.

""" Parallel prediction of sequence motifs """

# Python imports
import sys
import logging
import os
import subprocess
# External imports
import pp
# GimmeMotifs imports
from gimmemotifs.tools import *
from gimmemotifs.comparison import *
from gimmemotifs.nmer_predict import *
from gimmemotifs.config import *

def pp_predict_motifs(fastafile, analysis="small", organism="hg18", single=False, background="", tools={}, job_server="", n_cpu=8):

	config = MotifConfig()

	if not tools:
		tools = dict([(x,1) for x in config.get_default_params["tools"].split(",")])

	logger = logging.getLogger('prediction.pp_predict_motifs')

	wmin = 5

	analysis_max = {"xs":5,"small":8, "medium":10,"large":14, "xl":20}
	wmax = analysis_max[analysis]

	if analysis == "xl":
		analysis = "large"

	if analysis == "xs":
		analysis = "small"

	if not job_server:
		job_server = pp.Server(n_cpu)

	result = []
	jobs = []

	# Some program specific code
	weeder_organism = {"hg18":"HS", "mm9":"MM", "yeast":"SC", "xenTro2":"XT"}[organism]
	ms_background = os.path.join(config.get_bg_dir(), "%s.%s.bg" % (organism, "MotifSampler"))

	# Start with longer running jobs
	if tools.has_key("MoAn") and tools["MoAn"]:
		moan = MoAn()
		logger.debug("Starting MoAn job")
		jobs.append(job_server.submit(moan.run, (fastafile, ".",{"analysis": analysis, "background":background}), (MotifProgram,),("gimmemotifs.config",)))
	else:
		logger.debug("Skipping MoAn")

	if tools.has_key("gadem") and tools["gadem"]:
		gadem = Gadem()
		logger.debug("Starting gadem job")
		jobs.append(job_server.submit(gadem.run, (fastafile, ".",{}), (MotifProgram,),("gimmemotifs.config",)))
	else:
		logger.debug("Skipping gadem")

	#step = 2
	# TODO: remove this!
	#if organism == "xenTro2":
	step = 1

	if tools.has_key("Weeder") and tools["Weeder"]:
		weeder = Weeder()
		logger.debug("Starting Weeder job, analysis %s" % analysis)
		jobs.append(job_server.submit(weeder.run, (fastafile, ".",{"analysis":analysis, "single":single}), (MotifProgram,),("gimmemotifs.config",)))
	else:
		logger.debug("Skipping Weeder")

	if tools.has_key("meme") and tools["meme"]:
		meme = Meme()

		# This check is necessary because meme errors and pp don't play nice together
		p = subprocess.Popen(meme.bin(),stdout=subprocess.PIPE,stderr=subprocess.PIPE)
		stdout,stderr = p.communicate()
		if not stderr:
			for i in range(wmin, wmax + 1, step):
				logger.debug("Starting Meme job, width %s" % i)
				jobs.append(job_server.submit(meme.run, (fastafile, ".",{"width":i, "single":single}), (MotifProgram,),("gimmemotifs.config",)))
		else:
			print "Error running meme: %s" % stderr
	else:
		logger.debug("Skipping meme")

	if  tools.has_key("MotifSampler") and tools["MotifSampler"]:
		motifsampler = MotifSampler()
		for i in range(wmin, wmax + 1, step):
	 		logger.debug("Starting MotifSampler job, width %s" % (i))
			jobs.append(job_server.submit(motifsampler.run, (fastafile, ".",{'width':i, 'background':ms_background, "single":single},), (MotifProgram,),("gimmemotifs.config",)))
	else:
		logger.debug("Skipping MotifSampler")

	if tools.has_key("trawler") and tools["trawler"]:
		trawler = Trawler()
		logger.debug("Starting trawler job, analysis %s" % analysis)
		jobs.append(job_server.submit(trawler.run, (fastafile, ".",{"analysis":analysis, "background":background, "single":single}), (MotifProgram,),("gimmemotifs.config",)))
	else:
		logger.debug("Skipping trawler")

	if tools.has_key("Improbizer") and tools["Improbizer"]:
		improbizer = Improbizer()
		logger.debug("Starting improbizer job")
		jobs.append(job_server.submit(improbizer.run, (fastafile, ".",{"analysis":analysis, "background":background}), (MotifProgram,),("gimmemotifs.config",)))
	else:
		logger.debug("Skipping Improbizer")

	if tools.has_key("MDmodule") and tools["MDmodule"]:
		mdmodule = MDmodule()
		for i in range(wmin, wmax + 1, step):
			logger.debug("Starting MDmodule job, width %s" % (i))
			jobs.append(job_server.submit(mdmodule.run, (fastafile, ".",{'width':i},), (MotifProgram,),("gimmemotifs.config",)))
	else:
		logger.debug("Skipping MDmodule")

	if tools.has_key("BioProspector") and tools["BioProspector"]:
		prospector = BioProspector()
		for i in range(wmin, wmax + 1, step):
			logger.debug("Starting BioProspector job, width %s" % (i))
			jobs.append(job_server.submit(prospector.run, (fastafile, ".",{'width':i, "background":background, "single":single},), (MotifProgram,),("gimmemotifs.config",)))
	else:
		logger.debug("Skipping BioProspector")

	if tools.has_key("WannaMotif") and tools["WannaMotif"]:
		logger.debug("Starting WannaMotif job")
		jobs.append(job_server.submit(nmer_predict, (fastafile,),(),()))

	motifs = []
	for job in jobs:
		ret = job()
		if ret:
			#print "pp_predict", ret
			(m, stdout, stderr) = ret
			if m and len(m) > 1:
				#print "pp_predict motif ", m

				motifs += m
			logger.debug(stdout)
			logger.debug(stderr)

	return motifs

<commit_msg>Added some organism mappings for Weeder<commit_after># Copyright (c) 2009-2010 Simon van Heeringen <s.vanheeringen@ncmls.ru.nl>
#
# This module is free software. You can redistribute it and/or modify it under
# the terms of the MIT License, see the file COPYING included with this
# distribution.

""" Parallel prediction of sequence motifs """

# Python imports
import sys
import logging
import os
import subprocess
# External imports
import pp
# GimmeMotifs imports
from gimmemotifs.tools import *
from gimmemotifs.comparison import *
from gimmemotifs.nmer_predict import *
from gimmemotifs.config import *

def pp_predict_motifs(fastafile, analysis="small", organism="hg18", single=False, background="", tools={}, job_server="", n_cpu=8):

	config = MotifConfig()

	if not tools:
		tools = dict([(x,1) for x in config.get_default_params["tools"].split(",")])

	logger = logging.getLogger('prediction.pp_predict_motifs')

	wmin = 5

	analysis_max = {"xs":5,"small":8, "medium":10,"large":14, "xl":20}
	wmax = analysis_max[analysis]

	if analysis == "xl":
		analysis = "large"

	if analysis == "xs":
		analysis = "small"

	if not job_server:
		job_server = pp.Server(n_cpu)

	result = []
	jobs = []

	# Some program specific code
	weeder_organism = {
		"hg18":"HS",
		"hg19":"HS",
		"mm9":"MM",
		"yeast":"SC",
		"sacCer2":"SC",
		"xenTro2":"XT"}[organism]

	ms_background = os.path.join(config.get_bg_dir(), "%s.%s.bg" % (organism, "MotifSampler"))

	# Start with longer running jobs
	if tools.has_key("MoAn") and tools["MoAn"]:
		moan = MoAn()
		logger.debug("Starting MoAn job")
		jobs.append(job_server.submit(moan.run, (fastafile, ".",{"analysis": analysis, "background":background}), (MotifProgram,),("gimmemotifs.config",)))
	else:
		logger.debug("Skipping MoAn")

	if tools.has_key("gadem") and tools["gadem"]:
		gadem = Gadem()
		logger.debug("Starting gadem job")
		jobs.append(job_server.submit(gadem.run, (fastafile, ".",{}), (MotifProgram,),("gimmemotifs.config",)))
	else:
		logger.debug("Skipping gadem")

	#step = 2
	# TODO: remove this!
	#if organism == "xenTro2":
	step = 1

	if tools.has_key("Weeder") and tools["Weeder"]:
		weeder = Weeder()
		logger.debug("Starting Weeder job, analysis %s" % analysis)
		jobs.append(job_server.submit(weeder.run, (fastafile, ".",{"analysis":analysis, "single":single}), (MotifProgram,),("gimmemotifs.config",)))
	else:
		logger.debug("Skipping Weeder")

	if tools.has_key("meme") and tools["meme"]:
		meme = Meme()

		# This check is necessary because meme errors and pp don't play nice together
		p = subprocess.Popen(meme.bin(),stdout=subprocess.PIPE,stderr=subprocess.PIPE)
		stdout,stderr = p.communicate()
		if not stderr:
			for i in range(wmin, wmax + 1, step):
				logger.debug("Starting Meme job, width %s" % i)
				jobs.append(job_server.submit(meme.run, (fastafile, ".",{"width":i, "single":single}), (MotifProgram,),("gimmemotifs.config",)))
		else:
			print "Error running meme: %s" % stderr
	else:
		logger.debug("Skipping meme")

	if  tools.has_key("MotifSampler") and tools["MotifSampler"]:
		motifsampler = MotifSampler()
		for i in range(wmin, wmax + 1, step):
	 		logger.debug("Starting MotifSampler job, width %s" % (i))
			jobs.append(job_server.submit(motifsampler.run, (fastafile, ".",{'width':i, 'background':ms_background, "single":single},), (MotifProgram,),("gimmemotifs.config",)))
	else:
		logger.debug("Skipping MotifSampler")

	if tools.has_key("trawler") and tools["trawler"]:
		trawler = Trawler()
		logger.debug("Starting trawler job, analysis %s" % analysis)
		jobs.append(job_server.submit(trawler.run, (fastafile, ".",{"analysis":analysis, "background":background, "single":single}), (MotifProgram,),("gimmemotifs.config",)))
	else:
		logger.debug("Skipping trawler")

	if tools.has_key("Improbizer") and tools["Improbizer"]:
		improbizer = Improbizer()
		logger.debug("Starting improbizer job")
		jobs.append(job_server.submit(improbizer.run, (fastafile, ".",{"analysis":analysis, "background":background}), (MotifProgram,),("gimmemotifs.config",)))
	else:
		logger.debug("Skipping Improbizer")

	if tools.has_key("MDmodule") and tools["MDmodule"]:
		mdmodule = MDmodule()
		for i in range(wmin, wmax + 1, step):
			logger.debug("Starting MDmodule job, width %s" % (i))
			jobs.append(job_server.submit(mdmodule.run, (fastafile, ".",{'width':i},), (MotifProgram,),("gimmemotifs.config",)))
	else:
		logger.debug("Skipping MDmodule")

	if tools.has_key("BioProspector") and tools["BioProspector"]:
		prospector = BioProspector()
		for i in range(wmin, wmax + 1, step):
			logger.debug("Starting BioProspector job, width %s" % (i))
			jobs.append(job_server.submit(prospector.run, (fastafile, ".",{'width':i, "background":background, "single":single},), (MotifProgram,),("gimmemotifs.config",)))
	else:
		logger.debug("Skipping BioProspector")

	if tools.has_key("WannaMotif") and tools["WannaMotif"]:
		logger.debug("Starting WannaMotif job")
		jobs.append(job_server.submit(nmer_predict, (fastafile,),(),()))

	motifs = []
	for job in jobs:
		ret = job()
		if ret:
			#print "pp_predict", ret
			(m, stdout, stderr) = ret
			if m and len(m) > 1:
				#print "pp_predict motif ", m

				motifs += m
			logger.debug(stdout)
			logger.debug(stderr)

	return motifs

<|endoftext|>


<commit_before># -*- coding: utf-8 -*-

from spambl import AddressListItem
from nose_parameterized import parameterized

class BaseValueTesterTest(object):

    classification = ('TEST',)

    def _get_expected_items(self, values):
        item = lambda i: AddressListItem(i, self.tested_instance,
                                             self.classification)
        return [item(v) for v in values]

class BaseHostListTest(BaseValueTesterTest):
    ''' A common test case for all classes that represent
    a host list stored locally or by a remote service '''

    invalid_host_input = [
                          ('ipv4', u'255.0.120.1.1'),
                          ('ipv6', '2001:db8:abcef:123::42'),
                          ('host', '-aaa')
                          ]

    valid_host_input = [
                        ('ipv4', u'255.0.120.1'),
                        ('hostname', 'test.pl')
                        ]

    valid_ipv6 = '2001:ddd:ccc:111::33'

    __get_expected_items = BaseValueTesterTest._get_expected_items

    def _test_function_for_invalid(self, function, value):

        self.host_factory_mock.side_effect = ValueError
        self.assertRaises(ValueError, function, value)

    @parameterized.expand(invalid_host_input)
    def test_contains_for_invalid(self, _, value):

        self._test_function_for_invalid(self.tested_instance.__contains__, value)

    @parameterized.expand(invalid_host_input)
    def test_lookup_for_invalid(self, _, value):
        self._test_function_for_invalid(self.tested_instance.lookup, value)

    def _test_contains_for_listed(self, value):

        self._set_matching_hosts(value)
        self.assertTrue(value in self.tested_instance)

    def _test_contains_not_for_listed(self, value):

        self.assertFalse(value in self.tested_instance)

    def _test_lookup_for_listed(self, value):

        expected = self.__get_expected_items([value])[0]
        self._set_matching_hosts(value)
        self.assertEqual(self.tested_instance.lookup(value), expected)

    def _test_lookup_for_not_listed(self, value):

        self.assertIsNone(self.tested_instance.lookup(value))

    @parameterized.expand(valid_host_input)
    def test_contains_for_listed(self, _, value):
        self._test_contains_for_listed(value)

    @parameterized.expand(valid_host_input)
    def test_contains_for_not_listed(self, _, value):
        self._test_contains_not_for_listed(value)

    @parameterized.expand(valid_host_input)
    def test_lookup_for_listed(self, _, value):
        self._test_lookup_for_listed(value)

    @parameterized.expand(valid_host_input)
    def test_lookup_for_not_listed(self, _, value):

        self._test_lookup_for_not_listed(value)
<commit_msg>Add HostListWithoutIpV6SupportTest class<commit_after># -*- coding: utf-8 -*-

from spambl import AddressListItem
from nose_parameterized import parameterized

class BaseValueTesterTest(object):

    classification = ('TEST',)

    def _get_expected_items(self, values):
        item = lambda i: AddressListItem(i, self.tested_instance,
                                             self.classification)
        return [item(v) for v in values]

class BaseHostListTest(BaseValueTesterTest):
    ''' A common test case for all classes that represent
    a host list stored locally or by a remote service '''

    invalid_host_input = [
                          ('ipv4', u'255.0.120.1.1'),
                          ('ipv6', '2001:db8:abcef:123::42'),
                          ('host', '-aaa')
                          ]

    valid_host_input = [
                        ('ipv4', u'255.0.120.1'),
                        ('hostname', 'test.pl')
                        ]

    valid_ipv6 = '2001:ddd:ccc:111::33'

    __get_expected_items = BaseValueTesterTest._get_expected_items

    def _test_function_for_invalid(self, function, value):

        self.host_factory_mock.side_effect = ValueError
        self.assertRaises(ValueError, function, value)

    @parameterized.expand(invalid_host_input)
    def test_contains_for_invalid(self, _, value):

        self._test_function_for_invalid(self.tested_instance.__contains__, value)

    @parameterized.expand(invalid_host_input)
    def test_lookup_for_invalid(self, _, value):
        self._test_function_for_invalid(self.tested_instance.lookup, value)

    def _test_contains_for_listed(self, value):

        self._set_matching_hosts(value)
        self.assertTrue(value in self.tested_instance)

    def _test_contains_not_for_listed(self, value):

        self.assertFalse(value in self.tested_instance)

    def _test_lookup_for_listed(self, value):

        expected = self.__get_expected_items([value])[0]
        self._set_matching_hosts(value)
        self.assertEqual(self.tested_instance.lookup(value), expected)

    def _test_lookup_for_not_listed(self, value):

        self.assertIsNone(self.tested_instance.lookup(value))

    @parameterized.expand(valid_host_input)
    def test_contains_for_listed(self, _, value):
        self._test_contains_for_listed(value)

    @parameterized.expand(valid_host_input)
    def test_contains_for_not_listed(self, _, value):
        self._test_contains_not_for_listed(value)

    @parameterized.expand(valid_host_input)
    def test_lookup_for_listed(self, _, value):
        self._test_lookup_for_listed(value)

    @parameterized.expand(valid_host_input)
    def test_lookup_for_not_listed(self, _, value):

        self._test_lookup_for_not_listed(value)

class HostListWithoutIpV6SupportTest(BaseHostListTest):
    ''' A test case for classes representing host list that
    raise an error when being queried for valid ip6 addresses '''

    def _test_function_raises_ValueError_for_valid_ipv6(self, function):

        self.assertRaises(ValueError, function, self.valid_ipv6)

    def test_contains_raises_ValueError_for_valid_ipv6(self):

        self._test_function_raises_ValueError_for_valid_ipv6(self.tested_instance.__contains__)

    def test_lookup_raises_ValueError_for_valid_ipv6(self):

        self._test_function_raises_ValueError_for_valid_ipv6(self.tested_instance.lookup)

<|endoftext|>


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

import base64
import logging
import re
import urlparse

import kcsapi

def reload_modules():
    reload(kcsapi)
    kcsapi.reload_modules()

KCSAPI_PATH_REGEX = re.compile(r'/kcsapi(?P<api_name>/.*)')
KCSAPI_PREFIX = 'svdata='

# UTF-8 Byte Order Mark.
BOM = '\xEF\xBB\xBF'

class KCSAPIHandler(object):

    def __init__(self, har_manager, debug):
        self._logger = logging.getLogger('kcaa.kcsapi_util')
        self.har_manager = har_manager
        self.debug = debug
        self.objects = {}
        self.define_handlers()

    def define_handlers(self):
        """Define KCSAPI handlers.

        The reason to define handlers here, not in a module level, is to
        refresh the handler class objects to reflect what's in source files
        under kcsapi/ directory.
        """
        # API URLs can be classified into 4 classes:
        # - /api_get_master/FOO: Get the general definition about FOO. User
        #                        independent.
        # - /api_get_member/FOO: Get the current status about FOO. User
        #                        dependent.
        # - /api_req_FOO/BAR: Make changes on FOO by doing an action BAR.
        #                     Doesn't necessarily deliver useful information
        #                     in the response (i.e. oftentimes the request
        #                     itself is important).
        # - Others.
        self.kcsapi_handlers = {
            # Initialization, or account information
            # /api_start2 now delivers most (all?) master information;
            # invariant data required for modelling, rendering and everything.
            # TODO: Support /api_start2. The basic plan is to delegate each
            # master handler to parse the subset of them, e.g. for
            # ShipDefinitionList, just parse response.api_data.api_mst_ship.
            '/api_auth_member/logincheck': [kcsapi.model.NullHandler()],
            '/api_req_member/get_incentive': [kcsapi.model.NullHandler()],
            '/api_start2': [kcsapi.mission.MissionList,
                            kcsapi.ship.ShipDefinitionList],
            # Encyclopedia
            '/api_get_member/book2': [kcsapi.model.NullHandler()],
            # Ships
            '/api_get_member/ship': [kcsapi.ship.ShipList],
            '/api_get_member/ship2': [kcsapi.ship.ShipList],
            '/api_get_member/ship3': [kcsapi.ship.ShipList],
            '/api_req_hensei/lock': [kcsapi.ship.ShipList],
            '/api_req_hokyu/charge': [kcsapi.ship.ShipList],
            '/api_req_kaisou/remodeling': [kcsapi.ship.ShipList],
            '/api_req_kousyou/getship': [kcsapi.ship.ShipList],
            # Port.
            # Like /api_start2, /api_port/port delivers most (all?) member
            # information; variant data for the player, ships, repair or
            # building docks.
            # TODO: Support all /api_port/port handlers.
            '/api_port/port': [kcsapi.ship.ShipList],
            '/api_get_member/deck': [kcsapi.fleet.FleetList,
                                     kcsapi.mission.MissionList],
            '/api_get_member/deck_port': [kcsapi.fleet.FleetList,
                                          kcsapi.mission.MissionList],
            # Fleets (deck).
            '/api_req_hensei/change': [kcsapi.model.NullHandler()],
            # Repair docks
            '/api_get_member/ndock': [kcsapi.repair.RepairDock],
            # Quests
            '/api_get_member/questlist': [kcsapi.quest.QuestList],
            '/api_req_quest/start': [kcsapi.model.NullHandler()],
            '/api_req_quest/stop': [kcsapi.model.NullHandler()],
            # Missions
            '/api_get_member/mission': [kcsapi.mission.MissionList],
            '/api_req_mission/start': [kcsapi.model.NullHandler()],
            '/api_req_mission/result': [kcsapi.model.NullHandler()],
            # Items
            #'/api_get_member/useitem': [],
            # Furnitures
            # Not interested in furniture configuration.
            '/api_get_member/furniture': [kcsapi.model.NullHandler()],
            # Maparea
            # Delivers only names.
            '/api_get_master/maparea': [kcsapi.model.NullHandler()],
            # Log
            # Almost useless.
            '/api_get_member/actionlog': [kcsapi.model.NullHandler()],
        }
        # Eager handlers accept all KCSAPI responses regardless of API URL.
        self.kcsapi_eager_handlers = [
            kcsapi.client.Screen,
        ]

    def serialize_objects(self):
        """Serialize objects so that the client can deserialize and restore the
        state."""
        return {key: [value.__class__.__module__, value.__class__.__name__,
                      value.json()]
                for key, value in self.objects.iteritems()}

    def deserialize_objects(self, serialized_objects):
        """Deserialize objects to restore the previous state.

        *IMPORTANT NOTE: this method uses :meth:`eval` to find the module (the
        first value in the list). Do not call this method with a data from an
        untrusted source.*
        """
        namespace = __name__.rpartition('.')[0]
        namespace = namespace + '.' if namespace else ''
        for key, value in serialized_objects.iteritems():
            module_name, class_name, text = value
            if module_name.startswith(namespace):
                module_name = module_name[len(namespace):]
            module = eval(module_name)
            cls = getattr(module, class_name)
            self.objects[key] = cls.parse_text(text)

    def get_kcsapi_responses(self, entries):
        for entry in entries:
            o = urlparse.urlparse(entry['request']['url'])
            match = KCSAPI_PATH_REGEX.match(o.path)
            if match:
                api_name = match.group('api_name')
                request = kcsapi.jsonobject.parse(
                    {param['name']: param['value'] for param in
                     entry['request']['postData']['params']},
                    readonly=True)
                content = entry['response']['content']
                text = content['text']
                # Highly likely the KCSAPI response is Base64 encoded, because
                # the API server doesn't attach charset information.
                encoding = content.get('encoding')
                if encoding == 'base64':
                    text = base64.b64decode(text)
                # Remove BOM if any.
                if text.startswith(BOM):
                    text = text[len(BOM):]
                # Skip response prefix which makes JSON parsing fail.
                if text.startswith(KCSAPI_PREFIX):
                    text = text[len(KCSAPI_PREFIX):]
                else:
                    self._logger.debug(
                        'Unexpected KCSAPI response got for {}'.format(o.path))
                    self._logger.debug(
                        'Raw response: {}'.format(text))
                    self._logger.debug('First 64 bytes: {}'.format(
                        ' '.join(('{:X}'.format(ord(c))) for c in text[:64])))
                    continue
                # KCSAPI response should be in UTF-8.
                response = kcsapi.jsonobject.parse_text(text, readonly=True,
                                                        encoding='utf8')
                yield api_name, request, response

    def dispatch(self, api_name, request, response):
        try:
            handlers = self.kcsapi_handlers[api_name]
            self._logger.debug('Accessed KCSAPI: {}'.format(api_name))
        except KeyError:
            handlers = [kcsapi.model.DefaultHandler(api_name)]
            self._logger.debug('Unknown KCSAPI:  {}'.format(api_name))
        for handler in handlers + self.kcsapi_eager_handlers:
            object_type = handler.__name__
            old_obj = self.objects.get(object_type)
            if not old_obj:
                obj = handler()
                # Handler may return None in case there is no need to handle
                # the KCSAPI response.
                if obj:
                    obj.update(api_name, request, response, self.objects,
                               self.debug)
                    self.objects[object_type] = obj
                    yield obj
            else:
                old_obj.update(api_name, request, response, self.objects,
                               self.debug)
                yield old_obj

    def get_updated_objects(self):
        entries = self.har_manager.get_updated_entries()
        if not entries:
            return
        for api_name, request, response in self.get_kcsapi_responses(entries):
            # Process only succeeded ones.
            if not response.api_result:
                self._logger.warn('KCSAPI request on {} failed.'.format(
                    api_name))
                continue
            for obj in self.dispatch(api_name, request, response):
                yield obj
<commit_msg>Remove maparea master handler.<commit_after>#!/usr/bin/env python

import base64
import logging
import re
import urlparse

import kcsapi

def reload_modules():
    reload(kcsapi)
    kcsapi.reload_modules()

KCSAPI_PATH_REGEX = re.compile(r'/kcsapi(?P<api_name>/.*)')
KCSAPI_PREFIX = 'svdata='

# UTF-8 Byte Order Mark.
BOM = '\xEF\xBB\xBF'

class KCSAPIHandler(object):

    def __init__(self, har_manager, debug):
        self._logger = logging.getLogger('kcaa.kcsapi_util')
        self.har_manager = har_manager
        self.debug = debug
        self.objects = {}
        self.define_handlers()

    def define_handlers(self):
        """Define KCSAPI handlers.

        The reason to define handlers here, not in a module level, is to
        refresh the handler class objects to reflect what's in source files
        under kcsapi/ directory.
        """
        # API URLs can be classified into 4 classes:
        # - /api_get_master/FOO: Get the general definition about FOO. User
        #                        independent.
        # - /api_get_member/FOO: Get the current status about FOO. User
        #                        dependent.
        # - /api_req_FOO/BAR: Make changes on FOO by doing an action BAR.
        #                     Doesn't necessarily deliver useful information
        #                     in the response (i.e. oftentimes the request
        #                     itself is important).
        # - Others.
        self.kcsapi_handlers = {
            # Initialization, or account information
            # /api_start2 now delivers most (all?) master information;
            # invariant data required for modelling, rendering and everything.
            # TODO: Support /api_start2. The basic plan is to delegate each
            # master handler to parse the subset of them, e.g. for
            # ShipDefinitionList, just parse response.api_data.api_mst_ship.
            '/api_auth_member/logincheck': [kcsapi.model.NullHandler()],
            '/api_req_member/get_incentive': [kcsapi.model.NullHandler()],
            '/api_start2': [kcsapi.mission.MissionList,
                            kcsapi.ship.ShipDefinitionList],
            # Encyclopedia
            '/api_get_member/book2': [kcsapi.model.NullHandler()],
            # Ships
            '/api_get_member/ship': [kcsapi.ship.ShipList],
            '/api_get_member/ship2': [kcsapi.ship.ShipList],
            '/api_get_member/ship3': [kcsapi.ship.ShipList],
            '/api_req_hensei/lock': [kcsapi.ship.ShipList],
            '/api_req_hokyu/charge': [kcsapi.ship.ShipList],
            '/api_req_kaisou/remodeling': [kcsapi.ship.ShipList],
            '/api_req_kousyou/getship': [kcsapi.ship.ShipList],
            # Port.
            # Like /api_start2, /api_port/port delivers most (all?) member
            # information; variant data for the player, ships, repair or
            # building docks.
            # TODO: Support all /api_port/port handlers.
            '/api_port/port': [kcsapi.ship.ShipList],
            '/api_get_member/deck': [kcsapi.fleet.FleetList,
                                     kcsapi.mission.MissionList],
            '/api_get_member/deck_port': [kcsapi.fleet.FleetList,
                                          kcsapi.mission.MissionList],
            # Fleets (deck).
            '/api_req_hensei/change': [kcsapi.model.NullHandler()],
            # Repair docks
            '/api_get_member/ndock': [kcsapi.repair.RepairDock],
            # Quests
            '/api_get_member/questlist': [kcsapi.quest.QuestList],
            '/api_req_quest/start': [kcsapi.model.NullHandler()],
            '/api_req_quest/stop': [kcsapi.model.NullHandler()],
            # Missions
            '/api_get_member/mission': [kcsapi.mission.MissionList],
            '/api_req_mission/start': [kcsapi.model.NullHandler()],
            '/api_req_mission/result': [kcsapi.model.NullHandler()],
            # Items
            #'/api_get_member/useitem': [],
            # Furnitures
            # Not interested in furniture configuration.
            '/api_get_member/furniture': [kcsapi.model.NullHandler()],
            # Log
            # Almost useless.
            '/api_get_member/actionlog': [kcsapi.model.NullHandler()],
        }
        # Eager handlers accept all KCSAPI responses regardless of API URL.
        self.kcsapi_eager_handlers = [
            kcsapi.client.Screen,
        ]

    def serialize_objects(self):
        """Serialize objects so that the client can deserialize and restore the
        state."""
        return {key: [value.__class__.__module__, value.__class__.__name__,
                      value.json()]
                for key, value in self.objects.iteritems()}

    def deserialize_objects(self, serialized_objects):
        """Deserialize objects to restore the previous state.

        *IMPORTANT NOTE: this method uses :meth:`eval` to find the module (the
        first value in the list). Do not call this method with a data from an
        untrusted source.*
        """
        namespace = __name__.rpartition('.')[0]
        namespace = namespace + '.' if namespace else ''
        for key, value in serialized_objects.iteritems():
            module_name, class_name, text = value
            if module_name.startswith(namespace):
                module_name = module_name[len(namespace):]
            module = eval(module_name)
            cls = getattr(module, class_name)
            self.objects[key] = cls.parse_text(text)

    def get_kcsapi_responses(self, entries):
        for entry in entries:
            o = urlparse.urlparse(entry['request']['url'])
            match = KCSAPI_PATH_REGEX.match(o.path)
            if match:
                api_name = match.group('api_name')
                request = kcsapi.jsonobject.parse(
                    {param['name']: param['value'] for param in
                     entry['request']['postData']['params']},
                    readonly=True)
                content = entry['response']['content']
                text = content['text']
                # Highly likely the KCSAPI response is Base64 encoded, because
                # the API server doesn't attach charset information.
                encoding = content.get('encoding')
                if encoding == 'base64':
                    text = base64.b64decode(text)
                # Remove BOM if any.
                if text.startswith(BOM):
                    text = text[len(BOM):]
                # Skip response prefix which makes JSON parsing fail.
                if text.startswith(KCSAPI_PREFIX):
                    text = text[len(KCSAPI_PREFIX):]
                else:
                    self._logger.debug(
                        'Unexpected KCSAPI response got for {}'.format(o.path))
                    self._logger.debug(
                        'Raw response: {}'.format(text))
                    self._logger.debug('First 64 bytes: {}'.format(
                        ' '.join(('{:X}'.format(ord(c))) for c in text[:64])))
                    continue
                # KCSAPI response should be in UTF-8.
                response = kcsapi.jsonobject.parse_text(text, readonly=True,
                                                        encoding='utf8')
                yield api_name, request, response

    def dispatch(self, api_name, request, response):
        try:
            handlers = self.kcsapi_handlers[api_name]
            self._logger.debug('Accessed KCSAPI: {}'.format(api_name))
        except KeyError:
            handlers = [kcsapi.model.DefaultHandler(api_name)]
            self._logger.debug('Unknown KCSAPI:  {}'.format(api_name))
        for handler in handlers + self.kcsapi_eager_handlers:
            object_type = handler.__name__
            old_obj = self.objects.get(object_type)
            if not old_obj:
                obj = handler()
                # Handler may return None in case there is no need to handle
                # the KCSAPI response.
                if obj:
                    obj.update(api_name, request, response, self.objects,
                               self.debug)
                    self.objects[object_type] = obj
                    yield obj
            else:
                old_obj.update(api_name, request, response, self.objects,
                               self.debug)
                yield old_obj

    def get_updated_objects(self):
        entries = self.har_manager.get_updated_entries()
        if not entries:
            return
        for api_name, request, response in self.get_kcsapi_responses(entries):
            # Process only succeeded ones.
            if not response.api_result:
                self._logger.warn('KCSAPI request on {} failed.'.format(
                    api_name))
                continue
            for obj in self.dispatch(api_name, request, response):
                yield obj
<|endoftext|>