Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backoff #6

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions backoff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import os.path
from datetime import datetime, timedelta

SIN_PENALIDAD = 2
ULTIMAS_ENTREGAS = 10
# Por si acaso que algun error nuestro cause errores, permitimos desactivar el backoff,
# aunque sea momentaneamente
BACKOFF_ACTIVADO = True

def get_id_cursada():
"""Devuelve el identificador de la cursada según año y cuatrimestre.

El identificador es del tipo ‘2015_2’ o ‘2016_1’, donde el segundo elemento
indica el cuatrimestre.

El cuatrimestre es:

- 1 si la fecha es antes del 1 de agosto;
- 2 si es igual o posterior.
"""
today = datetime.today()
cutoff = today.replace(month=8, day=1)
return "{}_{}".format(today.year, 1 if today < cutoff else 2)


def get_id_estudiante(planilla, padron_o_grupo):
"""
Los grupos se registran como Gxx en la planilla, pero en el repositorio se
guardan como padrón1_padrón2 (con padrón1 < padrón2).
Esta función devuelve el indentificador correspondiente de acuerdo al grupo
o padrón.
"""
# Si es un grupo formado por dos personas.
if padron_o_grupo in planilla.grupos and len(planilla.grupos[padron_o_grupo]) == 2:
padrones = planilla.grupos[padron_o_grupo]
return "{}_{}".format(min(padrones), max(padrones))

# Si es un grupo formado por una persona.
if padron_o_grupo in planilla.grupos and len(planilla.grupos[padron_o_grupo]) == 1:
return planilla.grupos[padron_o_grupo][0]

# En otro caso es un padrón.
return padron_o_grupo


def obtener_entregas(repo, tp, id_cursada, id_grupo):
"""
Devuelve una lista con los
"""
rel_dir = os.path.join(tp, id_cursada, id_grupo)

# Si el path no existe, entonces no hubo entregas hasta ahora.
if not os.path.exists(os.path.join(repo.working_dir, rel_dir)):
return []

log = repo.git.log("--format=%at", "--date=iso", rel_dir)
return [datetime.fromtimestamp(int(date)) for date in log.split("\n")]

def backoff(x):
"""
Calcula el backoff (en horas) correspondiente a una cantidad de entregas
La funcion tiene la forma:
0 si x <= SIN_PENALIDAD
backoff(x) = x - SIN_PENALIDAD si SIN_PEALIDAD < x < ULTIMAS_ENTREGAS
ULTIMAS_ENTREGAS - SIN_PENALIDAD en otro caso

Siendo SIN_PENALIDAD = 2 y ULTIMAS_ENTREGAS = 10,
Es la función lineal que empieza en x = 2, y termina en x = 10 con valor 8.
"""
return 0 if x <= SIN_PENALIDAD else x - SIN_PENALIDAD

def tiempo_siguiente_entrega(entregas):
"""
Calcula el momento a partir del cual el alumno puede volver a hacer la entrega,
teniendo en cuenta el backoff que tiene que cumplir el alumno respecto de la
ultima entrega que hizo.
"""
# Le sumo el delta a la ultima entrega
return entregas[-1] + timedelta(hours = backoff(len(entregas)))

def _validar_backoff_entregas(entregas, tiempo_actual):
"""
Logica de la validacion. Recibe el tiempo actual para poder hacerlo
testeable
"""
# Si aun no hizo entregas, entonces cumple
if len(entregas) == 0:
return

# Me quedo con las ultimas entregas, para poner un máximo de tiempo
entregas = entregas[-ULTIMAS_ENTREGAS:]
siguiente = tiempo_siguiente_entrega(entregas)

if siguiente > datetime.today():
raise BackoffException("No puede hacer esta entrega hasta: " + str(siguiente))

def validar_backoff(repo, planilla, tp, padron_o_grupo):
"""
Levanta una excepción de tipo BackoffException cuando un alumno o grupo
hace demasiados intentos en muy poco tiempo para realizar una entrega.
"""
if not BACKOFF_ACTIVADO:
return

tp = tp.lower()
id_cursada = get_id_cursada()
id_grupo = get_id_estudiante(planilla, padron_o_grupo)

entregas = obtener_entregas(repo, tp, id_cursada, id_grupo)
_validar_backoff_entregas(entregas, datetime.today())


class BackoffException(Exception):
"""
Excepcion que se lanza cuando no se cumplen los tiempos de espera del corrector,
luego de sucesivos intentos
"""
pass
44 changes: 44 additions & 0 deletions backoff_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from backoff import _validar_backoff_entregas as validar
from backoff import BackoffException
from datetime import datetime, timedelta
import unittest

HOY = datetime.today()

class TestValidacionBackoff(unittest.TestCase):

def test_sin_entregas(self):
validar([], HOY)

def test_entrega_pocas_veces(self):
# Entrego hace 1 segundo
validar([HOY - timedelta(seconds = 1)], HOY)

def test_entrega_hace_mucho(self):
# Entrego hace 5 dias
validar([HOY - timedelta(5)], HOY)

def test_entrega_pasa_backoff(self):
# Entrego hace 6, 5, 4 y 3 horas
validar([HOY - timedelta(hours = 6), HOY - timedelta(hours = 5), HOY - timedelta(hours = 4), HOY - timedelta(hours = 3)], HOY)

def test_entrega_no_pasa_backoff(self):
with self.assertRaises(BackoffException):
validar([HOY, HOY, HOY, HOY], HOY)

def test_muchas_entregas_hace_tiempo(self):
tiempos = [HOY - timedelta(10, seconds = 10 - i) for i in range(10)]
validar(tiempos, HOY)

def test_muchas_entregas_hace_poco_tiempo(self):
tiempos = [HOY - timedelta(hours = 10 - i) for i in range(8)]
with self.assertRaises(BackoffException):
validar(tiempos, HOY)

def test_algunas_entregas_poco_tiempo(self):
tiempos = [HOY - timedelta(minutes = 7, hours = 3), HOY - timedelta(minutes = 5, hours = 3), HOY - timedelta(minutes = 3)]
with self.assertRaises(BackoffException):
validar(tiempos, HOY)

if __name__ == "__main__":
unittest.main()
2 changes: 2 additions & 0 deletions config-sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
EMAIL_TO = 'tps.7540rw@gmail.com'
APP_TITLE = 'Algoritmos y Programación 1 - Entrega de TPs'

REPO_ENTREGAS = ""

SERVICE_ACCOUNT_CREDENTIALS = {
"type": "service_account",
"project_id": "...",
Expand Down
20 changes: 14 additions & 6 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
import mimetypes
import smtplib
import traceback
from backoff import validar_backoff
from collections import namedtuple
from email import encoders
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from git import Repo
from urllib.parse import urlencode

import httplib2
Expand All @@ -20,12 +22,14 @@
from flask import request
from werkzeug.utils import secure_filename

from config import SENDER_NAME, EMAIL_TO, APP_TITLE, RECAPTCHA_SECRET, RECAPTCHA_SITE_ID, TEST, CLIENT_ID, \
CLIENT_SECRET, OAUTH_REFRESH_TOKEN, GRUPAL, INDIVIDUAL
from config import SENDER_NAME, EMAIL_TO, APP_TITLE, RECAPTCHA_SECRET,\
RECAPTCHA_SITE_ID, TEST, CLIENT_ID, CLIENT_SECRET, OAUTH_REFRESH_TOKEN,\
REPO_ENTREGAS, GRUPAL, INDIVIDUAL
from planilla import fetch_planilla

app = Flask(__name__)
File = namedtuple('File', ['content', 'filename'])
repo_entregas = Repo(REPO_ENTREGAS)


EXTENSIONES_ACEPTADAS = {'zip', 'tar', 'gz', 'pdf'}
Expand Down Expand Up @@ -163,18 +167,22 @@ def get_emails_alumno(planilla, padron_o_grupo):
@app.route('/', methods=['POST'])
def post():
try:
validate_captcha()
validar_captcha()
planilla = fetch_planilla()

tp = request.form['tp']
padron_o_grupo = request.form['identificador']

# Valida que no se use el corrector como un oráculo.
validar_backoff(repo_entregas, planilla, tp, padron_o_grupo)

if tp not in planilla.entregas:
raise Exception('La entrega {} es inválida'.format(tp))

files = get_files()
if not files:
raise Exception('No se ha adjuntado ningún archivo con extensión válida.')

padron_o_grupo = request.form['identificador']

# Valida si la entrega es individual o grupal de acuerdo a lo ingresado.
validate_grupo(planilla, padron_o_grupo, tp)

Expand All @@ -197,7 +205,7 @@ def post():
raise e


def validate_captcha():
def validar_captcha():
response = urlfetch.fetch(
url='https://www.google.com/recaptcha/api/siteverify',
params=urlencode({
Expand Down
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@ certifi==2017.7.27.1
chardet==3.0.4
click==6.7
Flask==0.12.2
gitdb2==2.0.3
GitPython==2.1.7
gspread==0.6.2
httplib2==0.10.3
idna==2.6
itsdangerous==0.24
Jinja2==2.9.6
MarkupSafe==1.0
oauth2client==4.1.2
pkg-resources==0.0.0
pyasn1==0.3.2
pyasn1-modules==0.0.11
requests==2.18.4
rsa==3.4.2
six==1.10.0
smmap2==2.0.3
urlfetch==1.0.2
urllib3==1.22
Werkzeug==0.12.2