Disabled external gits
This commit is contained in:
4
cs452-fos/fos-assignments/5-inference/build.sbt
Normal file
4
cs452-fos/fos-assignments/5-inference/build.sbt
Normal file
@@ -0,0 +1,4 @@
|
||||
scalaVersion := "3.0.2"
|
||||
|
||||
libraryDependencies += "org.scala-lang.modules" %% "scala-parser-combinators" % "2.0.0"
|
||||
|
BIN
cs452-fos/fos-assignments/5-inference/project.zip
Normal file
BIN
cs452-fos/fos-assignments/5-inference/project.zip
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
sbt.version=1.5.5
|
@@ -0,0 +1,131 @@
|
||||
package fos
|
||||
|
||||
object Infer {
|
||||
case class TypeScheme(params: List[TypeVar], tp: Type)
|
||||
type Env = List[(String, TypeScheme)]
|
||||
type Constraint = (Type, Type)
|
||||
|
||||
case class TypeError(msg: String) extends Exception(msg)
|
||||
|
||||
extension (t: Type)
|
||||
def in(s: Type): Boolean = getTV(s).contains(t)
|
||||
|
||||
extension (e: Env)
|
||||
def has(t: TypeVar): Boolean = e.find(_._2.params.contains(t)).nonEmpty
|
||||
|
||||
private var ftv_count = -1
|
||||
def freshTV(): TypeVar = {
|
||||
ftv_count += 1
|
||||
TypeVar("v"+ftv_count.toString)
|
||||
}
|
||||
|
||||
def getTV(t: Type): List[TypeVar] = getTV(t, Nil)
|
||||
def getTV(t: Type, ex: List[TypeVar]): List[TypeVar] = t match {
|
||||
case tpe@TypeVar(_) if !(ex.contains(tpe))=> List(tpe)
|
||||
case FunType(tpe1, tpe2) => getTV(tpe1,ex) ::: getTV(tpe2,ex)
|
||||
case _ => Nil
|
||||
}
|
||||
|
||||
def generalizeTV(tp: Type, env: Env) : TypeScheme = TypeScheme(getTV(tp).filter(env.has), tp)
|
||||
|
||||
|
||||
|
||||
def collectHelper(env: Env, t: Term, tpe: Type): (Type, List[Constraint]) =
|
||||
collectHelper(env, t, tpe, tpe)
|
||||
|
||||
def collectHelper(env: Env, t: Term, tpe1: Type, tpe2: Type): (Type, List[Constraint]) =
|
||||
val (tp1, c1) = collect(env,t)
|
||||
(tpe1, (tp1, tpe2) :: c1)
|
||||
|
||||
def collectHelperAbs(env: Env, x: String, t: Term, tpe: Type): (Type, List[Constraint]) =
|
||||
val (tp1, c1) = collect((x, TypeScheme(Nil,tpe)) :: env, t)
|
||||
(FunType(tpe, tp1), c1)
|
||||
|
||||
def collectHelperIf(env: Env, t1: Term, t2: Term, t3: Term): (Type, List[Constraint]) =
|
||||
val (tp1, c1) = collect(env, t1)
|
||||
val (tp2, c2) = collect(env, t2)
|
||||
val (tp3, c3) = collect(env, t3)
|
||||
(tp2, (tp1, BoolType) :: (tp2, tp3) :: c1 ::: c2 ::: c3)
|
||||
|
||||
def collectHelperApp(env: Env, t1: Term, t2: Term, tpe: Type): (Type, List[Constraint]) =
|
||||
val (tp1, c1) = collect(env, t1)
|
||||
val (tp2, c2) = collect(env, t2)
|
||||
(tpe, (tp1, FunType(tp2, tpe)) :: c1 ::: c2)
|
||||
|
||||
def collectHelperLet(env: Env, x: String, t1: Term, t2: Term): (Type, List[Constraint]) =
|
||||
val (tp1, c1) = collect(env, t1)
|
||||
val subst = unify(c1)
|
||||
val tp1Unified = subst(tp1)
|
||||
val fenv = env
|
||||
.map((s,ts) => (s, ts.params, subst(ts.tp)))
|
||||
.map((s,tsp,sub) => (s, TypeScheme(tsp.filter(_.in(sub)), sub)))
|
||||
val ts = generalizeTV(tp1Unified, fenv)
|
||||
val (tp2, c2) = collect((x, ts) :: fenv, t2)
|
||||
(tp2, c1 ::: c2)
|
||||
|
||||
|
||||
|
||||
def collect(env: Env, t: Term): (Type, List[Constraint]) =
|
||||
// println("Term: "+t)
|
||||
// println("Env: "+env)
|
||||
t match {
|
||||
case True | False => (BoolType, Nil)
|
||||
case Zero => (NatType, Nil)
|
||||
case Pred(t1) => collectHelper(env, t1, NatType)
|
||||
case Succ(t1) => collectHelper(env, t1, NatType)
|
||||
case IsZero(t1) => collectHelper(env, t1, BoolType, NatType)
|
||||
case If(t1, t2, t3) => collectHelperIf(env, t1, t2, t3)
|
||||
case Var(x) if (env.exists(_._1 == x)) => ( env.find(_._1 == x).get._2.tp, Nil)
|
||||
case Abs(x, tp, t1) => tp match {
|
||||
case EmptyTypeTree() => collectHelperAbs(env, x, t1, freshTV())
|
||||
case _ => collectHelperAbs(env, x, t1, tp.tpe)
|
||||
}
|
||||
case App(t1, t2) => collectHelperApp(env, t1, t2, freshTV())
|
||||
case Let(x, tp, t1, t2) => tp match {
|
||||
case EmptyTypeTree() => collectHelperLet(env, x, t1, t2)
|
||||
case _ => collect(env, App(Abs(x, tp, t2), t1))
|
||||
}
|
||||
case _ => throw TypeError(f"Collect is stuck on term `$t` !")
|
||||
}
|
||||
|
||||
|
||||
def unify(c: List[Constraint]): Type => Type = {
|
||||
val substitutions = unifyRec(c, Map())
|
||||
// println("Subst: "+ substitutions)
|
||||
return (x) => subst(x,substitutions)
|
||||
}
|
||||
|
||||
def subst(tpe: Type, constraints: Map[Type, Type]): Type = tpe match {
|
||||
case FunType(tpe1, tpe2) => FunType(subst(tpe1, constraints), subst(tpe2, constraints))
|
||||
case a@TypeVar(_) if constraints.contains(a) => subst(constraints(a), constraints)
|
||||
case others => others
|
||||
}
|
||||
|
||||
def unifyRec(c: List[Constraint], substitutions: Map[Type, Type]): Map[Type, Type] = {
|
||||
if (c.isEmpty)
|
||||
return substitutions
|
||||
// println("c: "+ c)
|
||||
// println("Subst: "+ substitutions)
|
||||
c.head match {
|
||||
case (s, t) if s == t => unifyRec(c.tail, substitutions)
|
||||
case (s@TypeVar(_), t) if !(s in t) => unifyRec(applyMapping(c.tail, s -> t), substitutions + (s -> t))
|
||||
case (s, t@TypeVar(_)) if !(t in s) => unifyRec(applyMapping(c.tail, t -> s), substitutions + (t -> s))
|
||||
case (FunType(s1, s2), FunType(t1, t2)) => unifyRec((s1, t1) :: (s2, t2) :: c.tail, substitutions)
|
||||
case _ => throw new TypeError(f"Impossible to find a substitution that satisfies the constraint set `${c.head}`")
|
||||
}
|
||||
}
|
||||
|
||||
def applyMapping(constraints: List[Constraint], st: (TypeVar, Type)): List[Constraint] =
|
||||
val (s, t) = st
|
||||
def recMapping(tp: Type): Type = tp match {
|
||||
case tp if tp == s => t
|
||||
case FunType(a, b) => FunType(recMapping(a), recMapping(b))
|
||||
case tp => tp
|
||||
}
|
||||
constraints.map {
|
||||
case (`s`, `s`) => (t, t)
|
||||
case (`s`, y) => (t, y)
|
||||
case (x , `s`) => (x, t)
|
||||
case (x, y) => (recMapping(x), recMapping(y))
|
||||
}
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
package fos
|
||||
|
||||
import Parser._
|
||||
import scala.util.parsing.input._
|
||||
|
||||
object Launcher {
|
||||
def main(args: Array[String]) = {
|
||||
val stdin = new java.io.BufferedReader(new java.io.InputStreamReader(System.in))
|
||||
val tokens = new lexical.Scanner(stdin.readLine())
|
||||
phrase(term)(tokens) match {
|
||||
case Success(term, _) =>
|
||||
try {
|
||||
val (tpe, c) = Infer.collect(Nil, term)
|
||||
// println("TPE: "+tpe)
|
||||
// println("C: "+c)
|
||||
val sub = Infer.unify(c)
|
||||
println("typed: " + sub(tpe))
|
||||
} catch {
|
||||
case tperror: Exception => println("type error: " + tperror.getMessage)
|
||||
}
|
||||
case e =>
|
||||
println(e)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,74 @@
|
||||
package fos
|
||||
|
||||
import scala.util.parsing.combinator.syntactical.StandardTokenParsers
|
||||
import scala.util.parsing.input._
|
||||
|
||||
object Parser extends StandardTokenParsers {
|
||||
lexical.delimiters ++= List("(", ")", "\\", ".", ":", "=", "->", "{", "}", ",", "*", "+")
|
||||
lexical.reserved ++= List("Bool", "Nat", "true", "false", "if", "then", "else", "succ",
|
||||
"pred", "iszero", "let", "in")
|
||||
|
||||
/** <pre>
|
||||
* Term ::= SimpleTerm { SimpleTerm }</pre>
|
||||
*/
|
||||
def term: Parser[Term] = positioned(
|
||||
simpleTerm ~ rep(simpleTerm) ^^ { case t ~ ts => (t :: ts).reduceLeft[Term](App.apply) }
|
||||
| failure("illegal start of term"))
|
||||
|
||||
/** <pre>
|
||||
* SimpleTerm ::= "true"
|
||||
* | "false"
|
||||
* | number
|
||||
* | "succ" Term
|
||||
* | "pred" Term
|
||||
* | "iszero" Term
|
||||
* | "if" Term "then" Term "else" Term
|
||||
* | ident
|
||||
* | "\" ident [":" Type] "." Term
|
||||
* | "(" Term ")"
|
||||
* | "let" ident [":" Type] "=" Term "in" Term</pre>
|
||||
*/
|
||||
def simpleTerm: Parser[Term] = positioned(
|
||||
"true" ^^^ True
|
||||
| "false" ^^^ False
|
||||
| numericLit ^^ { case chars => lit2Num(chars.toInt) }
|
||||
| "succ" ~ term ^^ { case "succ" ~ t => Succ(t) }
|
||||
| "pred" ~ term ^^ { case "pred" ~ t => Pred(t) }
|
||||
| "iszero" ~ term ^^ { case "iszero" ~ t => IsZero(t) }
|
||||
| "if" ~ term ~ "then" ~ term ~ "else" ~ term ^^ {
|
||||
case "if" ~ t1 ~ "then" ~ t2 ~ "else" ~ t3 => If(t1, t2, t3)
|
||||
}
|
||||
| ident ^^ { case id => Var(id) }
|
||||
| "\\" ~ ident ~ opt(":" ~ typ) ~ "." ~ term ^^ {
|
||||
case "\\" ~ x ~ Some(":" ~ tp) ~ "." ~ t => Abs(x, tp, t)
|
||||
case "\\" ~ x ~ None ~ "." ~ t => Abs(x, EmptyTypeTree(), t)
|
||||
}
|
||||
| "(" ~> term <~ ")" ^^ { case t => t }
|
||||
| "let" ~ ident ~ opt(":" ~ typ) ~ "=" ~ term ~ "in" ~ term ^^ {
|
||||
case "let" ~ x ~ Some(":" ~ tp) ~ "=" ~ t1 ~ "in" ~ t2 => Let(x, tp, t1, t2)
|
||||
case "let" ~ x ~ None ~ "=" ~ t1 ~ "in" ~ t2 => Let(x, EmptyTypeTree(), t1, t2)
|
||||
}
|
||||
| failure("illegal start of simple term"))
|
||||
|
||||
/** <pre>
|
||||
* Type ::= SimpleType { "->" Type }</pre>
|
||||
*/
|
||||
def typ: Parser[TypeTree] = positioned(
|
||||
baseType ~ opt("->" ~ typ) ^^ {
|
||||
case t1 ~ Some("->" ~ t2) => FunTypeTree(t1, t2)
|
||||
case t1 ~ None => t1
|
||||
}
|
||||
| failure("illegal start of type"))
|
||||
|
||||
/** <pre>
|
||||
* BaseType ::= "Bool" | "Nat" | "(" Type ")"</pre>
|
||||
*/
|
||||
def baseType: Parser[TypeTree] = positioned(
|
||||
"Bool" ^^^ BoolTypeTree()
|
||||
| "Nat" ^^^ NatTypeTree()
|
||||
| "(" ~> typ <~ ")" ^^ { case t => t }
|
||||
)
|
||||
|
||||
private def lit2Num(n: Int): Term =
|
||||
if (n == 0) Zero else Succ(lit2Num(n - 1))
|
||||
}
|
@@ -0,0 +1,83 @@
|
||||
package fos
|
||||
|
||||
import scala.util.parsing.input.Positional
|
||||
|
||||
sealed abstract class Term extends Positional
|
||||
|
||||
case object True extends Term {
|
||||
override def toString() = "true"
|
||||
}
|
||||
|
||||
case object False extends Term {
|
||||
override def toString() = "false"
|
||||
}
|
||||
|
||||
case object Zero extends Term {
|
||||
override def toString() = "0"
|
||||
}
|
||||
|
||||
case class Succ(t: Term) extends Term {
|
||||
override def toString() = "succ " + t
|
||||
}
|
||||
|
||||
case class Pred(t: Term) extends Term {
|
||||
override def toString() = "pred " + t
|
||||
}
|
||||
|
||||
case class IsZero(t: Term) extends Term {
|
||||
override def toString() = "iszero " + t
|
||||
}
|
||||
|
||||
case class If(cond: Term, t1: Term, t2: Term) extends Term {
|
||||
override def toString() = "if " + cond + " then " + t1 + " else " + t2
|
||||
}
|
||||
|
||||
case class Var(name: String) extends Term {
|
||||
override def toString() = name
|
||||
}
|
||||
|
||||
case class Abs(v: String, tp: TypeTree, t: Term) extends Term {
|
||||
override def toString() = "(\\" + v + ":" + tp + "." + t + ")"
|
||||
}
|
||||
|
||||
case class App(t1: Term, t2: Term) extends Term {
|
||||
override def toString() = t1.toString + (t2 match {
|
||||
case App(_, _) => " (" + t2.toString + ")" // left-associative
|
||||
case _ => " " + t2.toString
|
||||
})
|
||||
}
|
||||
case class Let(x: String, tp: TypeTree, v: Term, t: Term) extends Term {
|
||||
override def toString() = "let " + x + ":" + tp + " = " + v + " in " + t
|
||||
}
|
||||
|
||||
// Note that TypeTree is distinct from Type.
|
||||
// The former is how types are parsed, the latter is how types are represented.
|
||||
// We need this distinction because:
|
||||
// 1) There are type vars, which can't be written by our users, but are needed by the inferencer.
|
||||
// 2) There are empty types, which can be written, but aren't directly supported by the inferencer.
|
||||
abstract class TypeTree extends Positional {
|
||||
def tpe: Type
|
||||
}
|
||||
|
||||
case class BoolTypeTree() extends TypeTree {
|
||||
override def tpe = BoolType
|
||||
override def toString() = "Bool"
|
||||
}
|
||||
|
||||
case class NatTypeTree() extends TypeTree {
|
||||
override def tpe = NatType
|
||||
override def toString() = "Nat"
|
||||
}
|
||||
|
||||
case class FunTypeTree(t1: TypeTree, t2: TypeTree) extends TypeTree {
|
||||
override def tpe = FunType(t1.tpe, t2.tpe)
|
||||
override def toString() = (t1 match {
|
||||
case FunTypeTree(_, _) => "(" + t1 + ")" // right-associative
|
||||
case _ => t1.toString
|
||||
}) + "->" + t2
|
||||
}
|
||||
|
||||
case class EmptyTypeTree() extends TypeTree {
|
||||
override def tpe = throw new UnsupportedOperationException
|
||||
override def toString() = "_"
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package fos
|
||||
|
||||
// Note that TypeTree is distinct from Type.
|
||||
// See a comment on TypeTree to learn more.
|
||||
abstract class Type
|
||||
|
||||
case class TypeVar(name: String) extends Type {
|
||||
override def toString() = name
|
||||
}
|
||||
|
||||
case class FunType(t1: Type, t2: Type) extends Type {
|
||||
override def toString() = "(" + t1 + " -> " + t2 + ")"
|
||||
}
|
||||
|
||||
case object NatType extends Type {
|
||||
override def toString() = "Nat"
|
||||
}
|
||||
|
||||
case object BoolType extends Type {
|
||||
override def toString() = "Bool"
|
||||
}
|
||||
|
280
cs452-fos/fos-assignments/5-inference/submit.py
Normal file
280
cs452-fos/fos-assignments/5-inference/submit.py
Normal file
@@ -0,0 +1,280 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# This script was contributed by Timothée Loyck Andres.
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os.path
|
||||
import pathlib
|
||||
import shutil
|
||||
import ssl
|
||||
import tempfile
|
||||
from email import encoders
|
||||
from email.mime.base import MIMEBase
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from getpass import getpass
|
||||
from smtplib import SMTP_SSL, SMTPAuthenticationError, SMTPDataError
|
||||
from typing import List, Optional, Any, Union
|
||||
|
||||
# ========================= CONFIG VARIABLES =========================
|
||||
SMTP_SERVER = 'mail.epfl.ch'
|
||||
BOT_EMAIL_ADDRESS = 'lamp.fos.bot@gmail.com'
|
||||
|
||||
# The minimum number of people in a group
|
||||
MIN_SCIPERS = 1
|
||||
# The maximum number of people in a group
|
||||
MAX_SCIPERS = 3
|
||||
|
||||
# The number of the project (set to None to prompt user)
|
||||
PROJECT_NUMBER = None
|
||||
|
||||
# The name of the file in which the submission configuration is stored
|
||||
CONFIG_FILE_NAME = '.submission_info.json'
|
||||
# The name of the folder to be zipped
|
||||
SRC_FOLDER = 'src'
|
||||
# ====================================================================
|
||||
|
||||
project_path = pathlib.Path(__file__).parent.resolve()
|
||||
config_path = f'{project_path}/{CONFIG_FILE_NAME}'
|
||||
src_path = f'{project_path}/{SRC_FOLDER}'
|
||||
tmp_folder = tempfile.gettempdir()
|
||||
|
||||
|
||||
class ConfigData:
|
||||
def __init__(self, data_values: Optional[dict] = None, **kwargs):
|
||||
"""
|
||||
Creates a new data object with the given values. A dictionary may be passed, or keyword arguments for each data
|
||||
piece.
|
||||
|
||||
Contains: email address, username, SCIPER numbers of the group's members, project number
|
||||
|
||||
:argument data_values: an optional dictionary containing the required data
|
||||
:keyword email: the email of the user
|
||||
:keyword username: the GASPAR id of the user
|
||||
:keyword scipers: the list of SCIPER numbers of the group's members
|
||||
:keyword project_num: the project's number
|
||||
"""
|
||||
if data_values is not None:
|
||||
data = data_values
|
||||
elif len(kwargs) > 0:
|
||||
data = kwargs
|
||||
else:
|
||||
data = dict()
|
||||
|
||||
self.email: str = data.get('email')
|
||||
self.username: str = data.get('username')
|
||||
self.scipers: List[str] = data.get('scipers')
|
||||
self.project_num: int = data.get('project_num')
|
||||
|
||||
def get_config_data(self) -> dict:
|
||||
return {
|
||||
'email': self.email,
|
||||
'username': self.username,
|
||||
'scipers': self.scipers,
|
||||
'project_num': self.project_num
|
||||
}
|
||||
|
||||
|
||||
def get_scipers() -> List:
|
||||
"""
|
||||
Retrieves the group's SCIPER numbers.
|
||||
|
||||
:return: a list containing the SCIPER numbers of all the members of the FoS group
|
||||
"""
|
||||
|
||||
def is_sciper(string: str) -> bool:
|
||||
try:
|
||||
return len(string) == 6 and int(string) > 0
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
num_scipers = None
|
||||
scipers: List[str] = []
|
||||
|
||||
while num_scipers is None:
|
||||
num_scipers = get_sanitized_input(
|
||||
"Number of people in the group: ",
|
||||
int,
|
||||
predicate=lambda n: MIN_SCIPERS <= n <= MAX_SCIPERS,
|
||||
predicate_error_msg=f"The number of people must be between {MIN_SCIPERS} and {MAX_SCIPERS} included."
|
||||
)
|
||||
|
||||
for i in range(num_scipers):
|
||||
sciper = None
|
||||
while sciper is None:
|
||||
sciper = get_sanitized_input(
|
||||
f"SCIPER {i + 1}: ",
|
||||
predicate=is_sciper,
|
||||
predicate_error_msg="Invalid SCIPER number. Please try again."
|
||||
)
|
||||
scipers.append(sciper)
|
||||
|
||||
return scipers
|
||||
|
||||
|
||||
def get_sanitized_input(prompt: str, value_type: type = str, **kwargs) -> Optional[Any]:
|
||||
"""
|
||||
Sanitizes the user's input.
|
||||
|
||||
:param prompt: the message to be displayed for the user
|
||||
:param value_type: the type of value that we expect, for example str or int
|
||||
:keyword allow_empty: allow the input to be empty. The returned string may be the empty string
|
||||
:keyword predicate: a function that, when applied to the sanitized input, checks if it is valid
|
||||
:keyword predicate_error_msg: a message to be displayed if the predicate returns false on the input
|
||||
:return: the input as the passed type, or None if the input contained only whitespaces or if the type cast failed
|
||||
"""
|
||||
str_value = input(prompt).strip()
|
||||
if len(str_value) > 0 or kwargs.get('allow_empty'):
|
||||
try:
|
||||
value = value_type(str_value)
|
||||
p = kwargs.get('predicate')
|
||||
if p is not None and not p(value):
|
||||
if kwargs.get('predicate_error_msg') is None:
|
||||
print("Invalid value. Please try again.")
|
||||
elif len(kwargs.get('predicate_error_msg')) > 0:
|
||||
print(kwargs.get('predicate_error_msg'))
|
||||
return None
|
||||
return value
|
||||
except TypeError:
|
||||
raise TypeError(f"Incorrect value type: {value_type}")
|
||||
except ValueError:
|
||||
print(f"The value could not be interpreted as type {value_type.__name__}. Please try again.")
|
||||
return None
|
||||
|
||||
|
||||
def get_config(from_file: bool = True) -> ConfigData:
|
||||
"""
|
||||
Retrieves the configuration for sending the email. It may be fetched from a configuration file, or if it does not
|
||||
exist or is incomplete, it will ask the user for the data, then write it to the config file.
|
||||
|
||||
:param from_file: whether to retrieve the configuration from the config file if it exists. Default is True
|
||||
:return: the configuration to use for the email
|
||||
"""
|
||||
data = ConfigData()
|
||||
|
||||
# Set project number if it is already specified
|
||||
data.project_num = PROJECT_NUMBER
|
||||
|
||||
if from_file and not os.path.isfile(config_path):
|
||||
print('Please provide data that will be used to submit your project.')
|
||||
print(f'This information (sans the password) will be saved in: ./{CONFIG_FILE_NAME}')
|
||||
|
||||
if from_file and os.path.isfile(config_path):
|
||||
with open(config_path, 'r') as config_file:
|
||||
config = json.load(config_file)
|
||||
if type(config) is dict:
|
||||
data = ConfigData(config)
|
||||
|
||||
if data.scipers is None:
|
||||
data.scipers = get_scipers()
|
||||
while data.email is None:
|
||||
data.email = get_sanitized_input("Email address: ", predicate=lambda address: '@' in address)
|
||||
while data.username is None:
|
||||
data.username = get_sanitized_input("Gaspar ID: ")
|
||||
while data.project_num is None:
|
||||
data.project_num = get_sanitized_input("Project number: ", int, predicate=lambda n: n > 0)
|
||||
|
||||
set_config(data)
|
||||
return data
|
||||
|
||||
|
||||
def set_config(data: ConfigData) -> None:
|
||||
"""
|
||||
Saves the configuration in the config file.
|
||||
|
||||
:param data: the data to be saved
|
||||
"""
|
||||
with open(config_path, 'w') as config_file:
|
||||
json.dump(data.get_config_data(), config_file)
|
||||
|
||||
|
||||
def create_email(frm: str, to: str, subject: str, content: Optional[str] = None,
|
||||
attachments: Optional[Union[str, List[str]]] = None) -> MIMEMultipart:
|
||||
"""
|
||||
Creates an email.
|
||||
|
||||
:param frm: the address from which the email is sent
|
||||
:param to: the address to which send the email
|
||||
:param subject: the subject of the email
|
||||
:param content: the content of the email. Can be empty
|
||||
:param attachments: the attachments of the email. Can be a path or a list of paths
|
||||
"""
|
||||
message = MIMEMultipart()
|
||||
message['From'] = frm
|
||||
message['To'] = to
|
||||
message['Subject'] = subject
|
||||
|
||||
if content is not None:
|
||||
# Add content into body of message
|
||||
message.attach(MIMEText(content, 'plain'))
|
||||
|
||||
if attachments is not None:
|
||||
if type(attachments) is str:
|
||||
attachments = [attachments]
|
||||
|
||||
for attachment_path in attachments:
|
||||
part = MIMEBase("application", "octet-stream")
|
||||
|
||||
with open(attachment_path, 'rb') as attachment:
|
||||
part.set_payload(attachment.read())
|
||||
|
||||
encoders.encode_base64(part)
|
||||
part.add_header("Content-Disposition", f"attachment; filename={os.path.basename(attachment_path)}")
|
||||
message.attach(part)
|
||||
|
||||
return message
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
arg_parser = argparse.ArgumentParser(description="Submits the project to the bot for grading.", allow_abbrev=False)
|
||||
arg_parser.add_argument('-r', '--reset', action='store_true', help="ask for the submission data even if previously "
|
||||
"specified")
|
||||
arg_parser.add_argument('-s', '--self', action='store_true', help="send the mail to yourself instead of the bot")
|
||||
|
||||
if not os.path.isdir(src_path):
|
||||
arg_parser.exit(1, f"No {SRC_FOLDER} folder found. Aborting.\n")
|
||||
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
config: ConfigData = get_config(from_file=not args.reset)
|
||||
password: str = getpass("Gaspar password: ")
|
||||
|
||||
recipient = config.email if args.self else BOT_EMAIL_ADDRESS
|
||||
|
||||
mail = create_email(
|
||||
config.email,
|
||||
recipient,
|
||||
f"Project {config.project_num} ({', '.join(config.scipers)})",
|
||||
attachments=shutil.make_archive(f'{tmp_folder}/{SRC_FOLDER}', 'zip', root_dir=project_path,
|
||||
base_dir=f'{SRC_FOLDER}')
|
||||
)
|
||||
|
||||
with SMTP_SSL(SMTP_SERVER, context=ssl.create_default_context()) as server:
|
||||
try:
|
||||
server.login(config.username, password)
|
||||
server.sendmail(config.email, recipient, mail.as_string())
|
||||
print(f"Submission sent to {recipient}.")
|
||||
except SMTPAuthenticationError as e:
|
||||
if e.smtp_code == 535:
|
||||
print(f"Wrong GASPAR ID ({config.username}) or password. Your ID will be asked for again on the next"
|
||||
" run.")
|
||||
|
||||
# Remove (potentially) incorrect ID from config
|
||||
config.username = None
|
||||
set_config(config)
|
||||
|
||||
exit(2)
|
||||
else:
|
||||
raise
|
||||
except SMTPDataError as e:
|
||||
if e.smtp_code == 550:
|
||||
print("You email address seems to be incorrect. It will be asked for again on the next run.")
|
||||
|
||||
# Remove incorrect address from config
|
||||
config.email = None
|
||||
set_config(config)
|
||||
|
||||
exit(2)
|
||||
else:
|
||||
raise
|
Reference in New Issue
Block a user