<commit_before>
"""Define functions to send emails."""

import re

from pyramid.renderers import render
from pyramid_mailer import get_mailer
from pyramid_mailer.message import Attachment
from pyramid_mailer.message import Message

from jinja2 import Template, TemplateNotFound
import html2text

from spynl.main.utils import get_logger, get_settings

from spynl.main.exceptions import EmailTemplateNotFound, EmailRecipientNotGiven

DEFAULT_HTML_TEMPLATE = '''
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
                        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <title>{{subject}}</title>
        <meta name="viewport" content="width=device-width, initial-scale=1.0"/>

            {{content_string}}

    '''

def _sendmail(request, recipients, subject, plain_body, html_body=None,
              sender=None, attachments=None, fail_silently=True, mailer=None,
              reply_to=None, sender_name=None):
    """
    Send a mail using the pyramid mailer. This function also makes sure
    that if Spynl is not in a production environment, mail is sent to a dummy
    email address.

    :param Request request: the original request
    :param string recipients: addressees
    :param string subject: subject of mail
    :param string plain_body: content of mail
    :param string html_body: html content of mail
    :param string sender: senders address (optional,
                          default: no_reply@<spynl.domain>)
    :param string sender_name: sender name)
    :param [Attachment] attachments: attachments (optional)
    :param bool fail_silently: keep quiet when connection errors happen
    :param pyramid_mailer.Mailer mailer: mailer object (optional)
    """
    settings = get_settings()
    logger = get_logger()

    if mailer is None:
        mailer = get_mailer(request)

    if not sender:
        sender = settings.get('mail.sender')
        if not sender:
            domain = settings.get('spynl.domain', 'example.com')
            sender = 'no_reply@{}'.format(domain)

    sender = str(sender)
    if not recipients:
        if not fail_silently:
            raise EmailRecipientNotGiven()
        elif settings.get('mail.dummy_recipient'):
            recipients = [settings.get('mail.dummy_recipient')]
            logger.info("I will send this email to %s instead of %s.",
                        settings.get('mail.dummy_recipient'), recipients)
    if isinstance(recipients, str):
        recipients = [recipients]

    subject = str(subject).rstrip()

    # set Reply-To header if needed:
    extra_headers = {}
    if reply_to:
        extra_headers['Reply-To'] = reply_to
    if sender_name:
        # sanitize sender name to make sure we do not get bad headers or
        # unverified email errors.
        sender_name = re.sub('[\n<>@]', ' ', sender_name).strip()
        extra_headers['From'] = '{} <{}>'.format(sender_name, sender)
    message = Message(sender=sender, recipients=recipients, subject=subject,
                      body=plain_body, html=html_body,
                      extra_headers=extra_headers)
    if attachments:
        for attachment in attachments:
            message.attach(attachment)

    try:
        message.validate()
        logger.info('Sending email titled "%s" to %s from %s',
                    subject, recipients, sender)
        # Always set fail_silently to false, so we can log the
        # exception
        mailer.send_immediately(message, fail_silently=False)
    except Exception as e:
        if fail_silently:
            logger.exception(e)
            return False
        else:
            raise e

    return True

def send_template_email(request, recipient, template_string=None,
                        template_file=None, replacements=None,
                        subject='', fail_silently=True, mailer=None,
                        attachments=None, sender=None, reply_to=None,
                        sender_name=None):
    """
    Send email using a html template.

    The email's content (which can contain HTML code) is defined by a Jinja
    template. This content template can be given either by filename
    <template_file> (absolute path, without jinja2 extension) or directly in
    string form <template_string>. In both cases, the email content gets
    wrapped by a base
    template which defines a consistent layout. The base template to use is
    defined by a string type setting:
        `base_email_template: absolute path of template
    If the "base template" cannot be loaded the <DEFAULT_HTML_TEMPLATE> is
    used.
    Replacements can be send in to customise the email content.
    If no html or plain text was constructed in the end, replacements are being
    used to construct a basic text to be send as an email.

    If a template file is given, we assume that the file ends with .jinja2, and
    that there is a companion file .subject.jinja2 that defines the subject.
    """
    if (template_string is None and template_file is None) or \
            (template_string is not None and template_file is not None):
        raise Exception('One of <template_string> or <template_file> must be '
                        'given.')
    text_body = ''
    if not replacements:
        replacements = {}

    if template_file is not None:
        try:
            html_body = render(template_file + '.jinja2', replacements,
                               request=request)
            if not subject:
                subject = render(template_file + '.subject.jinja2',
                                 replacements, request=request)
                subject = subject.replace('\n', '')
        except TemplateNotFound:
            raise EmailTemplateNotFound(template_file)
    else:
        replacements['content_string'] = Template(template_string).render(
            **replacements)
        # Render the base template with the content of the given template
        base_template = get_settings().get('base_email_template')
        if base_template is not None:
            html_body = render(base_template, replacements, request=request)
        else:
            html_body = Template(DEFAULT_HTML_TEMPLATE).render(**replacements)

    html_body = html_body.replace('\n', '')

    text_maker = html2text.HTML2Text()
    text_maker.ignore_images = True
    text_maker.ignore_tables = True
    text_maker.wrap_links = False
    text_maker.use_automatic_links = False
    text_maker.body_width = 0
    text_body = text_maker.handle(html_body)

    text_body = Attachment(data=text_body, transfer_encoding="base64",
                           content_type="text/plain; charset=UTF-8",
                           disposition='inline')
    html_body = Attachment(data=html_body, transfer_encoding="base64",
                           content_type="text/html; charset=UTF-8",
                           disposition='inline')

    if not text_body and not html_body:
        str_replacements = '\n'.join(['{}: {}'.format(key, value)
                                      for key, value in replacements.items()])
        text_body = 'Your account has been changed.\n' + str_replacements
        template = Template(DEFAULT_HTML_TEMPLATE)
        replacements.update(subject=subject, content=text_body)
        html_body = template.render(**replacements)
        get_logger(__name__).error(
            'Body was not found in body_string or email template: %s',
            template)

    return _sendmail(request, recipient, subject, text_body, html_body,
                     fail_silently=fail_silently, mailer=mailer,
                     attachments=attachments, sender=sender, reply_to=reply_to,
                     sender_name=sender_name)
<commit_msg>SWPY-1143 allow multiple mail recipients<commit_after>
"""Define functions to send emails."""

import re

from pyramid.renderers import render
from pyramid_mailer import get_mailer
from pyramid_mailer.message import Attachment
from pyramid_mailer.message import Message

from jinja2 import Template, TemplateNotFound
import html2text

from spynl.main.utils import get_logger, get_settings

from spynl.main.exceptions import EmailTemplateNotFound, EmailRecipientNotGiven

DEFAULT_HTML_TEMPLATE = '''
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
                        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <title>{{subject}}</title>
        <meta name="viewport" content="width=device-width, initial-scale=1.0"/>

            {{content_string}}

    '''

def _sendmail(request, recipients, subject, plain_body, html_body=None,
              sender=None, attachments=None, fail_silently=True, mailer=None,
              reply_to=None, sender_name=None):
    """
    Send a mail using the pyramid mailer. This function also makes sure
    that if Spynl is not in a production environment, mail is sent to a dummy
    email address.

    :param Request request: the original request
    :param string recipients: addressees
    :param string subject: subject of mail
    :param string plain_body: content of mail
    :param string html_body: html content of mail
    :param string sender: senders address (optional,
                          default: no_reply@<spynl.domain>)
    :param string sender_name: sender name)
    :param [Attachment] attachments: attachments (optional)
    :param bool fail_silently: keep quiet when connection errors happen
    :param pyramid_mailer.Mailer mailer: mailer object (optional)
    """
    settings = get_settings()
    logger = get_logger()

    if mailer is None:
        mailer = get_mailer(request)

    if not sender:
        sender = settings.get('mail.sender')
        if not sender:
            domain = settings.get('spynl.domain', 'example.com')
            sender = 'no_reply@{}'.format(domain)

    sender = str(sender)
    if not recipients:
        if not fail_silently:
            raise EmailRecipientNotGiven()
        elif settings.get('mail.dummy_recipient'):
            recipients = [settings.get('mail.dummy_recipient')]
            logger.info("I will send this email to %s instead of %s.",
                        settings.get('mail.dummy_recipient'), recipients)
        else:
            return False

    if isinstance(recipients, str):
        recipients = [recipients]

    subject = str(subject).rstrip()

    # set Reply-To header if needed:
    extra_headers = {}
    if reply_to:
        extra_headers['Reply-To'] = reply_to
    if sender_name:
        # sanitize sender name to make sure we do not get bad headers or
        # unverified email errors.
        sender_name = re.sub('[\n<>@]', ' ', sender_name).strip()
        extra_headers['From'] = '{} <{}>'.format(sender_name, sender)
    message = Message(sender=sender, recipients=recipients, subject=subject,
                      body=plain_body, html=html_body,
                      extra_headers=extra_headers)
    if attachments:
        for attachment in attachments:
            message.attach(attachment)

    try:
        message.validate()
        logger.info('Sending email titled "%s" to %s from %s',
                    subject, recipients, sender)
        # Always set fail_silently to false, so we can log the
        # exception
        mailer.send_immediately(message, fail_silently=False)
    except Exception as e:
        if fail_silently:
            logger.exception(e)
            return False
        else:
            raise e

    return True

def send_template_email(request, recipient, template_string=None,
                        template_file=None, replacements=None,
                        subject='', fail_silently=True, mailer=None,
                        attachments=None, sender=None, reply_to=None,
                        sender_name=None):
    """
    Send email using a html template.

    The email's content (which can contain HTML code) is defined by a Jinja
    template. This content template can be given either by filename
    <template_file> (absolute path, without jinja2 extension) or directly in
    string form <template_string>. In both cases, the email content gets
    wrapped by a base
    template which defines a consistent layout. The base template to use is
    defined by a string type setting:
        `base_email_template: absolute path of template
    If the "base template" cannot be loaded the <DEFAULT_HTML_TEMPLATE> is
    used.
    Replacements can be send in to customise the email content.
    If no html or plain text was constructed in the end, replacements are being
    used to construct a basic text to be send as an email.

    If a template file is given, we assume that the file ends with .jinja2, and
    that there is a companion file .subject.jinja2 that defines the subject.
    """
    if (template_string is None and template_file is None) or \
            (template_string is not None and template_file is not None):
        raise Exception('One of <template_string> or <template_file> must be '
                        'given.')
    text_body = ''
    if not replacements:
        replacements = {}

    if template_file is not None:
        try:
            html_body = render(template_file + '.jinja2', replacements,
                               request=request)
            if not subject:
                subject = render(template_file + '.subject.jinja2',
                                 replacements, request=request)
                subject = subject.replace('\n', '')
        except TemplateNotFound:
            raise EmailTemplateNotFound(template_file)
    else:
        replacements['content_string'] = Template(template_string).render(
            **replacements)
        # Render the base template with the content of the given template
        base_template = get_settings().get('base_email_template')
        if base_template is not None:
            html_body = render(base_template, replacements, request=request)
        else:
            html_body = Template(DEFAULT_HTML_TEMPLATE).render(**replacements)

    html_body = html_body.replace('\n', '')

    text_maker = html2text.HTML2Text()
    text_maker.ignore_images = True
    text_maker.ignore_tables = True
    text_maker.wrap_links = False
    text_maker.use_automatic_links = False
    text_maker.body_width = 0
    text_body = text_maker.handle(html_body)

    text_body = Attachment(data=text_body, transfer_encoding="base64",
                           content_type="text/plain; charset=UTF-8",
                           disposition='inline')
    html_body = Attachment(data=html_body, transfer_encoding="base64",
                           content_type="text/html; charset=UTF-8",
                           disposition='inline')

    if not text_body and not html_body:
        str_replacements = '\n'.join(['{}: {}'.format(key, value)
                                      for key, value in replacements.items()])
        text_body = 'Your account has been changed.\n' + str_replacements
        template = Template(DEFAULT_HTML_TEMPLATE)
        replacements.update(subject=subject, content=text_body)
        html_body = template.render(**replacements)
        get_logger(__name__).error(
            'Body was not found in body_string or email template: %s',
            template)

    return _sendmail(request, recipient, subject, text_body, html_body,
                     fail_silently=fail_silently, mailer=mailer,
                     attachments=attachments, sender=sender, reply_to=reply_to,
                     sender_name=sender_name)
<|endoftext|>


<commit_before>from clovek import*
from racunalnik import*

CRNI = 1
BELI = 2

def matrika_nicel(n, m):
	"""
	Funkcija naredi matriko n x m ni
	"""
	sez = []
	for i in range(m):
		sez += [[0]*n]
	return sez

class Igra():
	def __init__(self, gui):
		self.tabela = matrika_nicel(19,19)
		self.igralec1 = Clovek(self,CRNI)#, Minimax(globina))
		self.igralec2 = Racunalnik(self, Minimax(globina), BELI)
		self.na_potezi = self.igralec1
		self.gui = gui

		#Mogoče, bova rabla
		self.konec = False
		self.poteze = []

	def zamenjaj(self):
		self.nasportnik()
		self.na_potezi.igraj()

	def razveljavi(self):
		if len(self.poteze)>0:
			(i,j) = self.poteze.pop()
			self.tabela[j][i] = 0
		else:
			assert "Seznam je prazen"
		# manjka self.igraj ....
		# izbris krozca

	def shrani_pozicijo(self):
		p = [self.tabela[i][:] for i in range(19)]
		self.poteze.append((p, self.na_potezi))

	def kopija(self):
		"""Vrni kopijo te igre, brez zgodovine."""
		k = Igra(self.gui)
		k.tabela = [self.tabela[i][:] for i in range(19)]
		k.na_potezi = self.na_potezi
		k.poteze = self.poteze[:]
		return k

        #Ali je poteza pravilna (to metodo bo poklical Gui - self.igra.pravilna(i,j))
	def pravilna(self, i, j):
		"""Funkcija preveri ali je poteza pravilna"""
		return ((self.tabela[j][i] == 0) and not (self.konec))

	def preveri_konec(self):
		"""Funkcija ugotovi, če je tabela polna"""
		for i in self.tabela:
			if 0 in i:
				break
		else:
			self.konec = True

	def nasportnik(self):
		if self.na_potezi == self.igralec1:
			self.na_potezi = self.igralec2
		elif self.na_potezi == self.igralec2:
			self.na_potezi = self.igralec1
		else:
		#	self.na_potezi = self.igralec1
			assert False, "Neveljaven nasprotnik"

	def povleci(self, i, j):
		if self.pravilna(i,j):
			if self.na_potezi == self.igralec1:
				self.tabela[j][i] = CRNI
				self.poteze += [(i, j)]
				#print(self.tabela)
			else:
				self.tabela[j][i] = BELI
				self.poteze += [(i, j)]
			#self.na_potezi = self.na_potezi.nasprotnik()
			(kaj, kdo, kje) = self.preveri_zmago(i,j)
			if kaj:
				print(str(kdo.barva),str(kje))
				self.gui.narisi_crto(kje)
				# if kdo == self.igralec1:
				# 	print("KONEC  " + "Zmagal je CRNI" + "  " + str(kje))
				# else:
				# 	print("KONEC  " + "Zmagal je BELI" + "  " + str(kje))
				#self.ustavi
				#self.stanje = "Zmagal je" + self.preveri_zmago()[1]
				#self.stanje ko bo igra izenačena
			#else:
			#	self.nasportnik()
			#	self.na_potezi.igraj()

		#Ali je konec (to metodo bo poklical Gui - self.igra.preveri_konec(i,j))
	def preveri_zmago(self, j, i):
		###Pri vrstici in stolpcu lahko poljubno igraš naprej in bo računalo vedno nove zmagovalne petorke. Pri diagonali pa vrne
		###vedno prvo zmagovalno diagonalo ter barva krogca se ne spreminja.!!! (Če ustavimo igro ob zmagi, to ne bo problem!)
		"""Preveri, če je konec in vrne trojko (Bool, zmagovalec, zmagovalna petorka)"""
		dolzina = len(self.tabela)
		#Preveri vodoravno
		#Izracuna prvi in zadnji clen zmagovalne petorke
		#prvi = max(0, j-4)
		#zadnji = min(j+4, 18)
		vrstica = i
		for stolpec in range(dolzina-4):
			if self.tabela[vrstica][stolpec] != 0:
				ena = self.tabela[vrstica][stolpec]
				dva = self.tabela[vrstica][stolpec+1]
				tri = self.tabela[vrstica][stolpec+2]
				stiri = self.tabela[vrstica][stolpec+3]
				pet = self.tabela[vrstica][stolpec+4]
				#print("{0}{1}{2}{3}{4}".format(ena,dva,tri,stiri,pet))
				if ena == dva == tri == stiri == pet:
					self.konec = True
					return (True, self.na_potezi, \
							[(vrstica,stolpec),(vrstica,stolpec+1),(vrstica,stolpec+2),(vrstica,stolpec+3),(vrstica,stolpec+4)])

		# #Preveri navpčno
		# #prvi = max(0, i-4)
		# #zadnji = min(i+4, 18)
		stolpec = j
		for vrstica in range(dolzina-4):
			if self.tabela[vrstica][stolpec] != 0:
					ena = self.tabela[vrstica][stolpec]
					dva = self.tabela[vrstica+1][stolpec]
					tri = self.tabela[vrstica+2][stolpec]
					stiri = self.tabela[vrstica+3][stolpec]
					pet = self.tabela[vrstica+4][stolpec]
					if ena == dva == tri == stiri == pet:
						self.konec = True
						return (True, self.na_potezi,\
							[(vrstica,stolpec),(vrstica+1,stolpec),(vrstica+2,stolpec),(vrstica+3,stolpec),(vrstica+4,stolpec)])

		#Preveri prvo diagonalo
		# diag = j-i
		# for vrstica in range(15):
		# 	if (vrstica - diag < 15) and (vrstica - diag >= 0) and (self.tabela[vrstica][vrstica - diag] != 0):
		# 		ena = self.tabela[vrstica][vrstica - diag]
		# 		dva = self.tabela[vrstica+1][vrstica - diag +1]
		# 		tri = self.tabela[vrstica+2][vrstica - diag + 2]
		# 		stiri = self.tabela[vrstica+3][vrstica - diag + 3]
		# 		pet = self.tabela[vrstica+4][vrstica - diag + 4]
		# 		if ena == dva == tri == stiri == pet:
		# 			self.konec = True
		# 			return (True, self.na_potezi,\
		# 				[(vrstica,vrstica - diag),(vrstica+1,vrstica - diag +1),(vrstica+2,vrstica - diag +2),(vrstica+3,vrstica - diag +3),(vrstica+4,vrstica - diag +4)])

		# #Preveri drugo diagonalo
		# diag = i+j
		# for vrstica in range(15):
		# 	if (diag - vrstica < 15) and (diag - vrstica >= 0) and (self.tabela[vrstica][diag - vrstica] != 0):
		# 		ena = self.tabela[vrstica][diag - vrstica]
		# 		dva = self.tabela[vrstica+1][diag - vrstica +1]
		# 		tri = self.tabela[vrstica+2][diag - vrstica + 2]
		# 		stiri = self.tabela[vrstica+3][diag - vrstica + 3]
		# 		pet = self.tabela[vrstica+4][diag - vrstica + 4]
		# 		if ena == dva == tri == stiri == pet:
		# 			self.konec = True
		# 			return (True, self.na_potezi,\
		# 				[(vrstica,diag - vrstica),(vrstica+1,diag - vrstica +1),(vrstica+2,diag - vrstica +2),(vrstica+3,diag - vrstica +3),(vrstica+4,diag - vrstica +4)])

		# return (False, None, None)

		# for vrstica in range(dolzina):
		# 	for stolpec in range(dolzina-4):
		# 		if self.tabela[vrstica][stolpec] != 0:
		# 			ena = self.tabela[vrstica][stolpec]
		# 			dva = self.tabela[vrstica][stolpec+1]
		# 			tri = self.tabela[vrstica][stolpec+2]
		# 			stiri = self.tabela[vrstica][stolpec+3]
		# 			pet = self.tabela[vrstica][stolpec+4]
		# 			if ena == dva == tri == stiri == pet:
		# 				self.konec = True
		# 				# Kaj če je 6 ali več v vrsto??? V sezamu je le 5 elementov.
		# 				# Dovolj je vrniti le začetno in končno potezo ujemanja
		# 				return (True, self.na_potezi, \
		# 					[(vrstica,stolpec),(vrstica,stolpec+1),(vrstica,stolpec+2),(vrstica,stolpec+3),(vrstica,stolpec+4)])

		# for stolpec in range(dolzina):
		# 	for vrstica in range(dolzina-4):
		# 		if self.tabela[vrstica][stolpec] != 0:
		# 			ena = self.tabela[vrstica][stolpec]
		# 			dva = self.tabela[vrstica+1][stolpec]
		# 			tri = self.tabela[vrstica+2][stolpec]
		# 			stiri = self.tabela[vrstica+3][stolpec]
		# 			pet = self.tabela[vrstica+4][stolpec]
		# 			if ena == dva == tri == stiri == pet:
		# 				self.konec = True
		# 				return (True, self.na_potezi,\
		# 					[(vrstica,stolpec),(vrstica+1,stolpec),(vrstica+2,stolpec),(vrstica+3,stolpec),(vrstica+4,stolpec)])

		for vrstica in range(4, dolzina):
			for stolpec in range(0, dolzina-4):
				if self.tabela[vrstica][stolpec] != 0:
					ena = self.tabela[vrstica][stolpec]
					dva = self.tabela[vrstica-1][stolpec+1]
					tri = self.tabela[vrstica-2][stolpec+2]
					stiri = self.tabela[vrstica-3][stolpec+3]
					pet = self.tabela[vrstica-4][stolpec+4]
					if ena == dva == tri == stiri == pet:
						self.konec = True
						return (True, self.na_potezi,\
							[(vrstica,stolpec),(vrstica-1,stolpec+1),(vrstica-2,stolpec+2),(vrstica-3,stolpec+3),(vrstica-4,stolpec+4)])

		for vrstica in range(0, dolzina-4):
			for stolpec in range(0, dolzina-4):
				if self.tabela[vrstica][stolpec] != 0:
					ena = self.tabela[vrstica][stolpec]
					dva = self.tabela[vrstica+1][stolpec+1]
					tri = self.tabela[vrstica+2][stolpec+2]
					stiri = self.tabela[vrstica+3][stolpec+3]
					pet = self.tabela[vrstica+4][stolpec+4]
					if ena == dva == tri == stiri == pet:
						self.konec = True
						return (True, self.na_potezi,\
							[(vrstica,stolpec),(vrstica+1,stolpec+1),(vrstica+2,stolpec+2),(vrstica+3,stolpec+3),(vrstica+4,stolpec+4)])
		return (False, None, None)
<commit_msg>Izbrisal shrani_potezno, ker je ne rabiva<commit_after>from clovek import*
from racunalnik import*

CRNI = 1
BELI = 2

def matrika_nicel(n, m):
	"""
	Funkcija naredi matriko n x m ni
	"""
	sez = []
	for i in range(m):
		sez += [[0]*n]
	return sez

class Igra():
	def __init__(self, gui):
		self.tabela = matrika_nicel(19,19)
		self.igralec1 = Clovek(self, CRNI)#, Minimax(globina))
		self.igralec2 = Racunalnik(self, Minimax(globina), BELI)
		self.na_potezi = self.igralec1
		self.gui = gui

		#Mogoče, bova rabla
		self.konec = False
		self.poteze = []

	def zamenjaj(self):
		self.nasportnik()
		self.na_potezi.igraj()

	def razveljavi(self):
		if len(self.poteze)>0:
			(i,j) = self.poteze.pop()
			self.tabela[j][i] = 0
		else:
			assert "Seznam je prazen"
		# manjka self.igraj ....
		# izbris krozca

	def kopija(self):
		"""Vrni kopijo te igre, brez zgodovine."""
		k = Igra(self.gui)
		k.tabela = [self.tabela[i][:] for i in range(19)]
		k.na_potezi = self.na_potezi
		k.poteze = self.poteze[:]
		return k

        #Ali je poteza pravilna (to metodo bo poklical Gui - self.igra.pravilna(i,j))
	def pravilna(self, i, j):
		"""Funkcija preveri ali je poteza pravilna"""
		return ((self.tabela[j][i] == 0) and not (self.konec))

	def preveri_konec(self):
		"""Funkcija ugotovi, če je tabela polna"""
		for i in self.tabela:
			if 0 in i:
				break
		else:
			self.konec = True

	def nasportnik(self):
		if self.na_potezi == self.igralec1:
			self.na_potezi = self.igralec2
		elif self.na_potezi == self.igralec2:
			self.na_potezi = self.igralec1
		else:
		#	self.na_potezi = self.igralec1
			assert False, "Neveljaven nasprotnik"

	def povleci(self, i, j):
		if self.pravilna(i,j):
			self.tabela[j][i] = self.na_potezi.barva
			self.poteze += [(i, j)]

			(kaj, kdo, kje) = self.preveri_zmago(i,j)
			if kaj:
				print(str(kdo.barva),str(kje))
				self.gui.narisi_crto(kje)
				# if kdo == self.igralec1:
				# 	print("KONEC  " + "Zmagal je CRNI" + "  " + str(kje))
				# else:
				# 	print("KONEC  " + "Zmagal je BELI" + "  " + str(kje))
				#self.ustavi
				#self.stanje = "Zmagal je" + self.preveri_zmago()[1]
				#self.stanje ko bo igra izenačena
			#else:
			#	self.nasportnik()
			#	self.na_potezi.igraj()

		#Ali je konec (to metodo bo poklical Gui - self.igra.preveri_konec(i,j))
	def preveri_zmago(self, j, i):
		###Pri vrstici in stolpcu lahko poljubno igraš naprej in bo računalo vedno nove zmagovalne petorke. Pri diagonali pa vrne
		###vedno prvo zmagovalno diagonalo ter barva krogca se ne spreminja.!!! (Če ustavimo igro ob zmagi, to ne bo problem!)
		"""Preveri, če je konec in vrne trojko (Bool, zmagovalec, zmagovalna petorka)"""
		dolzina = len(self.tabela)
		#Preveri vodoravno
		#Izracuna prvi in zadnji clen zmagovalne petorke
		#prvi = max(0, j-4)
		#zadnji = min(j+4, 18)
		vrstica = i
		for stolpec in range(dolzina-4):
			if self.tabela[vrstica][stolpec] != 0:
				ena = self.tabela[vrstica][stolpec]
				dva = self.tabela[vrstica][stolpec+1]
				tri = self.tabela[vrstica][stolpec+2]
				stiri = self.tabela[vrstica][stolpec+3]
				pet = self.tabela[vrstica][stolpec+4]
				#print("{0}{1}{2}{3}{4}".format(ena,dva,tri,stiri,pet))
				if ena == dva == tri == stiri == pet:
					self.konec = True
					return (True, self.na_potezi, \
							[(vrstica,stolpec),(vrstica,stolpec+1),(vrstica,stolpec+2),(vrstica,stolpec+3),(vrstica,stolpec+4)])

		# #Preveri navpčno
		# #prvi = max(0, i-4)
		# #zadnji = min(i+4, 18)
		stolpec = j
		for vrstica in range(dolzina-4):
			if self.tabela[vrstica][stolpec] != 0:
					ena = self.tabela[vrstica][stolpec]
					dva = self.tabela[vrstica+1][stolpec]
					tri = self.tabela[vrstica+2][stolpec]
					stiri = self.tabela[vrstica+3][stolpec]
					pet = self.tabela[vrstica+4][stolpec]
					if ena == dva == tri == stiri == pet:
						self.konec = True
						return (True, self.na_potezi,\
							[(vrstica,stolpec),(vrstica+1,stolpec),(vrstica+2,stolpec),(vrstica+3,stolpec),(vrstica+4,stolpec)])

		#Preveri prvo diagonalo
		# diag = j-i
		# for vrstica in range(15):
		# 	if (vrstica - diag < 15) and (vrstica - diag >= 0) and (self.tabela[vrstica][vrstica - diag] != 0):
		# 		ena = self.tabela[vrstica][vrstica - diag]
		# 		dva = self.tabela[vrstica+1][vrstica - diag +1]
		# 		tri = self.tabela[vrstica+2][vrstica - diag + 2]
		# 		stiri = self.tabela[vrstica+3][vrstica - diag + 3]
		# 		pet = self.tabela[vrstica+4][vrstica - diag + 4]
		# 		if ena == dva == tri == stiri == pet:
		# 			self.konec = True
		# 			return (True, self.na_potezi,\
		# 				[(vrstica,vrstica - diag),(vrstica+1,vrstica - diag +1),(vrstica+2,vrstica - diag +2),(vrstica+3,vrstica - diag +3),(vrstica+4,vrstica - diag +4)])

		# #Preveri drugo diagonalo
		# diag = i+j
		# for vrstica in range(15):
		# 	if (diag - vrstica < 15) and (diag - vrstica >= 0) and (self.tabela[vrstica][diag - vrstica] != 0):
		# 		ena = self.tabela[vrstica][diag - vrstica]
		# 		dva = self.tabela[vrstica+1][diag - vrstica +1]
		# 		tri = self.tabela[vrstica+2][diag - vrstica + 2]
		# 		stiri = self.tabela[vrstica+3][diag - vrstica + 3]
		# 		pet = self.tabela[vrstica+4][diag - vrstica + 4]
		# 		if ena == dva == tri == stiri == pet:
		# 			self.konec = True
		# 			return (True, self.na_potezi,\
		# 				[(vrstica,diag - vrstica),(vrstica+1,diag - vrstica +1),(vrstica+2,diag - vrstica +2),(vrstica+3,diag - vrstica +3),(vrstica+4,diag - vrstica +4)])

		# return (False, None, None)

		# for vrstica in range(dolzina):
		# 	for stolpec in range(dolzina-4):
		# 		if self.tabela[vrstica][stolpec] != 0:
		# 			ena = self.tabela[vrstica][stolpec]
		# 			dva = self.tabela[vrstica][stolpec+1]
		# 			tri = self.tabela[vrstica][stolpec+2]
		# 			stiri = self.tabela[vrstica][stolpec+3]
		# 			pet = self.tabela[vrstica][stolpec+4]
		# 			if ena == dva == tri == stiri == pet:
		# 				self.konec = True
		# 				# Kaj če je 6 ali več v vrsto??? V sezamu je le 5 elementov.
		# 				# Dovolj je vrniti le začetno in končno potezo ujemanja
		# 				return (True, self.na_potezi, \
		# 					[(vrstica,stolpec),(vrstica,stolpec+1),(vrstica,stolpec+2),(vrstica,stolpec+3),(vrstica,stolpec+4)])

		# for stolpec in range(dolzina):
		# 	for vrstica in range(dolzina-4):
		# 		if self.tabela[vrstica][stolpec] != 0:
		# 			ena = self.tabela[vrstica][stolpec]
		# 			dva = self.tabela[vrstica+1][stolpec]
		# 			tri = self.tabela[vrstica+2][stolpec]
		# 			stiri = self.tabela[vrstica+3][stolpec]
		# 			pet = self.tabela[vrstica+4][stolpec]
		# 			if ena == dva == tri == stiri == pet:
		# 				self.konec = True
		# 				return (True, self.na_potezi,\
		# 					[(vrstica,stolpec),(vrstica+1,stolpec),(vrstica+2,stolpec),(vrstica+3,stolpec),(vrstica+4,stolpec)])

		for vrstica in range(4, dolzina):
			for stolpec in range(0, dolzina-4):
				if self.tabela[vrstica][stolpec] != 0:
					ena = self.tabela[vrstica][stolpec]
					dva = self.tabela[vrstica-1][stolpec+1]
					tri = self.tabela[vrstica-2][stolpec+2]
					stiri = self.tabela[vrstica-3][stolpec+3]
					pet = self.tabela[vrstica-4][stolpec+4]
					if ena == dva == tri == stiri == pet:
						self.konec = True
						return (True, self.na_potezi,\
							[(vrstica,stolpec),(vrstica-1,stolpec+1),(vrstica-2,stolpec+2),(vrstica-3,stolpec+3),(vrstica-4,stolpec+4)])

		for vrstica in range(0, dolzina-4):
			for stolpec in range(0, dolzina-4):
				if self.tabela[vrstica][stolpec] != 0:
					ena = self.tabela[vrstica][stolpec]
					dva = self.tabela[vrstica+1][stolpec+1]
					tri = self.tabela[vrstica+2][stolpec+2]
					stiri = self.tabela[vrstica+3][stolpec+3]
					pet = self.tabela[vrstica+4][stolpec+4]
					if ena == dva == tri == stiri == pet:
						self.konec = True
						return (True, self.na_potezi,\
							[(vrstica,stolpec),(vrstica+1,stolpec+1),(vrstica+2,stolpec+2),(vrstica+3,stolpec+3),(vrstica+4,stolpec+4)])
		return (False, None, None)
<|endoftext|>


<commit_before>from datetime import datetime
import operator
import random
import re

from db import _key, msetbit, sequential_id

# This is pretty restrictive, but we can always relax it later.
VALID_EXPERIMENT_ALTERNATIVE_RE = re.compile(r"^[a-z0-9][a-z0-9\-_ ]*$", re.I)
RANDOM_SAMPLE = .2

class Client(object):

    def __init__(self, client_id, redis_conn):
        self.redis = redis_conn
        self.client_id = client_id
        self._sequential_id = None

    @property
    def sequential_id(self):
        if self._sequential_id is None:
            self._sequential_id = sequential_id(
                'internal_user_ids',
                self.client_id)
        return self._sequential_id

class ExperimentCollection(object):

    def __init__(self, redis_conn):
        self.redis = redis_conn
        self.experiments = []

    def __iter__(self):
        self.experiments = []
        for exp_key in self.redis.smembers(_key('experiments')):
            self.experiments.append(exp_key)

        return self

    def __next__(self):
        for i in self.experiments:
            yield Experiment.find(i, self.redis)

class Experiment(object):

    def __init__(self, name, alternatives, redis_conn):
        if len(alternatives) < 2:
            raise ValueError('experiments require at least two alternatives')

        self.name = name
        self.alternatives = Experiment.initialize_alternatives(
            name,
            alternatives,
            redis_conn)
        self.redis = redis_conn
        self.random_sample = RANDOM_SAMPLE

    def __repr__(self):
        return '<Experiment: {0} (version: {1})>'.format(self.name, self.version())

    def save(self):
        pipe = self.redis.pipeline()
        if self.is_new_record():
            pipe.sadd(_key('experiments'), self.name)

            # Set version to zero
            pipe.set(_key("experiments:{0}".format(self.name)), 0)

            # Empty desc by default
            pipe.hset(self.key(), 'description', '')

        pipe.hset(self.key(), 'created_at', datetime.now())
        # reverse here and use lpush to keep consistent with using lrange
        for alternative in reversed(self.alternatives):
            pipe.lpush("{0}:alternatives".format(self.key()), alternative.name)

        pipe.execute()

    def control(self):
        return self.alternatives[0]

    def created_at(self):
        return self.redis.hget(self.key(), 'created_at')

    def get_alternative_names(self):
        return [alt.name for alt in self.alternatives]

    def is_new_record(self):
        return not self.redis.sismember(_key("experiments"), self.name)

    def total_participants(self):
        key = _key("participations:{0}:_all:all".format(self.rawkey()))
        return self.redis.bitcount(key)

    def total_conversions(self):
        key = _key("conversions:{0}:_all:users:all".format(self.rawkey()))
        return self.redis.bitcount(key)

    def update_description(self, description=None):
        self.redis.hset(self.key(), 'description', description or '')

    def get_description(self):
        return self.redis.hget(self.key(), 'description')

    def reset(self):
        self.increment_version()

        experiment = Experiment(self.name, self.get_alternative_names(), self.redis)
        experiment.save()

    def delete(self):
        pipe = self.redis.pipeline()
        pipe.srem(_key('experiments'), self.name)
        pipe.delete(self.key())
        pipe.delete(_key(self.rawkey()))
        pipe.delete(_key('experiments:{0}'.format(self.name)))

        # Consider a 'non-keys' implementation of this
        keys = self.redis.keys('*{0}*'.format(self.rawkey()))
        for key in keys:
            pipe.delete(key)

        pipe.execute()

    def archive(self):
        self.redis.hset(self.key(), 'archived', 1)

    def unarchive(self):
        self.redis.hdel(self.key(), 'archived')

    def is_archived(self):
        return self.redis.hexists(self.key(), 'archived')

    def version(self):
        version = self.redis.get(_key("experiments:{0}".format(self.name)))
        return int(version) if version else 0

    def increment_version(self):
        self.redis.incr(_key('experiments:{0}'.format(self.name)))

    def convert(self, client):
        alternative = self.get_alternative_by_client_id(client)

        if not alternative:  # TODO or has already converted?
            raise ValueError('this client was not participaing')

        alternative.record_conversion(client)

    def set_winner(self, alternative_name):
        if alternative_name not in self.get_alternative_names():
            raise ValueError('this alternative is not in this experiment')

        self.redis.set(self._winner_key, alternative_name)

    def has_winner(self):
        return self.redis.exists(self._winner_key)

    def get_winner(self):
        if self.has_winner():
            return self.redis.get(self._winner_key)

        return None

    def reset_winner(self):
        self.redis.delete(self._winner_key)

    @property
    def _winner_key(self):
        return "{0}:winner".format(self.key())

    def get_alternative(self, client):
        if self.is_archived():
            return self.control()

        chosen_alternative = self.get_alternative_by_client_id(client)
        if not chosen_alternative:
            chosen_alternative = self.choose_alternative(client=client)
            chosen_alternative.record_participation(client)

        return chosen_alternative

    def get_alternative_by_client_id(self, client):
        # TODO, THIS IS SCRATCH/PROTO
        # MOVE INTO A LUA SCRIPT
        for alternative in self.get_alternative_names():
            key = _key("participations:{0}:{1}:all".format(self.rawkey(), alternative))
            if self.redis.getbit(key, client.sequential_id):
                return Alternative(alternative, self.name, self.redis)

        return None

    def has_converted_by_client_id(self, client_id):
        # TODO, THIS IS SCRATCH/PROTO
        # MOVE INTO A LUA SCRIPT
        for alternative in self.get_alternative_names():
            key = _key("conversions:{0}:{1}:all".format(self.rawkey(), alternative))
            if self.redis.getbit(key, client_id):
                return True

        return False

    def choose_alternative(self, client=None):
        # This will be hooked up with some fun math-guy-steve stuff later
        # return self._random_choice()
        if random.random() < self.random_sample:
            return self._random_choice()
        else:
            return Alternative(self._whiplash(), self.name, self.redis)

    def _random_choice(self):
        return random.choice(self.alternatives)

    # my best attempt at implementing whiplash/multi-armed-bandit
    # math guy steve, help!
    def _whiplash(self):
        stats = {}
        for alternative in self.alternatives:
            participant_count = alternative.participant_count()
            completed_count = alternative.completed_count()
            stats[alternative.name] = self._arm_guess(participant_count, completed_count)

        return max(stats.iteritems(), key=operator.itemgetter(1))[0]

    def _arm_guess(self, participant_count, completed_count):
        fairness_score = 7

        a = max([participant_count, 0])
        b = max([participant_count - completed_count, 0])

        return random.betavariate(a + fairness_score, b + fairness_score)

    def rawkey(self):
        return "{0}/{1}".format(self.name, self.version())

    def key(self):
        key = "experiments:{0}".format(self.rawkey())
        return _key(key)

    @classmethod
    def find(cls, experiment_name, redis_conn):
        if redis_conn.sismember(_key("experiments"), experiment_name):
            return cls(experiment_name, Experiment.load_alternatives(experiment_name, redis_conn), redis_conn)
        else:
            raise ValueError('experiment does not exist')

    @classmethod
    def find_or_create(cls, experiment_name, alternatives, redis_conn):
        if len(alternatives) < 2:
            raise ValueError('experiments require at least two alternatives')

        # We don't use the class method key here
        if redis_conn.sismember(_key("experiments"), experiment_name):
            # Note during refactor:
            # We're not instanciating a new Experiment, rather than this load_alternatives hackery
            exp = Experiment.find(experiment_name, redis_conn)

            # get the existing alternatives
            current_alternatives = exp.get_alternative_names()

            # Make sure the alternative options are correct.
            # If they are not, then we have to make a new version
            # above `experiment` is then returned eventually
            if sorted(current_alternatives) != sorted(alternatives):
                exp.increment_version()

                # initialize a new one
                experiment = cls(experiment_name, alternatives, redis_conn)
                experiment.save()
            else:
                experiment = exp
        # completely new experiment
        else:
            experiment = cls(experiment_name, alternatives, redis_conn)
            experiment.save()

        return experiment

    @staticmethod
    def all(redis_conn, exclude_archived=True):
        experiments = []
        keys = redis_conn.smembers(_key('experiments'))

        for key in keys:
            experiment = Experiment.find(key, redis_conn)
            if experiment.is_archived() and exclude_archived:
                continue
            experiments.append(Experiment.find(key, redis_conn))

        return experiments

    @staticmethod
    def load_alternatives(experiment_name, redis_conn):
        # get latest version of experiment
        version = redis_conn.get(_key("experiments:{0}".format(experiment_name)))
        key = _key("experiments:{0}/{1}:alternatives".format(experiment_name, version))
        return redis_conn.lrange(key, 0, -1)

    @staticmethod
    def initialize_alternatives(experiment_name, alternatives, redis_conn):
        for alternative_name in alternatives:
            if not Alternative.is_valid(alternative_name):
                raise ValueError('invalid alternative name')

        return [Alternative(n, experiment_name, redis_conn) for n in alternatives]

    @staticmethod
    def is_valid(experiment_name):
        return (isinstance(experiment_name, basestring) and
            VALID_EXPERIMENT_ALTERNATIVE_RE.match(experiment_name) is not None)

class AlternativeCollection(object):

    def __init__(self, redis_conn):
        self.redis = redis_conn
        self.alternatives = []

    # def __iter__(self):
    #     self.alternatives = []
    #     for exp_key in self.redis.smembers(_key('experiments')):
    #         self.experiments.append(exp_key)

    #     return self

    # def __next__(self):
    #     for i in self.alternatives:
    #         yield Experiment.find(i)

class Alternative(object):

    def __init__(self, name, experiment_name, redis_conn):
        self.name = name
        self.experiment_name = experiment_name
        self.redis = redis_conn

    def __repr__(self):
        return "<Alternative {0} (Experiment {1})".format(self.name, self.experiment_name)

    def is_control(self):
        return self.experiment().control().name == self.name

    def is_winner(self):
        return self.experiment().has_winner() and self.experiment().get_winner() == self.name

    def experiment(self):
        return Experiment.find(self.experiment_name, self.redis)

    def participant_count(self):
        key = _key("participations:{0}:{1}:all".format(self.experiment().rawkey(), self.name))
        return self.redis.bitcount(key)

    def completed_count(self):
        key = _key("conversions:{0}:{1}:users:all".format(self.experiment().rawkey(), self.name))
        return self.redis.bitcount(key)

    def record_participation(self, client):
        """Record a user's participation in a test along with a given variation"""
        date = datetime.now()
        experiment_key = self.experiment().rawkey()

        keys = [
            _key("participations:{0}:_all:all".format(experiment_key)),
            _key("participations:{0}:_all:{1}".format(experiment_key, date.strftime('%Y'))),
            _key("participations:{0}:_all:{1}".format(experiment_key, date.strftime('%Y-%m'))),
            _key("participations:{0}:_all:{1}".format(experiment_key, date.strftime('%Y-%m-%d'))),
            _key("participations:{0}:{1}:all".format(experiment_key, self.name)),
            _key("participations:{0}:{1}:{2}".format(experiment_key, self.name, date.strftime('%Y'))),
            _key("participations:{0}:{1}:{2}".format(experiment_key, self.name, date.strftime('%Y-%m'))),
            _key("participations:{0}:{1}:{2}".format(experiment_key, self.name, date.strftime('%Y-%m-%d'))),
        ]
        msetbit(keys=keys, args=([client.sequential_id, 1] * len(keys)))

    def record_conversion(self, client):
        """Record a user's conversion in a test along with a given variation"""
        date = datetime.now()
        experiment_key = self.experiment().rawkey()

        keys = [
            _key("conversions:{0}:_all:users:all".format(experiment_key)),
            _key("conversions:{0}:_all:users:{1}".format(experiment_key, date.strftime('%Y'))),
            _key("conversions:{0}:_all:users:{1}".format(experiment_key, date.strftime('%Y-%m'))),
            _key("conversions:{0}:_all:users:{1}".format(experiment_key, date.strftime('%Y-%m-%d'))),
            _key("conversions:{0}:{1}:users:all".format(experiment_key, self.name)),
            _key("conversions:{0}:{1}:users:{2}".format(experiment_key, self.name, date.strftime('%Y'))),
            _key("conversions:{0}:{1}:users:{2}".format(experiment_key, self.name, date.strftime('%Y-%m'))),
            _key("conversions:{0}:{1}:users:{2}".format(experiment_key, self.name, date.strftime('%Y-%m-%d'))),
        ]
        msetbit(keys=keys, args=([client.sequential_id, 1] * len(keys)))

    def conversion_rate(self):
        try:
            return self.completed_count() / float(self.participant_count())
        except ZeroDivisionError:
            return 0

    # Hacky z-score from https://github.com/andrew/split/blob/master/lib/split/alternative.rb
    def z_score(self):

        if self.is_control():
            return 'N/A'

        control = self.experiment().control()
        ctr_e = self.conversion_rate()
        ctr_c = control.conversion_rate()

        e = self.participant_count()
        c = control.participant_count()

        try:
            std_dev = pow(((ctr_e / pow(ctr_c, 3)) * ((e*ctr_e)+(c*ctr_c)-(ctr_c*ctr_e)*(c+e))/(c*e)), 0.5)
            return ((ctr_e / ctr_c) - 1) / std_dev
        except ZeroDivisionError:
            return 0

    def confidence_level(self):
        z_score = self.z_score()
        if z_score == 'N/A':
            return z_score

        z_score = abs(round(z_score, 3))

        ret = ''
        if z_score == 0.0:
            ret = 'No Change'
        elif z_score < 1.645:
            ret = 'No Confidence'
        elif z_score < 1.96:
            ret = '95% Confidence'
        elif z_score < 2.57:
            ret = '99% Confidence'
        else:
            ret = '99.9% Confidence'

        return ret

    def key(self):
        return _key("{0}:{1}".format(self.experiment_name, self.name))

    @staticmethod
    def number_to_percent(number, precision=2):
        return round(number * 100, precision)

    @staticmethod
    def is_valid(alternative_name):
        return (isinstance(alternative_name, basestring) and
            VALID_EXPERIMENT_ALTERNATIVE_RE.match(alternative_name) is not None)
<commit_msg>disable whiplash<commit_after>from datetime import datetime
import operator
import random
import re

from db import _key, msetbit, sequential_id

# This is pretty restrictive, but we can always relax it later.
VALID_EXPERIMENT_ALTERNATIVE_RE = re.compile(r"^[a-z0-9][a-z0-9\-_ ]*$", re.I)
RANDOM_SAMPLE = .2

class Client(object):

    def __init__(self, client_id, redis_conn):
        self.redis = redis_conn
        self.client_id = client_id
        self._sequential_id = None

    @property
    def sequential_id(self):
        if self._sequential_id is None:
            self._sequential_id = sequential_id(
                'internal_user_ids',
                self.client_id)
        return self._sequential_id

class ExperimentCollection(object):

    def __init__(self, redis_conn):
        self.redis = redis_conn
        self.experiments = []

    def __iter__(self):
        self.experiments = []
        for exp_key in self.redis.smembers(_key('experiments')):
            self.experiments.append(exp_key)

        return self

    def __next__(self):
        for i in self.experiments:
            yield Experiment.find(i, self.redis)

class Experiment(object):

    def __init__(self, name, alternatives, redis_conn):
        if len(alternatives) < 2:
            raise ValueError('experiments require at least two alternatives')

        self.name = name
        self.alternatives = Experiment.initialize_alternatives(
            name,
            alternatives,
            redis_conn)
        self.redis = redis_conn
        self.random_sample = RANDOM_SAMPLE

    def __repr__(self):
        return '<Experiment: {0} (version: {1})>'.format(self.name, self.version())

    def save(self):
        pipe = self.redis.pipeline()
        if self.is_new_record():
            pipe.sadd(_key('experiments'), self.name)

            # Set version to zero
            pipe.set(_key("experiments:{0}".format(self.name)), 0)

            # Empty desc by default
            pipe.hset(self.key(), 'description', '')

        pipe.hset(self.key(), 'created_at', datetime.now())
        # reverse here and use lpush to keep consistent with using lrange
        for alternative in reversed(self.alternatives):
            pipe.lpush("{0}:alternatives".format(self.key()), alternative.name)

        pipe.execute()

    def control(self):
        return self.alternatives[0]

    def created_at(self):
        return self.redis.hget(self.key(), 'created_at')

    def get_alternative_names(self):
        return [alt.name for alt in self.alternatives]

    def is_new_record(self):
        return not self.redis.sismember(_key("experiments"), self.name)

    def total_participants(self):
        key = _key("participations:{0}:_all:all".format(self.rawkey()))
        return self.redis.bitcount(key)

    def total_conversions(self):
        key = _key("conversions:{0}:_all:users:all".format(self.rawkey()))
        return self.redis.bitcount(key)

    def update_description(self, description=None):
        self.redis.hset(self.key(), 'description', description or '')

    def get_description(self):
        return self.redis.hget(self.key(), 'description')

    def reset(self):
        self.increment_version()

        experiment = Experiment(self.name, self.get_alternative_names(), self.redis)
        experiment.save()

    def delete(self):
        pipe = self.redis.pipeline()
        pipe.srem(_key('experiments'), self.name)
        pipe.delete(self.key())
        pipe.delete(_key(self.rawkey()))
        pipe.delete(_key('experiments:{0}'.format(self.name)))

        # Consider a 'non-keys' implementation of this
        keys = self.redis.keys('*{0}*'.format(self.rawkey()))
        for key in keys:
            pipe.delete(key)

        pipe.execute()

    def archive(self):
        self.redis.hset(self.key(), 'archived', 1)

    def unarchive(self):
        self.redis.hdel(self.key(), 'archived')

    def is_archived(self):
        return self.redis.hexists(self.key(), 'archived')

    def version(self):
        version = self.redis.get(_key("experiments:{0}".format(self.name)))
        return int(version) if version else 0

    def increment_version(self):
        self.redis.incr(_key('experiments:{0}'.format(self.name)))

    def convert(self, client):
        alternative = self.get_alternative_by_client_id(client)

        if not alternative:  # TODO or has already converted?
            raise ValueError('this client was not participaing')

        alternative.record_conversion(client)

    def set_winner(self, alternative_name):
        if alternative_name not in self.get_alternative_names():
            raise ValueError('this alternative is not in this experiment')

        self.redis.set(self._winner_key, alternative_name)

    def has_winner(self):
        return self.redis.exists(self._winner_key)

    def get_winner(self):
        if self.has_winner():
            return self.redis.get(self._winner_key)

        return None

    def reset_winner(self):
        self.redis.delete(self._winner_key)

    @property
    def _winner_key(self):
        return "{0}:winner".format(self.key())

    def get_alternative(self, client):
        if self.is_archived():
            return self.control()

        chosen_alternative = self.get_alternative_by_client_id(client)
        if not chosen_alternative:
            chosen_alternative = self.choose_alternative(client=client)
            chosen_alternative.record_participation(client)

        return chosen_alternative

    def get_alternative_by_client_id(self, client):
        # TODO, THIS IS SCRATCH/PROTO
        # MOVE INTO A LUA SCRIPT
        for alternative in self.get_alternative_names():
            key = _key("participations:{0}:{1}:all".format(self.rawkey(), alternative))
            if self.redis.getbit(key, client.sequential_id):
                return Alternative(alternative, self.name, self.redis)

        return None

    def has_converted_by_client_id(self, client_id):
        # TODO, THIS IS SCRATCH/PROTO
        # MOVE INTO A LUA SCRIPT
        for alternative in self.get_alternative_names():
            key = _key("conversions:{0}:{1}:all".format(self.rawkey(), alternative))
            if self.redis.getbit(key, client_id):
                return True

        return False

    def choose_alternative(self, client=None):
        # This will be hooked up with some fun math-guy-steve stuff later
        # return self._random_choice()
        # if random.random() < self.random_sample:
        return self._random_choice()
        #else:
        #    return Alternative(self._whiplash(), self.name, self.redis)

    def _random_choice(self):
        return random.choice(self.alternatives)

    # my best attempt at implementing whiplash/multi-armed-bandit
    # math guy steve, help!
    def _whiplash(self):
        stats = {}
        for alternative in self.alternatives:
            participant_count = alternative.participant_count()
            completed_count = alternative.completed_count()
            stats[alternative.name] = self._arm_guess(participant_count, completed_count)

        return max(stats.iteritems(), key=operator.itemgetter(1))[0]

    def _arm_guess(self, participant_count, completed_count):
        fairness_score = 7

        a = max([participant_count, 0])
        b = max([participant_count - completed_count, 0])

        return random.betavariate(a + fairness_score, b + fairness_score)

    def rawkey(self):
        return "{0}/{1}".format(self.name, self.version())

    def key(self):
        key = "experiments:{0}".format(self.rawkey())
        return _key(key)

    @classmethod
    def find(cls, experiment_name, redis_conn):
        if redis_conn.sismember(_key("experiments"), experiment_name):
            return cls(experiment_name, Experiment.load_alternatives(experiment_name, redis_conn), redis_conn)
        else:
            raise ValueError('experiment does not exist')

    @classmethod
    def find_or_create(cls, experiment_name, alternatives, redis_conn):
        if len(alternatives) < 2:
            raise ValueError('experiments require at least two alternatives')

        # We don't use the class method key here
        if redis_conn.sismember(_key("experiments"), experiment_name):
            # Note during refactor:
            # We're not instanciating a new Experiment, rather than this load_alternatives hackery
            exp = Experiment.find(experiment_name, redis_conn)

            # get the existing alternatives
            current_alternatives = exp.get_alternative_names()

            # Make sure the alternative options are correct.
            # If they are not, then we have to make a new version
            # above `experiment` is then returned eventually
            if sorted(current_alternatives) != sorted(alternatives):
                exp.increment_version()

                # initialize a new one
                experiment = cls(experiment_name, alternatives, redis_conn)
                experiment.save()
            else:
                experiment = exp
        # completely new experiment
        else:
            experiment = cls(experiment_name, alternatives, redis_conn)
            experiment.save()

        return experiment

    @staticmethod
    def all(redis_conn, exclude_archived=True):
        experiments = []
        keys = redis_conn.smembers(_key('experiments'))

        for key in keys:
            experiment = Experiment.find(key, redis_conn)
            if experiment.is_archived() and exclude_archived:
                continue
            experiments.append(Experiment.find(key, redis_conn))

        return experiments

    @staticmethod
    def load_alternatives(experiment_name, redis_conn):
        # get latest version of experiment
        version = redis_conn.get(_key("experiments:{0}".format(experiment_name)))
        key = _key("experiments:{0}/{1}:alternatives".format(experiment_name, version))
        return redis_conn.lrange(key, 0, -1)

    @staticmethod
    def initialize_alternatives(experiment_name, alternatives, redis_conn):
        for alternative_name in alternatives:
            if not Alternative.is_valid(alternative_name):
                raise ValueError('invalid alternative name')

        return [Alternative(n, experiment_name, redis_conn) for n in alternatives]

    @staticmethod
    def is_valid(experiment_name):
        return (isinstance(experiment_name, basestring) and
            VALID_EXPERIMENT_ALTERNATIVE_RE.match(experiment_name) is not None)

class AlternativeCollection(object):

    def __init__(self, redis_conn):
        self.redis = redis_conn
        self.alternatives = []

    # def __iter__(self):
    #     self.alternatives = []
    #     for exp_key in self.redis.smembers(_key('experiments')):
    #         self.experiments.append(exp_key)

    #     return self

    # def __next__(self):
    #     for i in self.alternatives:
    #         yield Experiment.find(i)

class Alternative(object):

    def __init__(self, name, experiment_name, redis_conn):
        self.name = name
        self.experiment_name = experiment_name
        self.redis = redis_conn

    def __repr__(self):
        return "<Alternative {0} (Experiment {1})".format(self.name, self.experiment_name)

    def is_control(self):
        return self.experiment().control().name == self.name

    def is_winner(self):
        return self.experiment().has_winner() and self.experiment().get_winner() == self.name

    def experiment(self):
        return Experiment.find(self.experiment_name, self.redis)

    def participant_count(self):
        key = _key("participations:{0}:{1}:all".format(self.experiment().rawkey(), self.name))
        return self.redis.bitcount(key)

    def completed_count(self):
        key = _key("conversions:{0}:{1}:users:all".format(self.experiment().rawkey(), self.name))
        return self.redis.bitcount(key)

    def record_participation(self, client):
        """Record a user's participation in a test along with a given variation"""
        date = datetime.now()
        experiment_key = self.experiment().rawkey()

        keys = [
            _key("participations:{0}:_all:all".format(experiment_key)),
            _key("participations:{0}:_all:{1}".format(experiment_key, date.strftime('%Y'))),
            _key("participations:{0}:_all:{1}".format(experiment_key, date.strftime('%Y-%m'))),
            _key("participations:{0}:_all:{1}".format(experiment_key, date.strftime('%Y-%m-%d'))),
            _key("participations:{0}:{1}:all".format(experiment_key, self.name)),
            _key("participations:{0}:{1}:{2}".format(experiment_key, self.name, date.strftime('%Y'))),
            _key("participations:{0}:{1}:{2}".format(experiment_key, self.name, date.strftime('%Y-%m'))),
            _key("participations:{0}:{1}:{2}".format(experiment_key, self.name, date.strftime('%Y-%m-%d'))),
        ]
        msetbit(keys=keys, args=([client.sequential_id, 1] * len(keys)))

    def record_conversion(self, client):
        """Record a user's conversion in a test along with a given variation"""
        date = datetime.now()
        experiment_key = self.experiment().rawkey()

        keys = [
            _key("conversions:{0}:_all:users:all".format(experiment_key)),
            _key("conversions:{0}:_all:users:{1}".format(experiment_key, date.strftime('%Y'))),
            _key("conversions:{0}:_all:users:{1}".format(experiment_key, date.strftime('%Y-%m'))),
            _key("conversions:{0}:_all:users:{1}".format(experiment_key, date.strftime('%Y-%m-%d'))),
            _key("conversions:{0}:{1}:users:all".format(experiment_key, self.name)),
            _key("conversions:{0}:{1}:users:{2}".format(experiment_key, self.name, date.strftime('%Y'))),
            _key("conversions:{0}:{1}:users:{2}".format(experiment_key, self.name, date.strftime('%Y-%m'))),
            _key("conversions:{0}:{1}:users:{2}".format(experiment_key, self.name, date.strftime('%Y-%m-%d'))),
        ]
        msetbit(keys=keys, args=([client.sequential_id, 1] * len(keys)))

    def conversion_rate(self):
        try:
            return self.completed_count() / float(self.participant_count())
        except ZeroDivisionError:
            return 0

    # Hacky z-score from https://github.com/andrew/split/blob/master/lib/split/alternative.rb
    def z_score(self):

        if self.is_control():
            return 'N/A'

        control = self.experiment().control()
        ctr_e = self.conversion_rate()
        ctr_c = control.conversion_rate()

        e = self.participant_count()
        c = control.participant_count()

        try:
            std_dev = pow(((ctr_e / pow(ctr_c, 3)) * ((e*ctr_e)+(c*ctr_c)-(ctr_c*ctr_e)*(c+e))/(c*e)), 0.5)
            return ((ctr_e / ctr_c) - 1) / std_dev
        except ZeroDivisionError:
            return 0

    def confidence_level(self):
        z_score = self.z_score()
        if z_score == 'N/A':
            return z_score

        z_score = abs(round(z_score, 3))

        ret = ''
        if z_score == 0.0:
            ret = 'No Change'
        elif z_score < 1.645:
            ret = 'No Confidence'
        elif z_score < 1.96:
            ret = '95% Confidence'
        elif z_score < 2.57:
            ret = '99% Confidence'
        else:
            ret = '99.9% Confidence'

        return ret

    def key(self):
        return _key("{0}:{1}".format(self.experiment_name, self.name))

    @staticmethod
    def number_to_percent(number, precision=2):
        return round(number * 100, precision)

    @staticmethod
    def is_valid(alternative_name):
        return (isinstance(alternative_name, basestring) and
            VALID_EXPERIMENT_ALTERNATIVE_RE.match(alternative_name) is not None)
<|endoftext|>


<commit_before>import os
import re
from socket import inet_aton
import sys
from urllib import unquote

import dateutil.parser
from redis import ConnectionError
from werkzeug.wrappers import Request, Response
from werkzeug.routing import Map, Rule
from werkzeug.exceptions import HTTPException, NotFound
from werkzeug.datastructures import Headers

from . import __version__
from api import participate, convert

from config import CONFIG as cfg
from metrics import init_statsd
from utils import to_bool

try:
    import db
except ConnectionError:
    print "Redis is currently unavailable or misconfigured"
    sys.exit()

from models import Experiment, Client
from utils import service_unavailable_on_connection_error, json_error, json_success

class CORSMiddleware(object):
    """Add Cross-origin resource sharing headers to every request."""

    def __init__(self, app, origin=None):
        self.app = app
        self.redis = self.app.redis
        self.statsd = self.app.statsd
        self.config = self.app.config

        self.origin = origin or cfg.get("cors_origin")
        self.origin_regexp = None
        if self.origin is not None and self.origin != '*':
            self.origin_regexp = re.compile(self.origin.replace("*", "(.*)"))

    def __call__(self, environ, start_response):

        def get_origin(status, headers):
            if self.origin == '*':
                return self.origin
            origin = environ.get("HTTP_ORIGIN", "")
            return origin if self.origin_regexp.match(origin) else "null"

        def add_cors_headers(status, headers, exc_info=None):
            headers = Headers(headers)
            headers.add("Access-Control-Allow-Origin",
                        get_origin(status, headers))
            headers.add("Access-Control-Allow-Headers",
                        cfg.get("cors_headers"))
            headers.add("Access-Control-Allow-Credentials",
                        cfg.get("cors_credentials"))
            headers.add("Access-Control-Allow-Methods",
                        cfg.get("cors_methods"))
            headers.add("Access-Control-Expose-Headers",
                        cfg.get("cors_expose_headers"))
            return start_response(status, headers.to_list(), exc_info)

        if environ.get("REQUEST_METHOD") == "OPTIONS":
            add_cors_headers("200 Ok", [("Content-Type", "text/plain")])
            return [b'200 Ok']

        return self.app(environ, add_cors_headers)

class Sixpack(object):

    def __init__(self, redis_conn):
        self.redis = redis_conn
        self.statsd = init_statsd(cfg) if cfg.get('metrics') else None

        self.config = cfg

        self.url_map = Map([
            Rule('/', endpoint='home'),
            Rule('/_status', endpoint='status'),
            Rule('/participate', endpoint='participate'),
            Rule('/convert', endpoint='convert'),
            Rule('/experiments/<name>', endpoint='experiment_details'),
            Rule('/favicon.ico', endpoint='favicon')
        ])

    def __call__(self, environ, start_response):
        return self.wsgi_app(environ, start_response)

    def wsgi_app(self, environ, start_response):
        request = Request(environ)
        if self.config.get('metrics'):
            dispatcher = self.dispatch_request_with_metrics
        else:
            dispatcher = self.dispatch_request
        response = dispatcher(request)
        return response(environ, start_response)

    def dispatch_request(self, request):
        adapter = self.url_map.bind_to_environ(request.environ)
        try:
            endpoint, values = adapter.match()
            return getattr(self, 'on_' + endpoint)(request, **values)
        except NotFound:
            return json_error({"message": "not found"}, request, 404)
        except HTTPException:
            return json_error({"message": "an internal error has occurred"}, request, 500)

    def _incr_status_code(self, code):
        self.statsd.incr('response_code.{}'.format(code))

    def dispatch_request_with_metrics(self, request):
        adapter = self.url_map.bind_to_environ(request.environ)
        try:
            endpoint, values = adapter.match()
            with self.statsd.timer('{}.response_time'.format(endpoint)):
                response = getattr(self, 'on_' + endpoint)(request, **values)
                self.statsd.incr('{}.count'.format(endpoint))
                self._incr_status_code(response.status_code)
                return response
        except NotFound:
            self._incr_status_code(404)
            return json_error({"message": "not found"}, request, 404)
        except HTTPException:
            self._incr_status_code(500)
            return json_error({"message": "an internal error has occurred"}, request, 500)

    @service_unavailable_on_connection_error
    def on_status(self, request):
        self.redis.ping()
        return json_success({'version': __version__}, request)

    def on_home(self, request):
        dales = """
                 ,-"-.__,-"-.__,-"-..
                ( C>  )( C>  )( C>  ))
               /.`-_-'||`-_-'||`-_-'/
              /-"-.--,-"-.--,-"-.--/|
             ( C>  )( C>  )( C>  )/ |
            (|`-_-',.`-_-',.`-_-'/  |
             `-----++-----++----'|  |
             |     ||     ||     |-'
             |     ||     ||     |
             |     ||     ||     |
              `-_-'  `-_-'  `-_-'
        https://github.com/seatgeek/sixpack"""
        return Response(dales)

    def on_favicon(self, request):
        return Response()

    @service_unavailable_on_connection_error
    def on_convert(self, request):
        if should_exclude_visitor(request):
            return json_success({'excluded': 'true'}, request)

        experiment_name = request.args.get('experiment')
        client_id = request.args.get('client_id')
        kpi = request.args.get('kpi', None)

        if client_id is None or experiment_name is None:
            return json_error({'message': 'missing arguments'}, request, 400)

        dt = None
        if request.args.get("datetime"):
            dt = dateutil.parser.parse(request.args.get("datetime"))

        try:
            alt = convert(experiment_name, client_id, kpi=kpi,
                          datetime=dt, redis=self.redis)
        except ValueError as e:
            return json_error({'message': str(e)}, request, 400)

        resp = {
            'alternative': {
                'name': alt.name
            },
            'experiment': {
                'name': alt.experiment.name,
            },
            'conversion': {
                'value': None,
                'kpi': kpi
            },
            'client_id': client_id
        }

        return json_success(resp, request)

    @service_unavailable_on_connection_error
    def on_participate(self, request):
        alts = request.args.getlist('alternatives')
        experiment_name = request.args.get('experiment')
        force = request.args.get('force')
        record_force = to_bool(request.args.get('record_force', 'false'))
        client_id = request.args.get('client_id')
        traffic_fraction = request.args.get('traffic_fraction')

        if traffic_fraction is not None:
            traffic_fraction = float(traffic_fraction)
        prefetch = to_bool(request.args.get('prefetch', 'false'))

        if client_id is None or experiment_name is None or alts is None:
            return json_error({'message': 'missing arguments'}, request, 400)

        dt = None
        if request.args.get("datetime"):
            dt = dateutil.parser.parse(request.args.get("datetime"))
        try:
            if should_exclude_visitor(request):
                exp = Experiment.find(experiment_name, redis=self.redis)
                if exp.winner is not None:
                    alt = exp.winner
                else:
                    alt = exp.control
            else:
                alt = participate(experiment_name, alts, client_id,
                                  force=force, record_force=record_force,
                                  traffic_fraction=traffic_fraction,
                                  prefetch=prefetch, datetime=dt, redis=self.redis)
        except ValueError as e:
            return json_error({'message': str(e)}, request, 400)

        resp = {
            'alternative': {
                'name': alt.name
            },
            'experiment': {
                'name': alt.experiment.name,
            },
            'client_id': client_id,
            'status': 'ok'
        }

        return json_success(resp, request)

    @service_unavailable_on_connection_error
    def on_experiment_details(self, request, name):
        exp = Experiment.find(name, redis=self.redis)
        if exp is None:
            return json_error({'message': 'experiment not found'}, request, 404)

        return json_success(exp.objectify_by_period('day', True), request)

def should_exclude_visitor(request):
    user_agent = request.args.get('user_agent')
    ip_address = request.args.get('ip_address')

    return is_robot(user_agent) or is_ignored_ip(ip_address)

def is_robot(user_agent):
    if user_agent is None:
        return False
    regex = re.compile(r"{0}".format(cfg.get('robot_regex')), re.I)
    return regex.search(unquote(user_agent)) is not None

def is_ignored_ip(ip_address):
    # Ignore invalid/local IP addresses
    try:
        inet_aton(unquote(ip_address))
    except:
        return False  # TODO Same as above not sure of default

    return unquote(ip_address) in cfg.get('ignored_ip_addresses')

# Method to run with built-in server
def create_app():
    app = Sixpack(db.REDIS)
    return CORSMiddleware(app)

#  Method to run with gunicorn
def start(environ, start_response):
    return create_app()(environ, start_response)
<commit_msg>Fix AttributeError on line 51 (#280)<commit_after>import os
import re
from socket import inet_aton
import sys
from urllib import unquote

import dateutil.parser
from redis import ConnectionError
from werkzeug.wrappers import Request, Response
from werkzeug.routing import Map, Rule
from werkzeug.exceptions import HTTPException, NotFound
from werkzeug.datastructures import Headers

from . import __version__
from api import participate, convert

from config import CONFIG as cfg
from metrics import init_statsd
from utils import to_bool

try:
    import db
except ConnectionError:
    print "Redis is currently unavailable or misconfigured"
    sys.exit()

from models import Experiment, Client
from utils import service_unavailable_on_connection_error, json_error, json_success

class CORSMiddleware(object):
    """Add Cross-origin resource sharing headers to every request."""

    def __init__(self, app, origin=None):
        self.app = app
        self.redis = self.app.redis
        self.statsd = self.app.statsd
        self.config = self.app.config

        self.origin = origin or cfg.get("cors_origin")
        self.origin_regexp = None
        if self.origin is not None and self.origin != '*':
            self.origin_regexp = re.compile(self.origin.replace("*", "(.*)"))

    def __call__(self, environ, start_response):

        def get_origin(status, headers):
            if self.origin == '*' or self.origin is None:
                return self.origin
            origin = environ.get("HTTP_ORIGIN", "")
            return origin if self.origin_regexp.match(origin) else "null"

        def add_cors_headers(status, headers, exc_info=None):
            headers = Headers(headers)
            headers.add("Access-Control-Allow-Origin",
                        get_origin(status, headers))
            headers.add("Access-Control-Allow-Headers",
                        cfg.get("cors_headers"))
            headers.add("Access-Control-Allow-Credentials",
                        cfg.get("cors_credentials"))
            headers.add("Access-Control-Allow-Methods",
                        cfg.get("cors_methods"))
            headers.add("Access-Control-Expose-Headers",
                        cfg.get("cors_expose_headers"))
            return start_response(status, headers.to_list(), exc_info)

        if environ.get("REQUEST_METHOD") == "OPTIONS":
            add_cors_headers("200 Ok", [("Content-Type", "text/plain")])
            return [b'200 Ok']

        return self.app(environ, add_cors_headers)

class Sixpack(object):

    def __init__(self, redis_conn):
        self.redis = redis_conn
        self.statsd = init_statsd(cfg) if cfg.get('metrics') else None

        self.config = cfg

        self.url_map = Map([
            Rule('/', endpoint='home'),
            Rule('/_status', endpoint='status'),
            Rule('/participate', endpoint='participate'),
            Rule('/convert', endpoint='convert'),
            Rule('/experiments/<name>', endpoint='experiment_details'),
            Rule('/favicon.ico', endpoint='favicon')
        ])

    def __call__(self, environ, start_response):
        return self.wsgi_app(environ, start_response)

    def wsgi_app(self, environ, start_response):
        request = Request(environ)
        if self.config.get('metrics'):
            dispatcher = self.dispatch_request_with_metrics
        else:
            dispatcher = self.dispatch_request
        response = dispatcher(request)
        return response(environ, start_response)

    def dispatch_request(self, request):
        adapter = self.url_map.bind_to_environ(request.environ)
        try:
            endpoint, values = adapter.match()
            return getattr(self, 'on_' + endpoint)(request, **values)
        except NotFound:
            return json_error({"message": "not found"}, request, 404)
        except HTTPException:
            return json_error({"message": "an internal error has occurred"}, request, 500)

    def _incr_status_code(self, code):
        self.statsd.incr('response_code.{}'.format(code))

    def dispatch_request_with_metrics(self, request):
        adapter = self.url_map.bind_to_environ(request.environ)
        try:
            endpoint, values = adapter.match()
            with self.statsd.timer('{}.response_time'.format(endpoint)):
                response = getattr(self, 'on_' + endpoint)(request, **values)
                self.statsd.incr('{}.count'.format(endpoint))
                self._incr_status_code(response.status_code)
                return response
        except NotFound:
            self._incr_status_code(404)
            return json_error({"message": "not found"}, request, 404)
        except HTTPException:
            self._incr_status_code(500)
            return json_error({"message": "an internal error has occurred"}, request, 500)

    @service_unavailable_on_connection_error
    def on_status(self, request):
        self.redis.ping()
        return json_success({'version': __version__}, request)

    def on_home(self, request):
        dales = """
                 ,-"-.__,-"-.__,-"-..
                ( C>  )( C>  )( C>  ))
               /.`-_-'||`-_-'||`-_-'/
              /-"-.--,-"-.--,-"-.--/|
             ( C>  )( C>  )( C>  )/ |
            (|`-_-',.`-_-',.`-_-'/  |
             `-----++-----++----'|  |
             |     ||     ||     |-'
             |     ||     ||     |
             |     ||     ||     |
              `-_-'  `-_-'  `-_-'
        https://github.com/seatgeek/sixpack"""
        return Response(dales)

    def on_favicon(self, request):
        return Response()

    @service_unavailable_on_connection_error
    def on_convert(self, request):
        if should_exclude_visitor(request):
            return json_success({'excluded': 'true'}, request)

        experiment_name = request.args.get('experiment')
        client_id = request.args.get('client_id')
        kpi = request.args.get('kpi', None)

        if client_id is None or experiment_name is None:
            return json_error({'message': 'missing arguments'}, request, 400)

        dt = None
        if request.args.get("datetime"):
            dt = dateutil.parser.parse(request.args.get("datetime"))

        try:
            alt = convert(experiment_name, client_id, kpi=kpi,
                          datetime=dt, redis=self.redis)
        except ValueError as e:
            return json_error({'message': str(e)}, request, 400)

        resp = {
            'alternative': {
                'name': alt.name
            },
            'experiment': {
                'name': alt.experiment.name,
            },
            'conversion': {
                'value': None,
                'kpi': kpi
            },
            'client_id': client_id
        }

        return json_success(resp, request)

    @service_unavailable_on_connection_error
    def on_participate(self, request):
        alts = request.args.getlist('alternatives')
        experiment_name = request.args.get('experiment')
        force = request.args.get('force')
        record_force = to_bool(request.args.get('record_force', 'false'))
        client_id = request.args.get('client_id')
        traffic_fraction = request.args.get('traffic_fraction')

        if traffic_fraction is not None:
            traffic_fraction = float(traffic_fraction)
        prefetch = to_bool(request.args.get('prefetch', 'false'))

        if client_id is None or experiment_name is None or alts is None:
            return json_error({'message': 'missing arguments'}, request, 400)

        dt = None
        if request.args.get("datetime"):
            dt = dateutil.parser.parse(request.args.get("datetime"))
        try:
            if should_exclude_visitor(request):
                exp = Experiment.find(experiment_name, redis=self.redis)
                if exp.winner is not None:
                    alt = exp.winner
                else:
                    alt = exp.control
            else:
                alt = participate(experiment_name, alts, client_id,
                                  force=force, record_force=record_force,
                                  traffic_fraction=traffic_fraction,
                                  prefetch=prefetch, datetime=dt, redis=self.redis)
        except ValueError as e:
            return json_error({'message': str(e)}, request, 400)

        resp = {
            'alternative': {
                'name': alt.name
            },
            'experiment': {
                'name': alt.experiment.name,
            },
            'client_id': client_id,
            'status': 'ok'
        }

        return json_success(resp, request)

    @service_unavailable_on_connection_error
    def on_experiment_details(self, request, name):
        exp = Experiment.find(name, redis=self.redis)
        if exp is None:
            return json_error({'message': 'experiment not found'}, request, 404)

        return json_success(exp.objectify_by_period('day', True), request)

def should_exclude_visitor(request):
    user_agent = request.args.get('user_agent')
    ip_address = request.args.get('ip_address')

    return is_robot(user_agent) or is_ignored_ip(ip_address)

def is_robot(user_agent):
    if user_agent is None:
        return False
    regex = re.compile(r"{0}".format(cfg.get('robot_regex')), re.I)
    return regex.search(unquote(user_agent)) is not None

def is_ignored_ip(ip_address):
    # Ignore invalid/local IP addresses
    try:
        inet_aton(unquote(ip_address))
    except:
        return False  # TODO Same as above not sure of default

    return unquote(ip_address) in cfg.get('ignored_ip_addresses')

# Method to run with built-in server
def create_app():
    app = Sixpack(db.REDIS)
    return CORSMiddleware(app)

#  Method to run with gunicorn
def start(environ, start_response):
    return create_app()(environ, start_response)
<|endoftext|>


<commit_before>import requests # html requests
from bs4 import BeautifulSoup # to extract html content

'''
A class to scrape the organisasjoner.trondheim.kommune aktordatabasen
'''
class Scraper:
    def __init__(self):
        self.baseURL = 'https://organisasjoner.trondheim.kommune.no'

        #self.soup = BeautifulSoup(open('./tests/aktorer.html'), 'html.parser') # for development only
        #self.soup = BeautifulSoup(open('./tests/aktorer.html'), 'html.parser') # for development only
        self.soup = None

    '''
    A function to querey and check if the queried provider is registered.

    :param name: Thee exact registered provider name in the aktordatabasen
    :param flod_activity_type:
    :param area:
    '''
    def scrapeAktor(self, name,flod_activity_type="", area=""):
        #urlWithOutParamm ="https://organisasjoner.trondheim.kommune.no/organisations"
        #urlWithParam = "https://organisasjoner.trondheim.kommune.no/organisations?name=Rosenborg&brreg_activity_code=&flod_activity_type=&area="
        urlWithParam = "https://organisasjoner.trondheim.kommune.no/organisations?name="+name+"&brreg_activity_code=&flod_activity_type=&area="

        # need to fire a get request for urlWithParam
        r = requests.get(urlWithParam)
        self.soup = BeautifulSoup(r.content, 'html.parser')

        # ul class="nav nav-tabs nav-stacked"
        theList = self.soup.find_all('ul', class_="nav nav-tabs nav-stacked")
        theList = str(theList)
        theList.replace('[','')
        theList.replace(']','')
        # if no aktor matched, empty list,
        # if found, Python list with html ul list, with  we need
        # [

 ROSENBORG BALLKLUB

]
        if(len(theList) == 0):
            print('No match')
            return None

        self.soup = BeautifulSoup(theList, 'html.parser')
        theLinks = self.soup.find_all('a')
        # List with  tags that has the
        #
        #
        #   Navn som matches querey
        #
        #print(theLinks)

        for link in theLinks:
            # check for match againt aktordatabasen
            # if match, get new url and get that information
            link = str(link)
            start = link.find('') + 4
            end = link.find('')
            hrefStart = link.find('=') + 1
            hrefEnd = link.find('>')
            orgLink = link[hrefStart:hrefEnd]
            resultName = link[start:end]
            #print(orgLink) # organisations/573
            #no we can query on baseUrl + orgLink => https://organisasjoner.trondheim.kommune.no/rganisations/573
            #print(name.upper())
            #print(resultName.upper())
            if(name.upper() == resultName.upper()):

                orgLink = orgLink[1:-1]

                orgLink =  self.baseURL + '/'+ orgLink
                #print(orgLink)
                #print('orgLink from scrapeAktor')
                return self._scrapeInfo(orgLink=orgLink)

        # if not match, return null
        return None

    '''
    Private function to scrape the page of an registered provider in the
    aktordatabasen.

    :param orgLink: the aktordatabase url to scrape
    '''
    def _scrapeInfo(self, orgLink):
        information = {}
        r = requests.get(orgLink)
        self.soup = BeautifulSoup(r.content, 'html.parser')
        #print(self.soup.prettify())
        #self.soup = BeautifulSoup(open('./tests/ROSENBORG_BALLKLUB.html'), 'html.parser')
        rawInformation = self.soup.find_all('div', class_='summary-section')

        theList = str(rawInformation)
        theList.replace('[','')
        theList.replace(']','')

        #self.soup = BeautifulSoup(information, 'html.parser')

        for info in rawInformation:
            tables = str(info.dl)
            self.soup = BeautifulSoup(tables, 'html.parser')
            keys = self.soup.find_all('dt')
            values = self.soup.find_all('dd')
            addresses = self.soup.find_all('address')
            categories = self.soup.find_all('ul', class_='unstyled')

            for i in range(0, len(keys)):
                if(keys[i].string.strip() == 'Kategori(er)'):
                    value = ''
                    for cat in categories:
                        cat = categories.pop()
                        cat = str(cat)
                        #print(cat)
                        start = cat.find('
') + 4
                        end = cat.find('
')
                        cat = cat[start:end]
                        if(len(categories) == 0):
                            value += cat
                            break
                        else:
                            value += cat + ', '
                    information[keys[i].string] = value

                elif(keys[i].string != 'Adresse'):
                    information[keys[i].string] = values[i].string

                else:
                    adr = addresses.pop()
                    adr = str(adr)

                    start = adr.find('>') + 1
                    end = adr.find('
')
                    adr = adr[start:end]
                    adr = adr.replace('
', '')
                    adr = adr.replace('\n', '')
                    adr = adr.replace(' ', '')

                    information[keys[i].string] = adr.strip()

        #print(information)
        return information

def main():
    s = Scraper()
    #informasjon = s.scrapeAktor(name='rosenborg ballklub')
    #informasjon = s.scrapeAktor(name='LEANGEN ISHOCKEYKLUBB')
    #informasjon = s.scrapeAktor(name='Ikea Leangens Volleyball lag')
    informasjon = s.scrapeAktor(name='DANS MED OSS')
    print(informasjon)

#main()
<commit_msg>added the organisation id to the dictionary that is returned<commit_after>import requests # html requests
from bs4 import BeautifulSoup # to extract html content

'''
A class to scrape the organisasjoner.trondheim.kommune aktordatabasen
'''
class Scraper:
    def __init__(self):
        self.baseURL = 'https://organisasjoner.trondheim.kommune.no'

        #self.soup = BeautifulSoup(open('./tests/aktorer.html'), 'html.parser') # for development only
        #self.soup = BeautifulSoup(open('./tests/aktorer.html'), 'html.parser') # for development only
        self.soup = None

    '''
    A function to querey and check if the queried provider is registered.

    :param name: Thee exact registered provider name in the aktordatabasen
    :param flod_activity_type:
    :param area:
    '''
    def scrapeAktor(self, name,flod_activity_type="", area=""):
        #urlWithOutParamm ="https://organisasjoner.trondheim.kommune.no/organisations"
        #urlWithParam = "https://organisasjoner.trondheim.kommune.no/organisations?name=Rosenborg&brreg_activity_code=&flod_activity_type=&area="
        urlWithParam = "https://organisasjoner.trondheim.kommune.no/organisations?name="+name+"&brreg_activity_code=&flod_activity_type=&area="

        # need to fire a get request for urlWithParam
        r = requests.get(urlWithParam)
        self.soup = BeautifulSoup(r.content, 'html.parser')

        # ul class="nav nav-tabs nav-stacked"
        theList = self.soup.find_all('ul', class_="nav nav-tabs nav-stacked")
        theList = str(theList)
        theList.replace('[','')
        theList.replace(']','')
        # if no aktor matched, empty list,
        # if found, Python list with html ul list, with  we need
        # [

 ROSENBORG BALLKLUB

]
        if(len(theList) == 0):
            print('No match')
            return None

        self.soup = BeautifulSoup(theList, 'html.parser')
        theLinks = self.soup.find_all('a')
        # List with  tags that has the
        #
        #
        #   Navn som matches querey
        #
        #print(theLinks)

        for link in theLinks:
            # check for match againt aktordatabasen
            # if match, get new url and get that information
            link = str(link)
            start = link.find('') + 4
            end = link.find('')
            hrefStart = link.find('=') + 1
            hrefEnd = link.find('>')
            orgLink = link[hrefStart:hrefEnd]
            resultName = link[start:end]
            #print(orgLink)
            resultSplit = orgLink.split('/')
            #print(resultSplit)
            aktorID = resultSplit[1]
            aktorID = aktorID[:-1]
            #print(aktorID)
            #print(orgLink) # organisations/573
            #no we can query on baseUrl + orgLink => https://organisasjoner.trondheim.kommune.no/rganisations/573
            #print(name.upper())
            #print(resultName.upper())
            if(name.upper() == resultName.upper()):

                orgLink = orgLink[1:-1]

                orgLink =  self.baseURL + '/'+ orgLink
                #print(orgLink)
                #print('orgLink from scrapeAktor')
                return self._scrapeInfo(orgLink=orgLink, orgID=aktorID)

        # if not match, return null
        return None

    '''
    Private function to scrape the page of an registered provider in the
    aktordatabasen.

    :param orgLink: the aktordatabase url to scrape
    '''
    def _scrapeInfo(self, orgLink, orgID):
        information = {}
        r = requests.get(orgLink)
        self.soup = BeautifulSoup(r.content, 'html.parser')
        #print(self.soup.prettify())
        #self.soup = BeautifulSoup(open('./tests/ROSENBORG_BALLKLUB.html'), 'html.parser')
        rawInformation = self.soup.find_all('div', class_='summary-section')

        theList = str(rawInformation)
        theList.replace('[','')
        theList.replace(']','')

        #self.soup = BeautifulSoup(information, 'html.parser')

        for info in rawInformation:
            tables = str(info.dl)
            self.soup = BeautifulSoup(tables, 'html.parser')
            keys = self.soup.find_all('dt')
            values = self.soup.find_all('dd')
            addresses = self.soup.find_all('address')
            categories = self.soup.find_all('ul', class_='unstyled')

            for i in range(0, len(keys)):
                if(keys[i].string.strip() == 'Kategori(er)'):
                    value = ''
                    for cat in categories:
                        cat = categories.pop()
                        cat = str(cat)
                        #print(cat)
                        start = cat.find('
') + 4
                        end = cat.find('
')
                        cat = cat[start:end]
                        if(len(categories) == 0):
                            value += cat
                            break
                        else:
                            value += cat + ', '
                    information[keys[i].string] = value

                elif(keys[i].string != 'Adresse'):
                    information[keys[i].string] = values[i].string

                else:
                    adr = addresses.pop()
                    adr = str(adr)

                    start = adr.find('>') + 1
                    end = adr.find('
')
                    adr = adr[start:end]
                    adr = adr.replace('
', '')
                    adr = adr.replace('\n', '')
                    adr = adr.replace(' ', '')

                    information[keys[i].string] = adr.strip()
        if(len(information) !=0 ):
            information['Id'] = orgID

        #print(information)
        return information

def main():
    s = Scraper()
    #informasjon = s.scrapeAktor(name='rosenborg ballklub')
    #informasjon = s.scrapeAktor(name='LEANGEN ISHOCKEYKLUBB')
    #informasjon = s.scrapeAktor(name='Ikea Leangens Volleyball lag')
    informasjon = s.scrapeAktor(name='DANS MED OSS')
    print(informasjon)

main()
<|endoftext|>


<commit_before>import sys
import os
import shutil
import hashlib
import subprocess
import platform

def cython(pyx_files, working_path=''):
    """Use Cython to convert the given files to C.

    Parameters
    ----------
    pyx_files : list of str
        The input .pyx files.

    """
    # Do not build cython files if target is clean
    if len(sys.argv) >= 2 and sys.argv[1] == 'clean':
        return

    try:
        import Cython
    except ImportError:
        # If cython is not found, we do nothing -- the build will make use of
        # the distributed .c files
        print("Cython not found; falling back to pre-built %s" \
              % " ".join([f.replace('.pyx', '.c') for f in pyx_files]))
    else:
        for pyxfile in [os.path.join(working_path, f) for f in pyx_files]:

            # if the .pyx file stayed the same, we don't need to recompile
            if not _changed(pyxfile):
                continue

            c_file = pyxfile[:-4] + '.c'

            # run cython compiler
            cmd = 'cython -o %s %s' % (c_file, pyxfile)
            print(cmd)

            if platform.system() == 'Windows':
                status = subprocess.call(
                    [sys.executable,
                     os.path.join(os.path.dirname(sys.executable),
                                  'Scripts', 'cython.py'),
                     '-o', c_file, pyxfile],
                    shell=True)
            else:
                status = subprocess.call(['cython', '-o', c_file, pyxfile])

def _md5sum(f):
    m = hashlib.new('md5')
    while True:
        # Hash one 8096 byte block at a time
        d = f.read(8096)
        if not d:
            break
        m.update(d)
    return m.hexdigest()

def _changed(filename):
    """Compare the hash of a Cython file to the cached hash value on disk.

    """
    filename_cache = filename + '.md5'

    try:
        md5_cached = open(filename_cache, 'rb').read()
    except IOError:
        md5_cached = '0'

    with open(filename, 'rb') as f:
        md5_new = _md5sum(f)

        with open(filename_cache, 'wb') as cf:
            cf.write(md5_new.encode('utf-8'))

    return md5_cached != md5_new
<commit_msg>Fix build error on EPD for Windows<commit_after>import sys
import os
import shutil
import hashlib
import subprocess
import platform

def cython(pyx_files, working_path=''):
    """Use Cython to convert the given files to C.

    Parameters
    ----------
    pyx_files : list of str
        The input .pyx files.

    """
    # Do not build cython files if target is clean
    if len(sys.argv) >= 2 and sys.argv[1] == 'clean':
        return

    try:
        import Cython
    except ImportError:
        # If cython is not found, we do nothing -- the build will make use of
        # the distributed .c files
        print("Cython not found; falling back to pre-built %s" \
              % " ".join([f.replace('.pyx', '.c') for f in pyx_files]))
    else:
        for pyxfile in [os.path.join(working_path, f) for f in pyx_files]:

            # if the .pyx file stayed the same, we don't need to recompile
            if not _changed(pyxfile):
                continue

            c_file = pyxfile[:-4] + '.c'

            # run cython compiler
            cmd = 'cython -o %s %s' % (c_file, pyxfile)
            print(cmd)

            try:
                status = subprocess.call(['cython', '-o', c_file, pyxfile])
            except WindowsError:
                # On Windows cython.exe may be missing if Cython was installed
                # via distutils. Run the cython.py script instead.
                status = subprocess.call(
                    [sys.executable,
                     os.path.join(os.path.dirname(sys.executable),
                                  'Scripts', 'cython.py'),
                     '-o', c_file, pyxfile],
                    shell=True)

def _md5sum(f):
    m = hashlib.new('md5')
    while True:
        # Hash one 8096 byte block at a time
        d = f.read(8096)
        if not d:
            break
        m.update(d)
    return m.hexdigest()

def _changed(filename):
    """Compare the hash of a Cython file to the cached hash value on disk.

    """
    filename_cache = filename + '.md5'

    try:
        md5_cached = open(filename_cache, 'rb').read()
    except IOError:
        md5_cached = '0'

    with open(filename, 'rb') as f:
        md5_new = _md5sum(f)

        with open(filename_cache, 'wb') as cf:
            cf.write(md5_new.encode('utf-8'))

    return md5_cached != md5_new
<|endoftext|>