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

import curses
from functools import partial
import signal
import time

__all__ = [
    'ctrl', 'main',
    'Game',
]

def ctrl(ch):
    return ord(ch) & 0x1f

def main(game_class, *args, **kws):
    stdscr = curses.initscr()
    try:
        game_class(stdscr, *args, **kws).go()
    finally:
        curses.endwin()

class Game(object):

    GAME_TITLE = NotImplemented

    def __init__(self, stdscr):
        self.stdscr = stdscr
        self.message = None
        self.message_timeout = None
        self.queue_redraw = True
        self.quit = False
        self.stopped = False

    def clear_message(self):
        '''
        Clears message line
        '''
        self.message = None
        self.message_timeout = None
        self.queue_redraw = True

    def set_message(self, msg, timeout = 1):
        '''
        Sets a message to display for the given timeout, in seconds.
        If timeout is None, the message will be displayed until clear_message
        is called.
        '''
        self.message = msg
        if timeout is None:
            self.message_timeout = None
        else:
            self.message_timeout = time.time() + timeout
        self.queue_redraw = True

    def draw(self):
        '''
        Draws the contents of the screen
        '''
        win = self.stdscr

        y, x = win.getmaxyx()
        win.clear()

        try:
            self.draw_title(y, x)

            if self.stopped:
                self.draw_stopped(y, x)
            else:
                self.draw_field(y, x)

            self.draw_message(y, x)
            self.refresh()
        except curses.error as e:
            msg = 'Screen is too small'
            self.draw_line(y - 1, 0, msg[:x], curses.A_BOLD)
            win.refresh()

    def draw_field(self, y, x):
        raise NotImplementedError

    def draw_message(self, y, x):
        '''Draws message'''
        win = self.stdscr
        if self.message:
            win.addstr(y - 1, 0, self.message[:x], curses.A_BOLD)

    def draw_stopped(self, y, x):
        raise NotImplementedError

    def draw_centered(self, y, x, s, attr = 0):
        '''
        Draws a string centered on the screen.
        y is line to draw, x is the max x value of the screen.
        '''
        self.stdscr.addstr(y, (x - len(s)) // 2, s, attr)

    def draw_line(self, y, x, s, attr = 0):
        self.stdscr.addstr(y, x, s, attr)

    def draw_title(self, y, x):
        '''Draws the title to the screen'''
        self.stdscr.addstr(0, 1, self.GAME_TITLE)
        self.stdscr.chgat(0, 0, x, curses.A_REVERSE)

    def go(self):
        self.init_ui()
        self.start_game()

        while not self.quit:
            self.before_tick()

            if self.queue_redraw:
                self.draw()
                self.queue_redraw = False

            self.poll_input()

            self.after_tick()

        self.end_game()

    def after_tick(self):
        if self.message_timeout and self.message_timeout <= time.time():
            self.clear_message()

    def before_tick(self):
        pass

    def end_game(self):
        pass

    def poll_input(self):
        ch = self.stdscr.getch()

        if ch != -1:
            self.handle_input(ch)

    def handle_input(self, ch):
        raise NotImplementedError

    def init_ui(self):
        self.stdscr.timeout(100)
        self.stdscr.keypad(True)
        curses.curs_set(0)
        curses.noecho()
        signal.signal(signal.SIGWINCH, self.win_resized)
        self.init_colors()

    def init_colors(self):
        curses.start_color()
        curses.use_default_colors()

    def new_game(self):
        raise NotImplementedError

    def quit_game(self):
        self.quit = True

    def start_game(self):
        raise NotImplementedError

    def redraw(self):
        self.queue_redraw = True

    def refresh(self):
        self.stdscr.refresh()

    def win_resized(self, *args):
        curses.endwin()
        curses.initscr()
        self.queue_redraw = True
<commit_msg>Fix for Windows<commit_after>#!/usr/bin/python3
# -*- coding: utf-8

import curses
from functools import partial
import signal
import time
import sys

__all__ = [
    'ctrl', 'main',
    'Game',
]

def ctrl(ch):
    return ord(ch) & 0x1f

def main(game_class, *args, **kws):
    stdscr = curses.initscr()
    try:
        game_class(stdscr, *args, **kws).go()
    finally:
        curses.endwin()

class Game(object):

    GAME_TITLE = NotImplemented

    def __init__(self, stdscr):
        self.stdscr = stdscr
        self.message = None
        self.message_timeout = None
        self.queue_redraw = True
        self.quit = False
        self.stopped = False

    def clear_message(self):
        '''
        Clears message line
        '''
        self.message = None
        self.message_timeout = None
        self.queue_redraw = True

    def set_message(self, msg, timeout = 1):
        '''
        Sets a message to display for the given timeout, in seconds.
        If timeout is None, the message will be displayed until clear_message
        is called.
        '''
        self.message = msg
        if timeout is None:
            self.message_timeout = None
        else:
            self.message_timeout = time.time() + timeout
        self.queue_redraw = True

    def draw(self):
        '''
        Draws the contents of the screen
        '''
        win = self.stdscr

        y, x = win.getmaxyx()
        win.clear()

        try:
            self.draw_title(y, x)

            if self.stopped:
                self.draw_stopped(y, x)
            else:
                self.draw_field(y, x)

            self.draw_message(y, x)
            self.refresh()
        except curses.error as e:
            msg = 'Screen is too small'
            self.draw_line(y - 1, 0, msg[:x], curses.A_BOLD)
            win.refresh()

    def draw_field(self, y, x):
        raise NotImplementedError

    def draw_message(self, y, x):
        '''Draws message'''
        win = self.stdscr
        if self.message:
            win.addstr(y - 1, 0, self.message[:x], curses.A_BOLD)

    def draw_stopped(self, y, x):
        raise NotImplementedError

    def draw_centered(self, y, x, s, attr = 0):
        '''
        Draws a string centered on the screen.
        y is line to draw, x is the max x value of the screen.
        '''
        self.stdscr.addstr(y, (x - len(s)) // 2, s, attr)

    def draw_line(self, y, x, s, attr = 0):
        self.stdscr.addstr(y, x, s, attr)

    def draw_title(self, y, x):
        '''Draws the title to the screen'''
        self.stdscr.addstr(0, 1, self.GAME_TITLE)
        self.stdscr.chgat(0, 0, x, curses.A_REVERSE)

    def go(self):
        self.init_ui()
        self.start_game()

        while not self.quit:
            self.before_tick()

            if self.queue_redraw:
                self.draw()
                self.queue_redraw = False

            self.poll_input()

            self.after_tick()

        self.end_game()

    def after_tick(self):
        if self.message_timeout and self.message_timeout <= time.time():
            self.clear_message()

    def before_tick(self):
        pass

    def end_game(self):
        pass

    def poll_input(self):
        ch = self.stdscr.getch()

        if ch != -1:
            self.handle_input(ch)

    def handle_input(self, ch):
        raise NotImplementedError

    def init_ui(self):
        self.stdscr.timeout(100)
        self.stdscr.keypad(True)
        curses.curs_set(0)
        curses.noecho()
        if not sys.platform.startswith('win'):
            signal.signal(signal.SIGWINCH, self.win_resized)
        self.init_colors()

    def init_colors(self):
        curses.start_color()
        curses.use_default_colors()

    def new_game(self):
        raise NotImplementedError

    def quit_game(self):
        self.quit = True

    def start_game(self):
        raise NotImplementedError

    def redraw(self):
        self.queue_redraw = True

    def refresh(self):
        self.stdscr.refresh()

    def win_resized(self, *args):
        curses.endwin()
        curses.initscr()
        self.queue_redraw = True
<|endoftext|>


<commit_before>#!/usr/bin/env python
# vim: set sts=4 sw=4 et:

import common
from common import CardSet
from common import NOLO, RAMI
from common import STOPPED, VOTING, ONGOING
from common import RuleError, GameError, GameState
import threading
import sys
import copy
import logging

class GameController(object):
    """
    The controller class that runs everything.
    """

    def __init__(self):
        super(GameController, self).__init__()
        self.players = []
        self.state = GameState()
        self.shutdown_event = threading.Event()

    def register_player(self, player):
        """
        Register a new Player.
        """
        if len(self.players) == 4:
            raise GameError('Already 4 players registered')

        self.players.append(player)
        player.id = self.players.index(player)
        player.team = player.id % 2

    def player_quit(self, player):
        """
        Player quits.
        """
        self._send_msg('Player %s quit' % player)
        self._reset()

    def _reset(self):
        """
        Reset the game.
        """
        logging.info('Resetting')
        self.players = []
        self.state = GameState()

    def _get_team_players(self, team):
        """
        Get players in a team.
        """
        return [player for player in self.players if player.team == team]

    def _get_team_str(self, team):
        """
        Get team string representation.
        """
        players = self._get_team_players(team)
        return '%d (%s and %s)' % (team + 1, players[0].player_name, players[1].player_name)

    def _send_msg(self, msg):
        """
        Send a message to all players.
        """
        logging.debug(msg)
        for player in self.players:
            player.send_message('', msg)

    def start_game(self):
        """
        Start the game.
        """
        if len(self.players) < 4:
            raise GameError('Not enough players')

        for player in self.players:
            player.start()

        self._start_new_hand()

    def wait_for_shutdown(self):
        """
        Wait for shutdown event and shut down after it.
        """
        self.shutdown_event.wait()
        self.shutdown()

    def _start_new_hand(self):
        """
        Start a new hand.
        """
        logging.info('New hand')
        self.state.tricks = [0, 0]

        # create a full deck
        deck = CardSet.new_full_deck()

        logging.debug('deck is %s' % str(deck))
        deck.shuffle()
        logging.debug('shuffled deck %s' % str(deck))

        deck.deal([player.hand for player in self.players])

        for player in self.players:
            player.hand.sort()
            logging.debug("%s's hand: %s" % (player.player_name,
                str(player.hand)))

        # voting
        self.state.mode = NOLO
        self.state.rami_chosen_by = None
        self.state.state = VOTING

        # uncomment following to skip voting
        #self.state.state = ONGOING
        # start the game
        self.state.next_in_turn(self.state.dealer + 1)
        self.players[self.state.turn].act(self, self.state)

    def _trick_played(self):
        """
        A trick (4 cards) has been played.
        """
        table = self.state.table
        high = table.get_cards(suit=table[0].suit).get_highest()
        team = high.played_by.team
        self._send_msg('Team %s takes this trick' % (self._get_team_str(team)))
        self.state.tricks[team] += 1
        self._send_msg('Tricks: %s' % self.state.tricks)
        self.state.table.clear()

        # do we have a winner?
        if self.state.tricks[0] + self.state.tricks[1] == 13:
            self._hand_played()
        else:
            self.state.next_in_turn(high.played_by.id)
            self.players[self.state.turn].act(self, self.state)

    def _hand_played(self):
        """
        A hand has been played.
        """

        if self.state.mode == NOLO:
            if self.state.tricks[0] < self.state.tricks[1]:
                winner = 0
                loser = 1
            else:
                winner = 1
                loser = 0
            score = (7 - self.state.tricks[winner]) * 4
        else:
            if self.state.tricks[0] > self.state.tricks[1]:
                winner = 0
                loser = 1
            else:
                winner = 1
                loser = 0
            if self.state.rami_chosen_by.team != winner:
                self._send_msg("Double points for taking opponent's rami!")
                score = (self.state.tricks[winner] - 6) * 8
            else:
                score = (self.state.tricks[winner] - 6) * 4

        self._send_msg('Team %s won this hand with %d tricks' %
                (self._get_team_str(winner), self.state.tricks[winner]))

        if self.state.score[loser] > 0:
            self._send_msg('Team %s fell down' % (self._get_team_str(loser)))
            self.state.score = [0, 0]
        else:
            self.state.score[winner] += score
            if self.state.score[winner] > 52:
                self._send_msg('Team %s won with score %d!' %
                        (self._get_team_str(winner), self.state.score[winner]))
                self.shutdown_event.set()
                return
            else:
                self._send_msg('Team %s is at %d' %
                        (self._get_team_str(winner), self.state.score[winner]))

        self.state.dealer = (self.state.dealer + 1) % 4
        self._start_new_hand()

    def shutdown(self):
        """
        Shutdown the game.
        """
        self.state.state = STOPPED
        for player in self.players:
            player.act(self, self.state)

        for player in self.players:
            if player.isAlive():
                player.join()

        sys.exit(0)

    def _vote_card(self, player, card):
        """
        Player votes with a card.
        """
        table = self.state.table

        card = copy.copy(card)
        card.played_by = player
        table.append(card)
        self.state.next_in_turn()
        if len(table) == 4:
            for card in table:
                # fire signals
                for plr in self.players:
                    plr.card_played(card.played_by, card, self.state)
                if card.suit == common.DIAMOND or card.suit == common.HEART:
                    self.state.mode = RAMI
                    self.state.rami_chosen_by = player
                    self.state.next_in_turn(card.played_by.id - 1)
                    break
            table.clear()
            if self.state.mode == NOLO:
                self._send_msg('Nolo it is')
            else:
                self._send_msg('Rami it is')
            self.state.state = ONGOING
            self._send_msg('Game on, %s begins!' % self.players[self.state.turn])

        self.players[self.state.turn].act(self, self.state)

    def play_card(self, player, card):
        """
        Play one card on the table.
        """
        if self.state.turn != player.id:
            raise RuleError('Not your turn')

        table = self.state.table

        if self.state.state == VOTING:
            self._vote_card(player, card)

        elif self.state.state == ONGOING:
            # make sure that suit is followed
            if len(table) > 0 and card.suit != table[0].suit:
                if len(player.hand.get_cards(suit=table[0].suit)) > 0:
                    raise RuleError('Suit must be followed')

            # make sure that the player actually has the card
            try:
                table.append(player.hand.take(card))
                card.played_by = player
            except ValueError:
                raise RuleError('Invalid card')

            # fire signals
            for plr in self.players:
                plr.card_played(player, card, self.state)

            if len(table) == 4:
                self._trick_played()
            else:
                self.state.next_in_turn()
                self.players[self.state.turn].act(self, self.state)

<commit_msg>fix voting signaling and logic<commit_after>#!/usr/bin/env python
# vim: set sts=4 sw=4 et:

import common
from common import CardSet
from common import NOLO, RAMI
from common import STOPPED, VOTING, ONGOING
from common import RuleError, GameError, GameState
import threading
import sys
import copy
import logging

class GameController(object):
    """
    The controller class that runs everything.
    """

    def __init__(self):
        super(GameController, self).__init__()
        self.players = []
        self.state = GameState()
        self.shutdown_event = threading.Event()

    def register_player(self, player):
        """
        Register a new Player.
        """
        if len(self.players) == 4:
            raise GameError('Already 4 players registered')

        self.players.append(player)
        player.id = self.players.index(player)
        player.team = player.id % 2

    def player_quit(self, player):
        """
        Player quits.
        """
        self._send_msg('Player %s quit' % player)
        self._reset()

    def _reset(self):
        """
        Reset the game.
        """
        logging.info('Resetting')
        self.players = []
        self.state = GameState()

    def _get_team_players(self, team):
        """
        Get players in a team.
        """
        return [player for player in self.players if player.team == team]

    def _get_team_str(self, team):
        """
        Get team string representation.
        """
        players = self._get_team_players(team)
        return '%d (%s and %s)' % (team + 1, players[0].player_name, players[1].player_name)

    def _send_msg(self, msg):
        """
        Send a message to all players.
        """
        logging.debug(msg)
        for player in self.players:
            player.send_message('', msg)

    def start_game(self):
        """
        Start the game.
        """
        if len(self.players) < 4:
            raise GameError('Not enough players')

        for player in self.players:
            player.start()

        self._start_new_hand()

    def wait_for_shutdown(self):
        """
        Wait for shutdown event and shut down after it.
        """
        self.shutdown_event.wait()
        self.shutdown()

    def _start_new_hand(self):
        """
        Start a new hand.
        """
        logging.info('New hand')
        self.state.tricks = [0, 0]

        # create a full deck
        deck = CardSet.new_full_deck()

        logging.debug('deck is %s' % str(deck))
        deck.shuffle()
        logging.debug('shuffled deck %s' % str(deck))

        deck.deal([player.hand for player in self.players])

        for player in self.players:
            player.hand.sort()
            logging.debug("%s's hand: %s" % (player.player_name,
                str(player.hand)))

        # voting
        self.state.mode = NOLO
        self.state.rami_chosen_by = None
        self.state.state = VOTING

        # uncomment following to skip voting
        #self.state.state = ONGOING
        # start the game
        self.state.next_in_turn(self.state.dealer + 1)
        self.players[self.state.turn].act(self, self.state)

    def _trick_played(self):
        """
        A trick (4 cards) has been played.
        """
        table = self.state.table
        high = table.get_cards(suit=table[0].suit).get_highest()
        team = high.played_by.team
        self._send_msg('Team %s takes this trick' % (self._get_team_str(team)))
        self.state.tricks[team] += 1
        self._send_msg('Tricks: %s' % self.state.tricks)
        self.state.table.clear()

        # do we have a winner?
        if self.state.tricks[0] + self.state.tricks[1] == 13:
            self._hand_played()
        else:
            self.state.next_in_turn(high.played_by.id)
            self.players[self.state.turn].act(self, self.state)

    def _hand_played(self):
        """
        A hand has been played.
        """

        if self.state.mode == NOLO:
            if self.state.tricks[0] < self.state.tricks[1]:
                winner = 0
                loser = 1
            else:
                winner = 1
                loser = 0
            score = (7 - self.state.tricks[winner]) * 4
        else:
            if self.state.tricks[0] > self.state.tricks[1]:
                winner = 0
                loser = 1
            else:
                winner = 1
                loser = 0
            if self.state.rami_chosen_by.team != winner:
                self._send_msg("Double points for taking opponent's rami!")
                score = (self.state.tricks[winner] - 6) * 8
            else:
                score = (self.state.tricks[winner] - 6) * 4

        self._send_msg('Team %s won this hand with %d tricks' %
                (self._get_team_str(winner), self.state.tricks[winner]))

        if self.state.score[loser] > 0:
            self._send_msg('Team %s fell down' % (self._get_team_str(loser)))
            self.state.score = [0, 0]
        else:
            self.state.score[winner] += score
            if self.state.score[winner] > 52:
                self._send_msg('Team %s won with score %d!' %
                        (self._get_team_str(winner), self.state.score[winner]))
                self.shutdown_event.set()
                return
            else:
                self._send_msg('Team %s is at %d' %
                        (self._get_team_str(winner), self.state.score[winner]))

        self.state.dealer = (self.state.dealer + 1) % 4
        self._start_new_hand()

    def shutdown(self):
        """
        Shutdown the game.
        """
        self.state.state = STOPPED
        for player in self.players:
            player.act(self, self.state)

        for player in self.players:
            if player.isAlive():
                player.join()

        sys.exit(0)

    def _vote_card(self, player, card):
        """
        Player votes with a card.
        """
        table = self.state.table

        card = copy.copy(card)
        card.played_by = player
        table.append(card)
        # fire signals
        for plr in self.players:
            plr.card_played(card.played_by, card, self.state)

        if card.suit == common.DIAMOND or card.suit == common.HEART:
            self.state.mode = RAMI
            self.state.rami_chosen_by = player
            self.state.next_in_turn(card.played_by.id - 1)
            self._begin_game()
        elif len(table) == 4:
            self.state.mode = NOLO
            self.state.next_in_turn()
            self._begin_game()
        else:
            self.state.next_in_turn()

        self.players[self.state.turn].act(self, self.state)

    def _begin_game(self):
        if self.state.mode == RAMI:
            self._send_msg('Rami it is')
        else:
            self._send_msg('Nolo it is')

        self._send_msg('Game on, %s begins!' % self.players[self.state.turn])

        self.state.table.clear()
        self.state.state = ONGOING

    def play_card(self, player, card):
        """
        Play one card on the table.
        """
        if self.state.turn != player.id:
            raise RuleError('Not your turn')

        table = self.state.table

        if self.state.state == VOTING:
            self._vote_card(player, card)

        elif self.state.state == ONGOING:
            # make sure that suit is followed
            if len(table) > 0 and card.suit != table[0].suit:
                if len(player.hand.get_cards(suit=table[0].suit)) > 0:
                    raise RuleError('Suit must be followed')

            # make sure that the player actually has the card
            try:
                table.append(player.hand.take(card))
                card.played_by = player
            except ValueError:
                raise RuleError('Invalid card')

            # fire signals
            for plr in self.players:
                plr.card_played(player, card, self.state)

            if len(table) == 4:
                self._trick_played()
            else:
                self.state.next_in_turn()
                self.players[self.state.turn].act(self, self.state)

<|endoftext|>


<commit_before>from collections import namedtuple, defaultdict
import copy
import itertools
import random

# rows: list of lists for top, middle, bottom rows
# draw: whatever has been drawn
# remaining: set of remaining cards
class PineappleGame1State:
  def __init__(self, rows, draw, remaining):
    self.rows = rows
    self.draw = draw
    self.remaining = remaining

'''
GAME CONSTANTS
'''
CARD_VALUES = 'VWXYZ23456789TJQKA'
DECK_CARD_VALUES = '23456789TJQKA'
HAND_ORDER = reversed(['RoFl', 'StFl', '4', '3+2', 'Fl', 'St', '3', '2+2', '2', '1'])
HAND_ORDER_DICT = {}
for i, hand_name in enumerate(HAND_ORDER):
  HAND_ORDER_DICT[hand_name] = i
MID_ROW_ROYALTIES = {
    '3': 2,
    'St': 4,
    'Fl': 8,
    '3+2': 12,
    '4': 20,
    'StFl': 30,
    'RoFl': 50
  }
MID_ROW_ROYALTIES = defaultdict(int, MID_ROW_ROYALTIES)
BOT_ROW_ROYALTIES = {
    'St': 2,
    'Fl': 4,
    '3+2': 6,
    '4': 10,
    'StFl': 15,
    'RoFl': 25
  }
BOT_ROW_ROYALTIES = defaultdict(int, BOT_ROW_ROYALTIES)
ROW_LENGTHS = [3, 5, 5]
NUM_ROWS = 3
FILL_CARDS = ['VF', 'WE', 'XE', 'YE', 'ZE']

'''
PARAMETERS
'''

# Simulates actuality of getting swept, with opponent having 20% bust chance
BUST_PENALTY = -6 * 0.8

'''
GLOBAL FUNCTIONS
'''

# Return the value of the card (from 2 to 14)
def card_value(card):
  return CARD_VALUES.index(card[0]) - 3

# Sort the provided cards according to rank. In increasing order if inc=True.
def sort_cards(cards, inc=True):
  if inc:
    return sorted(cards, lambda x, y: cmp(card_value(x), card_value(y)))
  else:
    return sorted(cards, lambda x, y: -cmp(card_value(x), card_value(y)))

# Turn a list of cards into a multiplicity table
def cards_to_mults(cards):
  cards = sort_cards(cards, inc=False)
  cur_streak = 1
  mults = []
  for i in range(len(cards) - 1):
    if cards[i][0] == cards[i+1][0]:
      cur_streak += 1
    else:
      mults += [(cur_streak, card_value(cards[i]))]
      cur_streak = 1
  mults += [(cur_streak, card_value(cards[-1]))]
  mults.sort(lambda x, y: -cmp(x, y))
  return mults

# Compute the hand associated with the given cards
# Hands are tuples with the form (hand_name, values...)
def compute_hand(cards):
  # Pad cards with filler cards if incomplete
  if len(cards) <= 3:
    deficit = 3 - len(cards)
  else:
    deficit = 5 - len(cards)
  cards = cards + FILL_CARDS[:deficit]

  # Sort cards descending
  cards = sort_cards(cards, False)
  mults = cards_to_mults(cards)

  def to_hand_tuple(hand_name, mults):
    return tuple([hand_name] + [x[1] for x in mults])

  if len(cards) > 3:
    is_straight = is_flush = False
    # Flush
    if all([card[1] == cards[0][1] for card in cards]):
      is_flush = True
    # Straight
    if len(mults) == 5:
      is_straight = True
      straight_mults = sorted(mults)
      for i in range(len(straight_mults) - 1):
        if straight_mults[i+1][1] != straight_mults[i][1] + 1:
          is_straight = False
          break
      if is_straight:
        straight_high = straight_mults[-1][1]
      # Wheel
      elif i == 3 and straight_mults[0][1] == 2 and straight_mults[-1][1] == 14:
        is_straight = True
        straight_high = 5
    if is_straight and is_flush:
      if straight_mults[0][1] == 10:
        return ("RoFl",)
      return ("StFl", straight_high)
    elif is_straight:
      return ("St", straight_high)
    elif is_flush:
      return to_hand_tuple("Fl", mults)

  # Check [NUM] of a kind hands
  if mults[0][0] == 4:
    hand_name = "4"
  elif mults[0][0] == 3:
    if len(mults) > 1 and mults[1][0] == 2:
      hand_name = "3+2"
    else:
      hand_name = "3"
  elif mults[0][0] == 2:
    if mults[1][0] == 2:
      hand_name = "2+2"
    else:
      hand_name = "2"
  else:
    hand_name = "1"
  return to_hand_tuple(hand_name, mults)

# Returns 1 if hand1 > hand2
# Returns -1 if hand1 < hand2
# Returns 0 on tie
def compare_hands(hand1, hand2):
  if HAND_ORDER_DICT[hand1[0]] > HAND_ORDER_DICT[hand2[0]]:
    return 1
  elif HAND_ORDER_DICT[hand1[0]] < HAND_ORDER_DICT[hand2[0]]:
    return -1
  else:
    min_len = min(len(hand1), len(hand2))
    return cmp(hand1[1:min_len], hand2[1:min_len])

# Compute the royalties associated with having the given hand on the given row
def royalties(hand, row):
  if row == 0:
    if hand[0] == '2':
      if hand[1] >= 6:
        return hand[1] - 5
    elif hand[0] == '3':
      return hand[1] + 8
  elif row == 1:
    return MID_ROW_ROYALTIES[hand[0]]
  elif row == 2:
    return BOT_ROW_ROYALTIES[hand[0]]
  else:
    raise RuntimeError("Invalid row: {}".format(row))
  return 0

# Returns whether or not the hands constitute a bust
def is_bust(hands):
  assert len(hands) == NUM_ROWS
  return compare_hands(hands[0], hands[1]) > 0 or compare_hands(hands[1], hands[2]) > 0

# Compute the total royalties earned from the provided rows
# Returns None on bust
def total_royalties(rows):
  assert len(rows) == NUM_ROWS
  for i in xrange(NUM_ROWS):
    assert len(rows[i]) == ROW_LENGTHS[i]
  hands = [compute_hand(cards) for cards in rows]
  if is_bust(hands):
    return None
  return sum(royalties(hand, row) for row, hand in enumerate(hands))

'''
GAME OBJECT
'''

class PineappleGame1(object):
  '''
  A game of Pineapple with only one player and opponent cards shown.
  '''
  def __init__(self):
    cards = [a + b for a, b in itertools.product(DECK_CARD_VALUES, 'CDHS')]
    self.deck_size = 52
    self.cards = set(cards)
    assert len(self.cards) == self.deck_size

  # Randomly choose an initial 5 card draw and create start state
  def get_start_state(self, hero_first):
    cards = copy.deepcopy(self.cards)
    if not hero_first:
      opponent_draw = random.sample(cards, 5)
      for card in opponent_draw:
        cards.remove(card)
    draw = random.sample(cards, 5)
    for card in draw:
      cards.remove(card)
    return PineappleGame1State(rows=[[], [], []], draw=draw, remaining=cards)

  def num_cards_played(self, state):
    return sum(len(x) for x in state.rows)

  def get_remaining_capacities(self, state):
    return [max_cards - len(row) for max_cards, row in zip(ROW_LENGTHS, state.rows)]

  # Returns a list of possible actions from the provided state. States are in the format:
  # ((card1, placement1), (card2, placement2))
  def actions(self, state):
    remaining_capacities = self.get_remaining_capacities(state)
    actions = []

    def find_assigns(i, num_cards, remaining_capacities, cur_assign):
      if i == num_cards:
        return [cur_assign]
      all_assigns = []
      for j in range(NUM_ROWS):
        if remaining_capacities[j] > 0:
          remaining_capacities[j] -= 1
          all_assigns += find_assigns(i+1, num_cards, remaining_capacities, cur_assign + [j])
          remaining_capacities[j] += 1
      return all_assigns

    num_to_play = 5 if len(state.draw) == 5 else 2
    placement_combos = find_assigns(0, num_to_play, remaining_capacities, [])
    for cards in itertools.combinations(state.draw, num_to_play):
      for placements in placement_combos:
        actions += [tuple(sorted([(card, placement) for card, placement in zip(cards, placements)]))]
    return actions

  # Returns whether the given state is terminal by checking for full rows
  def is_end(self, state):
    return all(len(state.rows[i]) == ROW_LENGTHS[i] for i in range(NUM_ROWS))

  # Utility function for certain policies to simulate the placement of cards according to
  # an action
  def sim_place_cards(self, state, action):
    state = copy.deepcopy(state)
    for card, placement in action:
      state.rows[placement] += [card]
    return state

  # Given the state and action, takes the action and then randomly simulates the drawing
  # of cards, returning a state.
  # Does not modify the provided state.
  # The input action does not need to be sorted.
  def get_random_outcome(self, state, action):
    state = copy.deepcopy(state)
    action = tuple(sorted(action))
    if action not in self.actions(state):
      raise RuntimeError("Illegal Action: {}".format(action))
    for card, placement in action:
      state.rows[placement] += [card]
    if len(action) == 5 and self.deck_size - len(state.remaining) == 5:
      opponent_draw = random.sample(state.remaining, 5)
    else:
      opponent_draw = random.sample(state.remaining, 2)
    for card in opponent_draw:
      state.remaining.remove(card)
    state.draw = random.sample(state.remaining, 3)
    for card in state.draw:
      state.remaining.remove(card)
    return state

  # Only call when is_end is true
  def utility(self, state):
    assert self.is_end(state)
    royalties = total_royalties(state.rows)
    if royalties is None:
      return BUST_PENALTY
    return royalties

  # Pretty print the state for display
  def print_state(self, state):
    discarded = []
    for card in self.cards:
      if card not in state.remaining and\
          not any([card in row or card in state.draw for row in state.rows]):
        discarded += [card]
    print 'Discard:', ' '.join(sort_cards(discarded))
    for row in state.rows:
      print '| ' + ' '.join(row)
    print 'Draw:', ' '.join(sorted(state.draw))<commit_msg>Bug fix for cards_to_mults, add SUITS.<commit_after>from collections import namedtuple, defaultdict
import copy
import itertools
import random

# rows: list of lists for top, middle, bottom rows
# draw: whatever has been drawn
# remaining: set of remaining cards
class PineappleGame1State:
  def __init__(self, rows, draw, remaining):
    self.rows = rows
    self.draw = draw
    self.remaining = remaining

'''
GAME CONSTANTS
'''
CARD_VALUES = 'VWXYZ23456789TJQKA'
DECK_CARD_VALUES = '23456789TJQKA'
SUITS = 'CDHS'
HAND_ORDER = reversed(['RoFl', 'StFl', '4', '3+2', 'Fl', 'St', '3', '2+2', '2', '1'])
HAND_ORDER_DICT = {}
for i, hand_name in enumerate(HAND_ORDER):
  HAND_ORDER_DICT[hand_name] = i
MID_ROW_ROYALTIES = {
    '3': 2,
    'St': 4,
    'Fl': 8,
    '3+2': 12,
    '4': 20,
    'StFl': 30,
    'RoFl': 50
  }
MID_ROW_ROYALTIES = defaultdict(int, MID_ROW_ROYALTIES)
BOT_ROW_ROYALTIES = {
    'St': 2,
    'Fl': 4,
    '3+2': 6,
    '4': 10,
    'StFl': 15,
    'RoFl': 25
  }
BOT_ROW_ROYALTIES = defaultdict(int, BOT_ROW_ROYALTIES)
ROW_LENGTHS = [3, 5, 5]
NUM_ROWS = 3
FILL_CARDS = ['VF', 'WE', 'XE', 'YE', 'ZE']

'''
PARAMETERS
'''

# Simulates actuality of getting swept, with opponent having 20% bust chance
BUST_PENALTY = -6 * 0.8

'''
GLOBAL FUNCTIONS
'''

# Return the value of the card (from 2 to 14)
def card_value(card):
  return CARD_VALUES.index(card[0]) - 3

# Sort the provided cards according to rank. In increasing order if inc=True.
def sort_cards(cards, inc=True):
  if inc:
    return sorted(cards, lambda x, y: cmp(card_value(x), card_value(y)))
  else:
    return sorted(cards, lambda x, y: -cmp(card_value(x), card_value(y)))

# Turn a list of cards into a multiplicity table
def cards_to_mults(cards):
  if len(cards) == 0:
    return []
  cards = sort_cards(cards, inc=False)
  cur_streak = 1
  mults = []
  for i in range(len(cards) - 1):
    if cards[i][0] == cards[i+1][0]:
      cur_streak += 1
    else:
      mults += [(cur_streak, card_value(cards[i]))]
      cur_streak = 1
  mults += [(cur_streak, card_value(cards[-1]))]
  mults.sort(lambda x, y: -cmp(x, y))
  return mults

# Compute the hand associated with the given cards
# Hands are tuples with the form (hand_name, values...)
def compute_hand(cards):
  # Pad cards with filler cards if incomplete
  if len(cards) <= 3:
    deficit = 3 - len(cards)
  else:
    deficit = 5 - len(cards)
  cards = cards + FILL_CARDS[:deficit]

  # Sort cards descending
  cards = sort_cards(cards, False)
  mults = cards_to_mults(cards)

  def to_hand_tuple(hand_name, mults):
    return tuple([hand_name] + [x[1] for x in mults])

  if len(cards) > 3:
    is_straight = is_flush = False
    # Flush
    if all([card[1] == cards[0][1] for card in cards]):
      is_flush = True
    # Straight
    if len(mults) == 5:
      is_straight = True
      straight_mults = sorted(mults)
      for i in range(len(straight_mults) - 1):
        if straight_mults[i+1][1] != straight_mults[i][1] + 1:
          is_straight = False
          break
      if is_straight:
        straight_high = straight_mults[-1][1]
      # Wheel
      elif i == 3 and straight_mults[0][1] == 2 and straight_mults[-1][1] == 14:
        is_straight = True
        straight_high = 5
    if is_straight and is_flush:
      if straight_mults[0][1] == 10:
        return ("RoFl",)
      return ("StFl", straight_high)
    elif is_straight:
      return ("St", straight_high)
    elif is_flush:
      return to_hand_tuple("Fl", mults)

  # Check [NUM] of a kind hands
  if mults[0][0] == 4:
    hand_name = "4"
  elif mults[0][0] == 3:
    if len(mults) > 1 and mults[1][0] == 2:
      hand_name = "3+2"
    else:
      hand_name = "3"
  elif mults[0][0] == 2:
    if mults[1][0] == 2:
      hand_name = "2+2"
    else:
      hand_name = "2"
  else:
    hand_name = "1"
  return to_hand_tuple(hand_name, mults)

# Returns 1 if hand1 > hand2
# Returns -1 if hand1 < hand2
# Returns 0 on tie
def compare_hands(hand1, hand2):
  if HAND_ORDER_DICT[hand1[0]] > HAND_ORDER_DICT[hand2[0]]:
    return 1
  elif HAND_ORDER_DICT[hand1[0]] < HAND_ORDER_DICT[hand2[0]]:
    return -1
  else:
    min_len = min(len(hand1), len(hand2))
    return cmp(hand1[1:min_len], hand2[1:min_len])

# Compute the royalties associated with having the given hand on the given row
def royalties(hand, row):
  if row == 0:
    if hand[0] == '2':
      if hand[1] >= 6:
        return hand[1] - 5
    elif hand[0] == '3':
      return hand[1] + 8
  elif row == 1:
    return MID_ROW_ROYALTIES[hand[0]]
  elif row == 2:
    return BOT_ROW_ROYALTIES[hand[0]]
  else:
    raise RuntimeError("Invalid row: {}".format(row))
  return 0

# Returns whether or not the hands constitute a bust
def is_bust(hands):
  assert len(hands) == NUM_ROWS
  return compare_hands(hands[0], hands[1]) > 0 or compare_hands(hands[1], hands[2]) > 0

# Compute the total royalties earned from the provided rows
# Returns None on bust
def total_royalties(rows):
  assert len(rows) == NUM_ROWS
  for i in xrange(NUM_ROWS):
    assert len(rows[i]) == ROW_LENGTHS[i]
  hands = [compute_hand(cards) for cards in rows]
  if is_bust(hands):
    return None
  return sum(royalties(hand, row) for row, hand in enumerate(hands))

'''
GAME OBJECT
'''

class PineappleGame1(object):
  '''
  A game of Pineapple with only one player and opponent cards shown.
  '''
  def __init__(self):
    cards = [a + b for a, b in itertools.product(DECK_CARD_VALUES, 'CDHS')]
    self.deck_size = 52
    self.cards = set(cards)
    assert len(self.cards) == self.deck_size

  # Randomly choose an initial 5 card draw and create start state
  def get_start_state(self, hero_first):
    cards = copy.deepcopy(self.cards)
    if not hero_first:
      opponent_draw = random.sample(cards, 5)
      for card in opponent_draw:
        cards.remove(card)
    draw = random.sample(cards, 5)
    for card in draw:
      cards.remove(card)
    return PineappleGame1State(rows=[[], [], []], draw=draw, remaining=cards)

  def num_cards_played(self, state):
    return sum(len(x) for x in state.rows)

  def get_remaining_capacities(self, state):
    return [max_cards - len(row) for max_cards, row in zip(ROW_LENGTHS, state.rows)]

  # Returns a list of possible actions from the provided state. States are in the format:
  # ((card1, placement1), (card2, placement2))
  def actions(self, state):
    remaining_capacities = self.get_remaining_capacities(state)
    actions = []

    def find_assigns(i, num_cards, remaining_capacities, cur_assign):
      if i == num_cards:
        return [cur_assign]
      all_assigns = []
      for j in range(NUM_ROWS):
        if remaining_capacities[j] > 0:
          remaining_capacities[j] -= 1
          all_assigns += find_assigns(i+1, num_cards, remaining_capacities, cur_assign + [j])
          remaining_capacities[j] += 1
      return all_assigns

    num_to_play = 5 if len(state.draw) == 5 else 2
    placement_combos = find_assigns(0, num_to_play, remaining_capacities, [])
    for cards in itertools.combinations(state.draw, num_to_play):
      for placements in placement_combos:
        actions += [tuple(sorted([(card, placement) for card, placement in zip(cards, placements)]))]
    return actions

  # Returns whether the given state is terminal by checking for full rows
  def is_end(self, state):
    return all(len(state.rows[i]) == ROW_LENGTHS[i] for i in range(NUM_ROWS))

  # Utility function for certain policies to simulate the placement of cards according to
  # an action
  def sim_place_cards(self, state, action):
    state = copy.deepcopy(state)
    for card, placement in action:
      state.rows[placement] += [card]
    return state

  # Given the state and action, takes the action and then randomly simulates the drawing
  # of cards, returning a state.
  # Does not modify the provided state.
  # The input action does not need to be sorted.
  def get_random_outcome(self, state, action):
    state = copy.deepcopy(state)
    action = tuple(sorted(action))
    if action not in self.actions(state):
      raise RuntimeError("Illegal Action: {}".format(action))
    for card, placement in action:
      state.rows[placement] += [card]
    if len(action) == 5 and self.deck_size - len(state.remaining) == 5:
      opponent_draw = random.sample(state.remaining, 5)
    else:
      opponent_draw = random.sample(state.remaining, 2)
    for card in opponent_draw:
      state.remaining.remove(card)
    state.draw = random.sample(state.remaining, 3)
    for card in state.draw:
      state.remaining.remove(card)
    return state

  # Only call when is_end is true
  def utility(self, state):
    assert self.is_end(state)
    royalties = total_royalties(state.rows)
    if royalties is None:
      return BUST_PENALTY
    return royalties

  # Pretty print the state for display
  def print_state(self, state):
    discarded = []
    for card in self.cards:
      if card not in state.remaining and\
          not any([card in row or card in state.draw for row in state.rows]):
        discarded += [card]
    print 'Discard:', ' '.join(sort_cards(discarded))
    for row in state.rows:
      print '| ' + ' '.join(row)
    print 'Draw:', ' '.join(sorted(state.draw))<|endoftext|>


<commit_before>"""
The code that should be run on Travis
"""

import os
import shlex
import shutil
import subprocess
import sys
import glob

from cryptography.fernet import Fernet

def decrypt_file(file, key):
    """
    Decrypts the file ``file``.

    The encrypted file is assumed to end with the ``.enc`` extension. The
    decrypted file is saved to the same location without the ``.enc``
    extension.

    The permissions on the decrypted file are automatically set to 0o600.

    See also :func:`doctr.local.encrypt_file`.

    """
    if not file.endswith('.enc'):
        raise ValueError("%s does not end with .enc" % file)

    fer = Fernet(key)

    with open(file, 'rb') as f:
        decrypted_file = fer.decrypt(f.read())

    with open(file[:-4], 'wb') as f:
        f.write(decrypted_file)

    os.chmod(file[:-4], 0o600)

def setup_deploy_key(keypath='github_deploy_key', key_ext='.enc'):
    """
    Decrypts the deploy key and configures it with ssh

    The key is assumed to be encrypted as keypath + key_ext, and the
    encryption key is assumed to be set in the environment variable
    DOCTR_DEPLOY_ENCRYPTION_KEY.

    """
    key = os.environ.get("DOCTR_DEPLOY_ENCRYPTION_KEY", None)
    if not key:
        raise RuntimeError("DOCTR_DEPLOY_ENCRYPTION_KEY environment variable is not set")

    key_filename = os.path.basename(keypath)
    key = key.encode('utf-8')
    decrypt_file(keypath + key_ext, key)

    key_path = os.path.expanduser("~/.ssh/" + key_filename)
    os.makedirs(os.path.expanduser("~/.ssh"), exist_ok=True)
    os.rename(keypath, key_path)

    with open(os.path.expanduser("~/.ssh/config"), 'a') as f:
        f.write("Host github.com"
                '  IdentityFile "%s"'
                "  LogLevel ERROR\n" % key_path)

    # start ssh-agent and add key to it
    # info from SSH agent has to be put into the environment
    agent_info = subprocess.check_output(['ssh-agent', '-s'])
    agent_info = agent_info.decode('utf-8')
    agent_info = agent_info.split()

    AUTH_SOCK = agent_info[0].split('=')[1][:-1]
    AGENT_PID = agent_info[3].split('=')[1][:-1]

    os.putenv('SSH_AUTH_SOCK', AUTH_SOCK)
    os.putenv('SSH_AGENT_PID', AGENT_PID)

    run(['ssh-add', os.path.expanduser('~/.ssh/' + key_filename)])

# XXX: Do this in a way that is streaming
def run_command_hiding_token(args, token):
    command = ' '.join(map(shlex.quote, args))
    command = command.replace(token.decode('utf-8'), '~'*len(token))
    print(command)
    p = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    out, err = p.stdout, p.stderr
    out = out.replace(token, b"~"*len(token))
    err = err.replace(token, b"~"*len(token))
    return (out, err, p.returncode)

def get_token():
    """
    Get the encrypted GitHub token in Travis

    Make sure the contents this variable do not link. The ``run()`` function
    will remove this from the output, so always use it.
    """
    token = os.environ.get("GH_TOKEN", None)
    if not token:
        raise RuntimeError("GH_TOKEN environment variable not set")
    token = token.encode('utf-8')
    return token

def run(args):
    """
    Run the command args

    Automatically hides the secret GitHub token from the output.
    """
    if "DOCTR_DEPLOY_ENCRYPTION_KEY" in os.environ:
        token = b''
    else:
        token = get_token()
    out, err, returncode = run_command_hiding_token(args, token)
    if out:
        print(out.decode('utf-8'))
    if err:
        print(err.decode('utf-8'), file=sys.stderr)
    if returncode != 0:
        sys.exit(returncode)

def get_current_repo():
    """
    Get the GitHub repo name for the current directory.

    Assumes that the repo is in the ``origin`` remote.
    """
    remote_url = subprocess.check_output(['git', 'config', '--get',
        'remote.origin.url']).decode('utf-8')

    # Travis uses the https clone url
    _, org, git_repo = remote_url.rsplit('.git', 1)[0].rsplit('/', 2)
    return (org + '/' + git_repo)

def setup_GitHub_push(deploy_repo, auth_type='deploy_key', full_key_path='github_deploy_key.enc', require_master=True):
    """
    Setup the remote to push to GitHub (to be run on Travis).

    ``auth_type`` should be either ``'deploy_key'`` or ``'token'``.

    For ``auth_type='token'``, this sets up the remote with the token and
    checks out the gh-pages branch. The token to push to GitHub is assumed to be in the ``GH_TOKEN`` environment
    variable.

    For ``auth_type='deploy_key'``, this sets up the remote with ssh access.
    """
    if auth_type not in ['deploy_key', 'token']:
        raise ValueError("auth_type must be 'deploy_key' or 'token'")

    TRAVIS_BRANCH = os.environ.get("TRAVIS_BRANCH", "")
    TRAVIS_PULL_REQUEST = os.environ.get("TRAVIS_PULL_REQUEST", "")

    if TRAVIS_BRANCH != "master" and require_master:
        print("The docs are only pushed to gh-pages from master. To allow pushing from "
        "a non-master branch, use the --no-require-master flag", file=sys.stderr)
        print("This is the {TRAVIS_BRANCH} branch".format(TRAVIS_BRANCH=TRAVIS_BRANCH), file=sys.stderr)
        return False

    if TRAVIS_PULL_REQUEST != "false":
        print("The website and docs are not pushed to gh-pages on pull requests", file=sys.stderr)
        return False

    print("Setting git attributes")
    # Should we add some user.email?
    run(['git', 'config', '--global', 'user.name', "Doctr (Travis CI)"])

    remotes = subprocess.check_output(['git', 'remote']).decode('utf-8').split('\n')
    if 'doctr_remote' in remotes:
        print("doctr_remote already exists")
    else:
        print("Adding doctr remote")
        if auth_type == 'token':
            token = get_token()
            run(['git', 'remote', 'add', 'doctr_remote',
                'https://{token}@github.com/{deploy_repo}.git'.format(token=token.decode('utf-8'),
                    deploy_repo=deploy_repo)])
        else:
            keypath, key_ext = full_key_path.rsplit('.', 1)
            key_ext = '.' + key_ext
            setup_deploy_key(keypath=keypath, key_ext=key_ext)
            run(['git', 'remote', 'add', 'doctr_remote',
                'git@github.com:{deploy_repo}.git'.format(deploy_repo=deploy_repo)])

    print("Fetching doctr remote")
    run(['git', 'fetch', 'doctr_remote'])

    #create gh-pages empty branch with .nojekyll if it doesn't already exist
    new_gh_pages = create_gh_pages()
    print("Checking out gh-pages")
    if new_gh_pages:
        run(['git', 'checkout', 'gh-pages'])
    else:
        run(['git', 'checkout', '-b', 'gh-pages', '--track', 'doctr_remote/gh-pages'])
    print("Done")

    return True

def gh_pages_exists():
    """
    Check if there is a remote gh-pages branch.

    This isn't completely robust. If there are multiple remotes and you have a
    ``gh-pages`` branch on the non-default remote, this won't see it.

    """
    remote_name = 'doctr_remote'
    branch_names = subprocess.check_output(['git', 'branch', '-r']).decode('utf-8').split()

    return '{}/gh-pages'.format(remote_name) in branch_names

def create_gh_pages():
    """
    If there is no remote ``gh-pages`` branch, create one.

    Return True if ``gh-pages`` was created, False if not.
    """
    if not gh_pages_exists():
        print("Creating gh-pages branch")
        run(['git', 'checkout', '--orphan', 'gh-pages'])
        # delete everything in the new ref.  this is non-destructive to existing
        # refs/branches, etc...
        run(['git', 'rm', '-rf', '.'])
        print("Adding .nojekyll file to gh-pages branch")
        run(['touch', '.nojekyll'])
        run(['git', 'add', '.nojekyll'])
        run(['git', 'commit', '-m', 'Create new gh-pages branch with .nojekyll'])
        print("Pushing gh-pages branch to remote")
        run(['git', 'push', '-u', 'doctr_remote', 'gh-pages'])
        # return to master branch
        run(['git', 'checkout', '-'])

        return True
    return False

def find_sphinx_build_dir():
    """
    Find build subfolder within sphinx docs directory.

    This is called by :func:`commit_docs` if keyword arg ``built_docs`` is not
    specified on the command line.
    """
    build = glob.glob('**/*build/html', recursive=True)
    if not build:
        raise RuntimeError("Could not find Sphinx build directory automatically")
    build_folder = build[0]

    return build_folder

# Here is the logic to get the Travis job number, to only run commit_docs in
# the right build.
#
# TRAVIS_JOB_NUMBER = os.environ.get("TRAVIS_JOB_NUMBER", '')
# ACTUAL_TRAVIS_JOB_NUMBER = TRAVIS_JOB_NUMBER.split('.')[1]

def sync_from_log(src, dst, log_file):
    """
    Sync the files in ``src`` to ``dst``.

    The files that are synced are logged to ``log_file``. If ``log_file``
    exists, the files in ``log_file`` are removed first.

    Returns ``(added, removed)``, where added is a list of all files synced from
    ``src`` (even if it already existed in ``dst``), and ``removed`` is every
    file from ``log_file`` that was removed from ``dst`` because it wasn't in
    ``src``. ``added`` also includes the log file.
    """
    from os.path import join, exists, isdir
    if not src.endswith(os.sep):
        src += os.sep

    added, removed = [], []

    if not exists(log_file):
        # Assume this is the first run
        print("%s doesn't exist. Not removing any files." % log_file)
    else:
        with open(log_file) as f:
            files = f.read().strip().split('\n')

        for new_f in files:
            new_f = new_f.strip()
            if exists(new_f):
                os.remove(new_f)
                removed.append(new_f)
            else:
                print("Warning: File %s doesn't exist." % new_f, file=sys.stderr)

    files = glob.iglob(join(src, '**'), recursive=True)
    # sorted makes this easier to test
    for f in sorted(files):
        new_f = join(dst, f[len(src):])
        if isdir(f):
            os.makedirs(new_f, exist_ok=True)
        else:
            shutil.copy2(f, new_f)
            added.append(new_f)
            if new_f in removed:
                removed.remove(new_f)

    with open(log_file, 'w') as f:
        f.write('\n'.join(added))

    added.append(log_file)

    return added, removed

def commit_docs(*, added, removed):
    """
    Commit the docs to ``gh-pages``

    Assumes that :func:`setup_GitHub_push`, which sets up the ``doctr_remote``
    remote, has been run and returned True.

    Returns True if changes were committed and False if no changes were
    committed.
    """
    TRAVIS_BUILD_NUMBER = os.environ.get("TRAVIS_BUILD_NUMBER", "<unknown>")

    for f in added:
        run(['git', 'add', f])
    for f in removed:
        run(['git', 'rm', f])

    # Only commit if there were changes
    if subprocess.run(['git', 'diff-index', '--quiet', 'HEAD', '--'],
        stdout=subprocess.PIPE, stderr=subprocess.PIPE).returncode != 0:
        print("Committing")
        run(['git', 'commit', '-am', "Update docs after building Travis build " + TRAVIS_BUILD_NUMBER])
        return True

    return False

def push_docs():
    """
    Push the changes to the `gh-pages` branch.

    Assumes that :func:`setup_GitHub_push` has been run and returned True, and
    that :func:`commit_docs` has been run. Does not push anything if no changes
    were made.

    """

    print("Pulling")
    run(["git", "pull"])
    print("Pushing commit")
    run(['git', 'push', '-q', 'doctr_remote', 'gh-pages'])
<commit_msg>Remote the doctr_remote if it already exists<commit_after>"""
The code that should be run on Travis
"""

import os
import shlex
import shutil
import subprocess
import sys
import glob

from cryptography.fernet import Fernet

def decrypt_file(file, key):
    """
    Decrypts the file ``file``.

    The encrypted file is assumed to end with the ``.enc`` extension. The
    decrypted file is saved to the same location without the ``.enc``
    extension.

    The permissions on the decrypted file are automatically set to 0o600.

    See also :func:`doctr.local.encrypt_file`.

    """
    if not file.endswith('.enc'):
        raise ValueError("%s does not end with .enc" % file)

    fer = Fernet(key)

    with open(file, 'rb') as f:
        decrypted_file = fer.decrypt(f.read())

    with open(file[:-4], 'wb') as f:
        f.write(decrypted_file)

    os.chmod(file[:-4], 0o600)

def setup_deploy_key(keypath='github_deploy_key', key_ext='.enc'):
    """
    Decrypts the deploy key and configures it with ssh

    The key is assumed to be encrypted as keypath + key_ext, and the
    encryption key is assumed to be set in the environment variable
    DOCTR_DEPLOY_ENCRYPTION_KEY.

    """
    key = os.environ.get("DOCTR_DEPLOY_ENCRYPTION_KEY", None)
    if not key:
        raise RuntimeError("DOCTR_DEPLOY_ENCRYPTION_KEY environment variable is not set")

    key_filename = os.path.basename(keypath)
    key = key.encode('utf-8')
    decrypt_file(keypath + key_ext, key)

    key_path = os.path.expanduser("~/.ssh/" + key_filename)
    os.makedirs(os.path.expanduser("~/.ssh"), exist_ok=True)
    os.rename(keypath, key_path)

    with open(os.path.expanduser("~/.ssh/config"), 'a') as f:
        f.write("Host github.com"
                '  IdentityFile "%s"'
                "  LogLevel ERROR\n" % key_path)

    # start ssh-agent and add key to it
    # info from SSH agent has to be put into the environment
    agent_info = subprocess.check_output(['ssh-agent', '-s'])
    agent_info = agent_info.decode('utf-8')
    agent_info = agent_info.split()

    AUTH_SOCK = agent_info[0].split('=')[1][:-1]
    AGENT_PID = agent_info[3].split('=')[1][:-1]

    os.putenv('SSH_AUTH_SOCK', AUTH_SOCK)
    os.putenv('SSH_AGENT_PID', AGENT_PID)

    run(['ssh-add', os.path.expanduser('~/.ssh/' + key_filename)])

# XXX: Do this in a way that is streaming
def run_command_hiding_token(args, token):
    command = ' '.join(map(shlex.quote, args))
    command = command.replace(token.decode('utf-8'), '~'*len(token))
    print(command)
    p = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    out, err = p.stdout, p.stderr
    out = out.replace(token, b"~"*len(token))
    err = err.replace(token, b"~"*len(token))
    return (out, err, p.returncode)

def get_token():
    """
    Get the encrypted GitHub token in Travis

    Make sure the contents this variable do not link. The ``run()`` function
    will remove this from the output, so always use it.
    """
    token = os.environ.get("GH_TOKEN", None)
    if not token:
        raise RuntimeError("GH_TOKEN environment variable not set")
    token = token.encode('utf-8')
    return token

def run(args):
    """
    Run the command args

    Automatically hides the secret GitHub token from the output.
    """
    if "DOCTR_DEPLOY_ENCRYPTION_KEY" in os.environ:
        token = b''
    else:
        token = get_token()
    out, err, returncode = run_command_hiding_token(args, token)
    if out:
        print(out.decode('utf-8'))
    if err:
        print(err.decode('utf-8'), file=sys.stderr)
    if returncode != 0:
        sys.exit(returncode)

def get_current_repo():
    """
    Get the GitHub repo name for the current directory.

    Assumes that the repo is in the ``origin`` remote.
    """
    remote_url = subprocess.check_output(['git', 'config', '--get',
        'remote.origin.url']).decode('utf-8')

    # Travis uses the https clone url
    _, org, git_repo = remote_url.rsplit('.git', 1)[0].rsplit('/', 2)
    return (org + '/' + git_repo)

def setup_GitHub_push(deploy_repo, auth_type='deploy_key', full_key_path='github_deploy_key.enc', require_master=True):
    """
    Setup the remote to push to GitHub (to be run on Travis).

    ``auth_type`` should be either ``'deploy_key'`` or ``'token'``.

    For ``auth_type='token'``, this sets up the remote with the token and
    checks out the gh-pages branch. The token to push to GitHub is assumed to be in the ``GH_TOKEN`` environment
    variable.

    For ``auth_type='deploy_key'``, this sets up the remote with ssh access.
    """
    if auth_type not in ['deploy_key', 'token']:
        raise ValueError("auth_type must be 'deploy_key' or 'token'")

    TRAVIS_BRANCH = os.environ.get("TRAVIS_BRANCH", "")
    TRAVIS_PULL_REQUEST = os.environ.get("TRAVIS_PULL_REQUEST", "")

    if TRAVIS_BRANCH != "master" and require_master:
        print("The docs are only pushed to gh-pages from master. To allow pushing from "
        "a non-master branch, use the --no-require-master flag", file=sys.stderr)
        print("This is the {TRAVIS_BRANCH} branch".format(TRAVIS_BRANCH=TRAVIS_BRANCH), file=sys.stderr)
        return False

    if TRAVIS_PULL_REQUEST != "false":
        print("The website and docs are not pushed to gh-pages on pull requests", file=sys.stderr)
        return False

    print("Setting git attributes")
    # Should we add some user.email?
    run(['git', 'config', '--global', 'user.name', "Doctr (Travis CI)"])

    remotes = subprocess.check_output(['git', 'remote']).decode('utf-8').split('\n')
    if 'doctr_remote' in remotes:
        print("doctr_remote already exists, removing")
        run(['git', 'remote', 'remove', 'doctr_remote'])
    else:
        print("Adding doctr remote")
        if auth_type == 'token':
            token = get_token()
            run(['git', 'remote', 'add', 'doctr_remote',
                'https://{token}@github.com/{deploy_repo}.git'.format(token=token.decode('utf-8'),
                    deploy_repo=deploy_repo)])
        else:
            keypath, key_ext = full_key_path.rsplit('.', 1)
            key_ext = '.' + key_ext
            setup_deploy_key(keypath=keypath, key_ext=key_ext)
            run(['git', 'remote', 'add', 'doctr_remote',
                'git@github.com:{deploy_repo}.git'.format(deploy_repo=deploy_repo)])

    print("Fetching doctr remote")
    run(['git', 'fetch', 'doctr_remote'])

    #create gh-pages empty branch with .nojekyll if it doesn't already exist
    new_gh_pages = create_gh_pages()
    print("Checking out gh-pages")
    if new_gh_pages:
        run(['git', 'checkout', 'gh-pages'])
    else:
        run(['git', 'checkout', '-b', 'gh-pages', '--track', 'doctr_remote/gh-pages'])
    print("Done")

    return True

def gh_pages_exists():
    """
    Check if there is a remote gh-pages branch.

    This isn't completely robust. If there are multiple remotes and you have a
    ``gh-pages`` branch on the non-default remote, this won't see it.

    """
    remote_name = 'doctr_remote'
    branch_names = subprocess.check_output(['git', 'branch', '-r']).decode('utf-8').split()

    return '{}/gh-pages'.format(remote_name) in branch_names

def create_gh_pages():
    """
    If there is no remote ``gh-pages`` branch, create one.

    Return True if ``gh-pages`` was created, False if not.
    """
    if not gh_pages_exists():
        print("Creating gh-pages branch")
        run(['git', 'checkout', '--orphan', 'gh-pages'])
        # delete everything in the new ref.  this is non-destructive to existing
        # refs/branches, etc...
        run(['git', 'rm', '-rf', '.'])
        print("Adding .nojekyll file to gh-pages branch")
        run(['touch', '.nojekyll'])
        run(['git', 'add', '.nojekyll'])
        run(['git', 'commit', '-m', 'Create new gh-pages branch with .nojekyll'])
        print("Pushing gh-pages branch to remote")
        run(['git', 'push', '-u', 'doctr_remote', 'gh-pages'])
        # return to master branch
        run(['git', 'checkout', '-'])

        return True
    return False

def find_sphinx_build_dir():
    """
    Find build subfolder within sphinx docs directory.

    This is called by :func:`commit_docs` if keyword arg ``built_docs`` is not
    specified on the command line.
    """
    build = glob.glob('**/*build/html', recursive=True)
    if not build:
        raise RuntimeError("Could not find Sphinx build directory automatically")
    build_folder = build[0]

    return build_folder

# Here is the logic to get the Travis job number, to only run commit_docs in
# the right build.
#
# TRAVIS_JOB_NUMBER = os.environ.get("TRAVIS_JOB_NUMBER", '')
# ACTUAL_TRAVIS_JOB_NUMBER = TRAVIS_JOB_NUMBER.split('.')[1]

def sync_from_log(src, dst, log_file):
    """
    Sync the files in ``src`` to ``dst``.

    The files that are synced are logged to ``log_file``. If ``log_file``
    exists, the files in ``log_file`` are removed first.

    Returns ``(added, removed)``, where added is a list of all files synced from
    ``src`` (even if it already existed in ``dst``), and ``removed`` is every
    file from ``log_file`` that was removed from ``dst`` because it wasn't in
    ``src``. ``added`` also includes the log file.
    """
    from os.path import join, exists, isdir
    if not src.endswith(os.sep):
        src += os.sep

    added, removed = [], []

    if not exists(log_file):
        # Assume this is the first run
        print("%s doesn't exist. Not removing any files." % log_file)
    else:
        with open(log_file) as f:
            files = f.read().strip().split('\n')

        for new_f in files:
            new_f = new_f.strip()
            if exists(new_f):
                os.remove(new_f)
                removed.append(new_f)
            else:
                print("Warning: File %s doesn't exist." % new_f, file=sys.stderr)

    files = glob.iglob(join(src, '**'), recursive=True)
    # sorted makes this easier to test
    for f in sorted(files):
        new_f = join(dst, f[len(src):])
        if isdir(f):
            os.makedirs(new_f, exist_ok=True)
        else:
            shutil.copy2(f, new_f)
            added.append(new_f)
            if new_f in removed:
                removed.remove(new_f)

    with open(log_file, 'w') as f:
        f.write('\n'.join(added))

    added.append(log_file)

    return added, removed

def commit_docs(*, added, removed):
    """
    Commit the docs to ``gh-pages``

    Assumes that :func:`setup_GitHub_push`, which sets up the ``doctr_remote``
    remote, has been run and returned True.

    Returns True if changes were committed and False if no changes were
    committed.
    """
    TRAVIS_BUILD_NUMBER = os.environ.get("TRAVIS_BUILD_NUMBER", "<unknown>")

    for f in added:
        run(['git', 'add', f])
    for f in removed:
        run(['git', 'rm', f])

    # Only commit if there were changes
    if subprocess.run(['git', 'diff-index', '--quiet', 'HEAD', '--'],
        stdout=subprocess.PIPE, stderr=subprocess.PIPE).returncode != 0:
        print("Committing")
        run(['git', 'commit', '-am', "Update docs after building Travis build " + TRAVIS_BUILD_NUMBER])
        return True

    return False

def push_docs():
    """
    Push the changes to the `gh-pages` branch.

    Assumes that :func:`setup_GitHub_push` has been run and returned True, and
    that :func:`commit_docs` has been run. Does not push anything if no changes
    were made.

    """

    print("Pulling")
    run(["git", "pull"])
    print("Pushing commit")
    run(['git', 'push', '-q', 'doctr_remote', 'gh-pages'])
<|endoftext|>


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

"""
Digital Ocean's API Wrapper
~~~~~~~~~~~~~~~~~~~~~~~~~~~

Requests is an HTTP library, written in Python, for human beings. Basic GET
usage:

   >>> from dop.client import Client
   >>> c = Client('client_id', 'api_key')
   >>> regions = client.regions()
   >>> for region in regions:
   >>>     print region.to_json()
   {'id': 1, 'name': u'New York 1'}
   {'id': 2, 'name': u'Amsterdam 1'}

:license: MIT, see LICENSE for more details.

"""

__title__ = 'dop'
__version__ = '01.3'
__build__ = 0x000103
__author__ = 'Antonio Hinojo'
__license__ = 'MIT'

from .client import Client, Droplet, Snapshot, Region, Image, Size, SSHKey
<commit_msg>Fixed version error<commit_after># -*- coding: utf-8 -*-

"""
Digital Ocean's API Wrapper
~~~~~~~~~~~~~~~~~~~~~~~~~~~

Requests is an HTTP library, written in Python, for human beings. Basic GET
usage:

   >>> from dop.client import Client
   >>> c = Client('client_id', 'api_key')
   >>> regions = client.regions()
   >>> for region in regions:
   >>>     print region.to_json()
   {'id': 1, 'name': u'New York 1'}
   {'id': 2, 'name': u'Amsterdam 1'}

:license: MIT, see LICENSE for more details.

"""

__title__ = 'dop'
__version__ = '0.1.3'
__build__ = 0x000103
__author__ = 'Antonio Hinojo'
__license__ = 'MIT'

from .client import Client, Droplet, Snapshot, Region, Image, Size, SSHKey
<|endoftext|>


<commit_before>#!/usr/bin/python
start_url="http://adsabs.harvard.edu/cgi-bin/nph-abs_connect?db_key=AST&db_key=PRE&qform=AST&arxiv_sel=astro-ph&arxiv_sel=cond-mat&arxiv_sel=cs&arxiv_sel=gr-qc&arxiv_sel=hep-ex&arxiv_sel=hep-lat&arxiv_sel=hep-ph&arxiv_sel=hep-th&arxiv_sel=math&arxiv_sel=math-ph&arxiv_sel=nlin&arxiv_sel=nucl-ex&arxiv_sel=nucl-th&arxiv_sel=physics&arxiv_sel=quant-ph&arxiv_sel=q-bio&aut_req=YES&aut_logic=OR&obj_logic=OR&author="
end_url="&object=&start_mon=&start_year=&end_mon=&end_year=&ttl_logic=OR&title=&txt_logic=OR&text=&nr_to_return=10&start_nr=1&jou_pick=ALL&ref_stems=&data_and=NO&abstract=YES&group_and=ALL&start_entry_day=&start_entry_mon=&start_entry_year=&end_entry_day=&end_entry_mon=&end_entry_year=&min_score=&sort=NDATE&data_type=PLAINTEXT&aut_syn=YES&ttl_syn=YES&txt_syn=YES&aut_wt=1.0&obj_wt=1.0&ttl_wt=0.3&txt_wt=3.0&aut_wgt=YES&obj_wgt=YES&ttl_wgt=YES&txt_wgt=YES&ttl_sco=YES&txt_sco=YES&version=1"

import sys, os
import urllib2

names_file = open(sys.argv[1])

#Default abstract storage
abstract_directory = "abstracts"
if len(sys.argv) > 2:
	abstract_directory = sys.argv[2]

if not os.path.exists(abstract_directory):
	os.makedirs(abstract_directory)

i = 0
for line in names_file:
	#Only names
	if line[0]!=',': continue
	if len(line) < 4: continue

	name = line[1:]
	cut_point = 0

	#Find last space
	for x in reversed(range(len(name))):
		if line[x] == ' ':
			cut_point = x
			break
	first_name = name[:x-1]
	last_name = name[x:]
	last_name = ''.join([char for char in last_name if char.isalpha()])

	print first_name+'_'+last_name+'.txt'

	query_name = first_name+'%2C+'+last_name
	query = start_url + query_name + end_url

	print query
	response = urllib2.urlopen(query)
	html = response.read()

	participant_file = open(abstract_directory+'/'+\
		first_name+'_'+last_name+'.txt', 'w')

	participant_file.write(html)
	i+=1
	if i >= 2:
		break
<commit_msg>Added number abstracts option<commit_after>#!/usr/bin/python
start_url="http://adsabs.harvard.edu/cgi-bin/nph-abs_connect?db_key=AST&db_key=PRE&qform=AST&arxiv_sel=astro-ph&arxiv_sel=cond-mat&arxiv_sel=cs&arxiv_sel=gr-qc&arxiv_sel=hep-ex&arxiv_sel=hep-lat&arxiv_sel=hep-ph&arxiv_sel=hep-th&arxiv_sel=math&arxiv_sel=math-ph&arxiv_sel=nlin&arxiv_sel=nucl-ex&arxiv_sel=nucl-th&arxiv_sel=physics&arxiv_sel=quant-ph&arxiv_sel=q-bio&aut_req=YES&aut_logic=OR&obj_logic=OR&author="
end_url="&object=&start_mon=&start_year=&end_mon=&end_year=&ttl_logic=OR&title=&txt_logic=OR&text=&nr_to_return=10&start_nr=1&jou_pick=ALL&ref_stems=&data_and=NO&abstract=YES&group_and=ALL&start_entry_day=&start_entry_mon=&start_entry_year=&end_entry_day=&end_entry_mon=&end_entry_year=&min_score=&sort=NDATE&data_type=PLAINTEXT&aut_syn=YES&ttl_syn=YES&txt_syn=YES&aut_wt=1.0&obj_wt=1.0&ttl_wt=0.3&txt_wt=3.0&aut_wgt=YES&obj_wgt=YES&ttl_wgt=YES&txt_wgt=YES&ttl_sco=YES&txt_sco=YES&version=1"

import sys, os
import urllib2

names_file = open(sys.argv[1])

#Default abstract storage
abstract_directory = "abstracts"
if len(sys.argv) > 2:
	abstract_directory = sys.argv[2]

if not os.path.exists(abstract_directory):
	os.makedirs(abstract_directory)

number_abstracts = 4
if len(sys.argv) > 3:
	number_abstracts = int(sys.argv[3])

i = 0
for line in names_file:
	#Only names
	if line[0]==',': continue
	if len(line) < 4: continue

	cut_point = 0

	#Find last space
	for x in reversed(range(len(line))):
		if line[x] == ' ':
			cut_point = x
			break
	first_name = line[:x]
	last_name = line[x+1:]
	last_name = ''.join([char for char in last_name if char.isalpha()])

	print first_name+'_'+last_name+'.txt'

	query_name = first_name+'%2C+'+last_name
	query = start_url + query_name + end_url.replace('10', str(number_abstracts))

	print query
	response = urllib2.urlopen(query)
	html = response.read()

	participant_file = open(abstract_directory+'/'+\
		first_name+'_'+last_name+'.txt', 'w')

	participant_file.write(html)
	i+=1
	if i >= 2:
		break
<|endoftext|>


<commit_before>APP_NAME = "dynamic_rest"
AUTHOR = "Anthony Leontiev"
AUTHOR_EMAIL = "ant@altschool.com, ryo@altschool.com"
DESCRIPTION = "Adds Dynamic API support to Django REST Framework."
REPO_NAME = "dynamic-rest"
PROJECT_NAME = "Dynamic REST"
ORG_NAME = "AltSchool"
VERSION = "1.5.4"
<commit_msg>bump version to match tag<commit_after>APP_NAME = "dynamic_rest"
AUTHOR = "Anthony Leontiev"
AUTHOR_EMAIL = "ant@altschool.com, ryo@altschool.com"
DESCRIPTION = "Adds Dynamic API support to Django REST Framework."
REPO_NAME = "dynamic-rest"
PROJECT_NAME = "Dynamic REST"
ORG_NAME = "AltSchool"
VERSION = "1.5.7"
<|endoftext|>


<commit_before>import pytest
from setup_e2e import create_table, delete_dataset

@pytest.fixture(scope="function")
def setup():
  print("=============CREATING TABLE=============")
  create_table()
  yield
  delete_dataset()
  print("=============DELETING TABLE=============")
<commit_msg>updated fixtures code<commit_after>import pytest
from setup_e2e import create_table, delete_dataset

@pytest.fixture(scope="session")
def setup():
  create_table()
  yield
  delete_dataset()
<|endoftext|>


<commit_before>"""
Manage ECM Groups.
"""

import collections
import difflib
import json
import shellish
import sys
from . import base

PatchStat = collections.namedtuple('PatchStat', 'adds, removes')

def patch_stats(patch):
    adds, removes = patch
    adds = list(base.totuples(adds))
    for x in adds:
        print(x)
    return PatchStat(len(adds), len(removes))

def patch_validate(patch):
    if not isinstance(patch, list) or len(patch) != 2:
        raise TypeError('Patch must be a 2 item list')
    for x in patch[1]:
        if not isinstance(x, list):
            raise TypeError('Removals must be lists')
    if not isinstance(patch[0], dict):
        raise TypeError('Additions must be an dict tree')

class Printer(object):
    """ Mixin for printing commands. """

    expands = ','.join([
        'statistics',
        'product',
        'account',
        'target_firmware',
        'configuration'
    ])

    def bundle_group(self, group):
        group['product'] = group['product']['name']
        group['firmware'] = group['target_firmware']['version']
        stats = group['statistics']
        group['online'] = stats['online_count']
        group['offline'] = stats['offline_count']
        group['total'] = stats['device_count']
        group['account_name'] = group['account']['name']
        group['suspended'] = stats['suspended_count']
        group['synchronized'] = stats['synched_count']
        return group

    def ratio_format(self, actual, possible, high=0.99, med=0.90, low=0.90):
        """ Color format output for a ratio. Bigger is better. """
        if not possible:
            return ''
        pct = round((actual / possible) * 100)
        if pct > .99:
            color = 'green'
        elif pct > .90:
            color = 'yellow'
        else:
            color = 'red'
        return '%s/%s (<%s>%d%%</%s>)' % (actual, possible, color, pct, color)

    def online_accessor(self, group):
        """ Table accessor for online column. """
        return self.ratio_format(group['online'], group['total'])

    def sync_accessor(self, group):
        """ Table accessor for config sync stats. """
        suspended = group['suspended']
        sync = group['synchronized']
        total = group['total']
        ret = self.ratio_format(sync, total)
        if suspended:
            ret += ', <red>%d suspended</red>' % suspended
        return ret

    def patch_accessor(self, group):
        """ Table accessor for patch stats. """
        stat = patch_stats(group['configuration'])
        output = []
        if stat.adds:
            output.append('<cyan>+%d</cyan>' % stat.adds)
        if stat.removes:
            output.append('<magenta>-%d</magenta>' % stat.removes)
        return '/'.join(output)

    def printer(self, groups):
        fields = (
            ("name", 'Name'),
            ("id", 'ID'),
            ("account_name", 'Account'),
            ("product", 'Product'),
            ("firmware", 'Firmware'),
            (self.online_accessor, 'Online'),
            (self.sync_accessor, 'Config Sync'),
            (self.patch_accessor, 'Config Patch'),
        )
        table = shellish.Table(headers=[x[1] for x in fields],
                               accessors=[x[0] for x in fields])
        table.print(map(self.bundle_group, groups))

class Show(Printer, base.ECMCommand):
    """ Show group(s). """

    name = 'show'

    def setup_args(self, parser):
        self.add_group_argument(nargs='?')
        super().setup_args(parser)

    def run(self, args):
        if args.ident:
            groups = [self.api.get_by_id_or_name('groups', args.ident,
                                                 expand=self.expands)]
        else:
            groups = self.api.get_pager('groups', expand=self.expands)
        self.printer(groups)

class Create(base.ECMCommand):
    """ Create a new group.
    A group mostly represents configuration for more than one device, but
    also manages settings such as alerts and log acquisition. """

    name = 'create'

    def setup_args(self, parser):
        self.add_argument('--name')
        self.add_product_argument('--product')
        self.add_firmware_argument('--firmware')

    def run(self, args):
        name = args.name or input('Name: ')
        if not name:
            raise SystemExit("Name required")

        product = args.product or input('Product: ')
        products = dict((x['name'], x)
                        for x in self.api.get_pager('products'))
        if product not in products:
            if not product:
                print("Product required")
            else:
                print("Invalid product:", product)
            print("\nValid products...")
            for x in sorted(products):
                print("\t", x)
            raise SystemExit(1)

        fw = args.firmware or input('Firmware: ')
        firmwares = dict((x['version'], x)
                         for x in self.api.get_pager('firmwares',
                         product=products[product]['id']))
        if fw not in firmwares:
            if not fw:
                print("Firmware required")
            else:
                print("Invalid firmware:", fw)
            print("\nValid firmares...")
            for x in sorted(firmwares):
                print("\t", x)
            raise SystemExit(1)

        self.api.post('groups', {
            "name": name,
            "product": products[product]['resource_uri'],
            "target_firmware": firmwares[fw]['resource_uri']
        })

class Config(base.ECMCommand):
    """ Show or alter the group configuration.
    The configuration stored in a group consists of a patch tuple;
    (additions, removals) respectively.  The patch format is a JSON array with
    exactly 2 entries to match the aforementioned tuple.

    Additions are a JSON object that holds a subset of a routers configuration
    tree.  When paths, or nodes, are absent they are simply ignored.  The
    config system will crawl the object looking for "leaf" nodes, which are
    applied as an overlay on the device.

    The actual algo for managing this layering is rather complex, so it is
    advised that you experiment with this feature in a test environment before
    attempting action on a production environment.

    The deletions section contains lists of paths to be removed from the
    router's config.  This is used in cases where you want to explicitly take
    out a configuration that is on the router by default.  A common case is
    removing one of the default LAN networks, such as the guest network.

    Example patch that updates config.system.desc and has no deletions.

        [{"system": {"desc": "New Value Here"}}, []]
    """

    name = 'config'

    def setup_args(self, parser):
        self.add_group_argument()
        edit_options = parser.add_argument_group('patch options (DANGEROUS)')
        or_group = edit_options.add_mutually_exclusive_group()
        self.add_argument('--patch-file', parser=or_group, help='JSON patch '
                          'file or - to read from stdin.')
        self.add_argument('--purge', action='store_true', parser=or_group,
                          help='Completely remove configuration.')
        self.add_argument('--force', '-f', action='store_true',
                          parser=edit_options, help='Do not prompt for '
                          'confirmation.')
        output_options = parser.add_argument_group('output options')
        self.add_argument('--json', action='store_true',
                          parser=output_options)
        self.add_argument('--output-file', '-o', parser=output_options)

    def run(self, args):
        group = self.api.get_by_id_or_name('groups', args.ident,
                                           expand='configuration')
        if args.purge or args.patch_file:
            return self.update(group, args)
        else:
            return self.show(group, args)

    def update(self, group, args):
        if args.purge:
            if not args.force:
                base.confirm('Confirm config purge of: %s' % group['name'])
        elif args.patch_file:
            with open(args.patch_file) as f:
                patch = json.load(f)
            patch_validate(patch)
            oldpatch = group['configuration']
            oldjson = json.dumps(oldpatch, indent=4, sort_keys=True)
            newjson = json.dumps(patch, indent=4, sort_keys=True)
            old_adds = list('='.join(map(str, x))
                            for x in base.totuples(group['configuration'][0]))
            new_adds = list('='.join(map(str, x))
                            for x in base.totuples(patch[0]))
            for line in difflib.unified_diff(oldjson.splitlines(True),
                                             newjson.splitlines(True),
                                             fromfile='current config',
                                             tofile='proposed config', n=10):
                print(line, end='')
            for line in difflib.unified_diff(old_adds, new_adds, n=0):
                if line[0] not in '-+':
                    continue
                print(line)
            base.confirm('Confirm update of config of: %s' % group['name'])
        else:
            raise RuntimeError('invalid call to update')

    def show(self, group, args):
        adds, removes = group['configuration']
        if args.output_file:
            args.json = True
            outfd = open(args.output_file, 'w')
        else:
            outfd = sys.stdout
        if args.json:
            print(json.dumps([adds, removes], indent=4), file=outfd)
        else:
            treelines = self.tree(base.todict({
                "<additions>": adds,
                "<removes>": removes
            }), render_only=True)
            for x in treelines:
                print(x, file=outfd)
        if outfd is not sys.stdout:
            outfd.close()

class Edit(base.ECMCommand):
    """ Edit group attributes. """

    name = 'edit'

    def setup_args(self, parser):
        self.add_group_argument()
        self.add_argument('--name')
        self.add_firmware_argument('--firmware')

    def run(self, args):
        group = self.api.get_by_id_or_name('groups', args.ident)
        updates = {}
        if args.name:
            updates['name'] = args.name
        if args.firmware:
            fw = self.api.get_by(['version'], 'firmwares', args.firmware)
            updates['target_firmware'] = fw['resource_uri']
        self.api.put('groups', group['id'], updates)

class Delete(base.ECMCommand):
    """ Delete one or more groups. """

    name = 'delete'

    def setup_args(self, parser):
        self.add_group_argument('idents', nargs='+')
        self.add_argument('-f', '--force', action="store_true")

    def run(self, args):
        for ident in args.idents:
            group = self.api.get_by_id_or_name('groups', ident)
            if not args.force and \
               not base.confirm('Delete group: %s' % group['name'],
                                exit=False):
                continue
            self.api.delete('groups', group['id'])

class Move(base.ECMCommand):
    """ Move group to a different account. """

    name = 'move'

    def setup_args(self, parser):
        self.add_group_argument()
        self.add_account_argument('new_account', metavar='NEW_ACCOUNT_ID_OR_NAME')
        self.add_argument('-f', '--force', action="store_true")

    def run(self, args):
        group = self.api.get_by_id_or_name('groups', args.ident)
        account = self.api.get_by_id_or_name('accounts', args.new_account)
        self.api.put('groups', group['id'],
                     {"account": account['resource_uri']})

class Search(Printer, base.ECMCommand):
    """ Search for groups. """

    name = 'search'
    fields = ['name', ('firmware', 'target_firmware.version'),
              ('product', 'product.name'), ('account', 'account.name')]

    def setup_args(self, parser):
        searcher = self.make_searcher('groups', self.fields)
        self.lookup = searcher.lookup
        self.add_search_argument(searcher)
        super().setup_args(parser)

    def run(self, args):
        results = list(self.lookup(args.search, expand=self.expands))
        if not results:
            raise SystemExit("No Results For: %s" % ' '.join(args.search))
        self.printer(results)

class Groups(base.ECMCommand):
    """ Manage ECM Groups. """

    name = 'groups'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.add_subcommand(Show, default=True)
        self.add_subcommand(Create)
        self.add_subcommand(Edit)
        self.add_subcommand(Delete)
        self.add_subcommand(Move)
        self.add_subcommand(Search)
        self.add_subcommand(Config)

command_classes = [Groups]
<commit_msg>Fix for groups setting invalid firmware.<commit_after>"""
Manage ECM Groups.
"""

import collections
import difflib
import json
import shellish
import sys
from . import base

PatchStat = collections.namedtuple('PatchStat', 'adds, removes')

def patch_stats(patch):
    adds, removes = patch
    adds = list(base.totuples(adds))
    for x in adds:
        print(x)
    return PatchStat(len(adds), len(removes))

def patch_validate(patch):
    if not isinstance(patch, list) or len(patch) != 2:
        raise TypeError('Patch must be a 2 item list')
    for x in patch[1]:
        if not isinstance(x, list):
            raise TypeError('Removals must be lists')
    if not isinstance(patch[0], dict):
        raise TypeError('Additions must be an dict tree')

class Printer(object):
    """ Mixin for printing commands. """

    expands = ','.join([
        'statistics',
        'product',
        'account',
        'target_firmware',
        'configuration'
    ])

    def bundle_group(self, group):
        group['product'] = group['product']['name']
        group['firmware'] = group['target_firmware']['version']
        stats = group['statistics']
        group['online'] = stats['online_count']
        group['offline'] = stats['offline_count']
        group['total'] = stats['device_count']
        group['account_name'] = group['account']['name']
        group['suspended'] = stats['suspended_count']
        group['synchronized'] = stats['synched_count']
        return group

    def ratio_format(self, actual, possible, high=0.99, med=0.90, low=0.90):
        """ Color format output for a ratio. Bigger is better. """
        if not possible:
            return ''
        pct = round((actual / possible) * 100)
        if pct > .99:
            color = 'green'
        elif pct > .90:
            color = 'yellow'
        else:
            color = 'red'
        return '%s/%s (<%s>%d%%</%s>)' % (actual, possible, color, pct, color)

    def online_accessor(self, group):
        """ Table accessor for online column. """
        return self.ratio_format(group['online'], group['total'])

    def sync_accessor(self, group):
        """ Table accessor for config sync stats. """
        suspended = group['suspended']
        sync = group['synchronized']
        total = group['total']
        ret = self.ratio_format(sync, total)
        if suspended:
            ret += ', <red>%d suspended</red>' % suspended
        return ret

    def patch_accessor(self, group):
        """ Table accessor for patch stats. """
        stat = patch_stats(group['configuration'])
        output = []
        if stat.adds:
            output.append('<cyan>+%d</cyan>' % stat.adds)
        if stat.removes:
            output.append('<magenta>-%d</magenta>' % stat.removes)
        return '/'.join(output)

    def printer(self, groups):
        fields = (
            ("name", 'Name'),
            ("id", 'ID'),
            ("account_name", 'Account'),
            ("product", 'Product'),
            ("firmware", 'Firmware'),
            (self.online_accessor, 'Online'),
            (self.sync_accessor, 'Config Sync'),
            (self.patch_accessor, 'Config Patch'),
        )
        table = shellish.Table(headers=[x[1] for x in fields],
                               accessors=[x[0] for x in fields])
        table.print(map(self.bundle_group, groups))

class Show(Printer, base.ECMCommand):
    """ Show group(s). """

    name = 'show'

    def setup_args(self, parser):
        self.add_group_argument(nargs='?')
        super().setup_args(parser)

    def run(self, args):
        if args.ident:
            groups = [self.api.get_by_id_or_name('groups', args.ident,
                                                 expand=self.expands)]
        else:
            groups = self.api.get_pager('groups', expand=self.expands)
        self.printer(groups)

class Create(base.ECMCommand):
    """ Create a new group.
    A group mostly represents configuration for more than one device, but
    also manages settings such as alerts and log acquisition. """

    name = 'create'

    def setup_args(self, parser):
        self.add_argument('--name')
        self.add_product_argument('--product')
        self.add_firmware_argument('--firmware')

    def run(self, args):
        name = args.name or input('Name: ')
        if not name:
            raise SystemExit("Name required")

        product = args.product or input('Product: ')
        products = dict((x['name'], x)
                        for x in self.api.get_pager('products'))
        if product not in products:
            if not product:
                print("Product required")
            else:
                print("Invalid product:", product)
            print("\nValid products...")
            for x in sorted(products):
                print("\t", x)
            raise SystemExit(1)

        fw = args.firmware or input('Firmware: ')
        firmwares = dict((x['version'], x)
                         for x in self.api.get_pager('firmwares',
                         product=products[product]['id']))
        if fw not in firmwares:
            if not fw:
                print("Firmware required")
            else:
                print("Invalid firmware:", fw)
            print("\nValid firmares...")
            for x in sorted(firmwares):
                print("\t", x)
            raise SystemExit(1)

        self.api.post('groups', {
            "name": name,
            "product": products[product]['resource_uri'],
            "target_firmware": firmwares[fw]['resource_uri']
        })

class Config(base.ECMCommand):
    """ Show or alter the group configuration.
    The configuration stored in a group consists of a patch tuple;
    (additions, removals) respectively.  The patch format is a JSON array with
    exactly 2 entries to match the aforementioned tuple.

    Additions are a JSON object that holds a subset of a routers configuration
    tree.  When paths, or nodes, are absent they are simply ignored.  The
    config system will crawl the object looking for "leaf" nodes, which are
    applied as an overlay on the device.

    The actual algo for managing this layering is rather complex, so it is
    advised that you experiment with this feature in a test environment before
    attempting action on a production environment.

    The deletions section contains lists of paths to be removed from the
    router's config.  This is used in cases where you want to explicitly take
    out a configuration that is on the router by default.  A common case is
    removing one of the default LAN networks, such as the guest network.

    Example patch that updates config.system.desc and has no deletions.

        [{"system": {"desc": "New Value Here"}}, []]
    """

    name = 'config'

    def setup_args(self, parser):
        self.add_group_argument()
        edit_options = parser.add_argument_group('patch options (DANGEROUS)')
        or_group = edit_options.add_mutually_exclusive_group()
        self.add_argument('--patch-file', parser=or_group, help='JSON patch '
                          'file or - to read from stdin.')
        self.add_argument('--purge', action='store_true', parser=or_group,
                          help='Completely remove configuration.')
        self.add_argument('--force', '-f', action='store_true',
                          parser=edit_options, help='Do not prompt for '
                          'confirmation.')
        output_options = parser.add_argument_group('output options')
        self.add_argument('--json', action='store_true',
                          parser=output_options)
        self.add_argument('--output-file', '-o', parser=output_options)

    def run(self, args):
        group = self.api.get_by_id_or_name('groups', args.ident,
                                           expand='configuration')
        if args.purge or args.patch_file:
            return self.update(group, args)
        else:
            return self.show(group, args)

    def update(self, group, args):
        if args.purge:
            if not args.force:
                base.confirm('Confirm config purge of: %s' % group['name'])
        elif args.patch_file:
            with open(args.patch_file) as f:
                patch = json.load(f)
            patch_validate(patch)
            oldpatch = group['configuration']
            oldjson = json.dumps(oldpatch, indent=4, sort_keys=True)
            newjson = json.dumps(patch, indent=4, sort_keys=True)
            old_adds = list('='.join(map(str, x))
                            for x in base.totuples(group['configuration'][0]))
            new_adds = list('='.join(map(str, x))
                            for x in base.totuples(patch[0]))
            for line in difflib.unified_diff(oldjson.splitlines(True),
                                             newjson.splitlines(True),
                                             fromfile='current config',
                                             tofile='proposed config', n=10):
                print(line, end='')
            for line in difflib.unified_diff(old_adds, new_adds, n=0):
                if line[0] not in '-+':
                    continue
                print(line)
            base.confirm('Confirm update of config of: %s' % group['name'])
        else:
            raise RuntimeError('invalid call to update')

    def show(self, group, args):
        adds, removes = group['configuration']
        if args.output_file:
            args.json = True
            outfd = open(args.output_file, 'w')
        else:
            outfd = sys.stdout
        if args.json:
            print(json.dumps([adds, removes], indent=4), file=outfd)
        else:
            treelines = self.tree(base.todict({
                "<additions>": adds,
                "<removes>": removes
            }), render_only=True)
            for x in treelines:
                print(x, file=outfd)
        if outfd is not sys.stdout:
            outfd.close()

class Edit(base.ECMCommand):
    """ Edit group attributes. """

    name = 'edit'

    def setup_args(self, parser):
        self.add_group_argument()
        self.add_argument('--name')
        self.add_firmware_argument('--firmware')

    def run(self, args):
        group = self.api.get_by_id_or_name('groups', args.ident,
                                           expand='product')
        updates = {}
        if args.name:
            updates['name'] = args.name
        if args.firmware:
            fw = self.api.get_by(['version'], 'firmwares', args.firmware,
                                 product=group['product']['id'])
            updates['target_firmware'] = fw['resource_uri']
        self.api.put('groups', group['id'], updates)

class Delete(base.ECMCommand):
    """ Delete one or more groups. """

    name = 'delete'

    def setup_args(self, parser):
        self.add_group_argument('idents', nargs='+')
        self.add_argument('-f', '--force', action="store_true")

    def run(self, args):
        for ident in args.idents:
            group = self.api.get_by_id_or_name('groups', ident)
            if not args.force and \
               not base.confirm('Delete group: %s' % group['name'],
                                exit=False):
                continue
            self.api.delete('groups', group['id'])

class Move(base.ECMCommand):
    """ Move group to a different account. """

    name = 'move'

    def setup_args(self, parser):
        self.add_group_argument()
        self.add_account_argument('new_account', metavar='NEW_ACCOUNT_ID_OR_NAME')
        self.add_argument('-f', '--force', action="store_true")

    def run(self, args):
        group = self.api.get_by_id_or_name('groups', args.ident)
        account = self.api.get_by_id_or_name('accounts', args.new_account)
        self.api.put('groups', group['id'],
                     {"account": account['resource_uri']})

class Search(Printer, base.ECMCommand):
    """ Search for groups. """

    name = 'search'
    fields = ['name', ('firmware', 'target_firmware.version'),
              ('product', 'product.name'), ('account', 'account.name')]

    def setup_args(self, parser):
        searcher = self.make_searcher('groups', self.fields)
        self.lookup = searcher.lookup
        self.add_search_argument(searcher)
        super().setup_args(parser)

    def run(self, args):
        results = list(self.lookup(args.search, expand=self.expands))
        if not results:
            raise SystemExit("No Results For: %s" % ' '.join(args.search))
        self.printer(results)

class Groups(base.ECMCommand):
    """ Manage ECM Groups. """

    name = 'groups'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.add_subcommand(Show, default=True)
        self.add_subcommand(Create)
        self.add_subcommand(Edit)
        self.add_subcommand(Delete)
        self.add_subcommand(Move)
        self.add_subcommand(Search)
        self.add_subcommand(Config)

command_classes = [Groups]
<|endoftext|>