diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..73f69e0
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
+# Editor-based HTTP Client requests
+/httpRequests/
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..6649a8c
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..ffb90a4
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/penngrader.iml b/.idea/penngrader.iml
new file mode 100644
index 0000000..0baffc0
--- /dev/null
+++ b/.idea/penngrader.iml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Backend/grader_lambda.py b/Backend/grader_lambda.py
index 45776b5..95a6a71 100644
--- a/Backend/grader_lambda.py
+++ b/Backend/grader_lambda.py
@@ -1,3 +1,20 @@
+"""
+ The grader_lambda is the AWS lambda class that will handle test-related events. Its primary responsibility
+ is to take in an event, parse out key details, such as which homework it's relevant to, which student,
+ which test case, etc., then handle the actual testing.
+
+ The basic flow is:
+
+ event happens => lambda_handler is invoked => dynamo DB is updated w/ submission & score
+ > Examine event
+ > Get submission details
+ > Import any libraries
+ > Test submission
+ > Log the resulting score
+"""
+
+
+
import sys
sys.path.append('/opt')
import os
@@ -21,6 +38,14 @@
def lambda_handler(event, context):
+ """ This lambda handler is meant to take in an event and context, then:
+
+ 1. Parse out the test details: homework_id, student_id, test_case_id, answer
+ 2. Get all of the associated libraries and the test function: test_case, libraries
+ 3. Import those required libraries
+ 4. Score the students work against the test_case: student_score, max_score
+ 5. Log the submission in dynamo
+ """
try:
homework_id, student_id, test_case_id, answer = parse_event(event)
test_case, libraries = get_test_and_libraries(homework_id, test_case_id)
@@ -33,6 +58,9 @@ def lambda_handler(event, context):
def parse_event(event):
+ """ Simple helper method to parse an event as a dict.
+ Returns: (homework_id, student_id, test_case_id, answer)
+ """
try:
body = ast.literal_eval(event['body'])
return body['homework_id'], \
@@ -44,6 +72,10 @@ def parse_event(event):
def get_test_and_libraries(homework_id, test_case_id):
+ """ Retrieve the test function and associated libraries required. This pulls down from dynamo,
+ then passes back a tuple of (test_case, libraries). It can then be used to import the libraries
+ and test the student submission
+ """
try:
response = dynamo.get_item(TableName = TEST_CASES_TABLE, Key={'homework_id': {'S': homework_id}})
return deserialize(response['Item']['test_cases']['S'])[test_case_id], \
@@ -53,6 +85,10 @@ def get_test_and_libraries(homework_id, test_case_id):
def import_libraries(libraries): # TO-FINISH #
+ """ Apparently unfinished, but imports required libraries for a given test. Tests consist of
+ the function that actually performs the testing and the required libraries to run that function +
+ the function being tested. This takes care of importing those libraries
+ """
try:
packages = libraries['packages']
imports = libraries['imports']
@@ -78,6 +114,10 @@ def import_libraries(libraries): # TO-FINISH #
def grade(test_case, answer):
+ """ Generate a grade for a given test case. test_case is a function that is passed in. The function
+ gets executed here using answer as its param. This will catch any errors in the code and returns
+ a tuple of (score, max_score)
+ """
try:
return test_case(answer)
except Exception as exception:
@@ -87,6 +127,16 @@ def grade(test_case, answer):
def store_submission(student_score, max_score, homework_id, test_case_id, student_id):
+ """ Log the student submission in the database.
+ params:
+ - student_score: the number of points the student received on a problem
+ - max_score: the maximum score on that problem
+ - homeword_id: the key for this particular homework
+ - test_case_id: the key for this particular test case (problem)
+ - student_id: the key for the student
+
+ This will be passed to the dynamo DB as a new record
+ """
try:
db_entry = {
'TableName': GRADEBOOK_TABLE,
@@ -116,25 +166,37 @@ def store_submission(student_score, max_score, homework_id, test_case_id, studen
def serialize(obj):
+ """ Simple helper method to encode a serialized object
+ """
byte_serialized = dill.dumps(obj, recurse = True)
return base64.b64encode(byte_serialized).decode("utf-8")
def deserialize(obj):
+ """ Simple helper method to decode a base64 encoded object
+ """
byte_decoded = base64.b64decode(obj)
return dill.loads(byte_decoded)
-def build_response_message(student_score, max_score):
+def build_response_message(student_score, max_score, msg=None):
+ """ Build the string response message
+ """
+ out = ""
if student_score == max_score:
- return 'Correct! You earned {}/{} points. You are a star!\n\n'.format(student_score, max_score) + \
+ out = 'Correct! You earned {}/{} points. You are a star!\n\n'.format(student_score, max_score) + \
'Your submission has been successfully recorded in the gradebook.'
else:
- return 'You earned {}/{} points.\n\n'.format(student_score, max_score) + \
+ out = 'You earned {}/{} points.\n\n'.format(student_score, max_score) + \
'But, don\'t worry you can re-submit and we will keep only your latest score.'
+ if msg is not None:
+ out += "\n\n{}".format(msg)
+ return out
def build_http_response(status_code, message):
+ """ Build formatted http response as string
+ """
return {
'statusCode': status_code,
'body': str(message),
diff --git a/Backend/grades_lambda.py b/Backend/grades_lambda.py
index 24b1725..a42f34f 100644
--- a/Backend/grades_lambda.py
+++ b/Backend/grades_lambda.py
@@ -1,3 +1,17 @@
+"""
+ The grades_lambda is the AWS lambda class that will handle grade-related events. Its primary responsibility
+ is to take in an event, parse out key details, such as which homework it's relevant to and whether an
+ individual student or all students, then return grading details for that homework.
+
+ The basic flow is:
+
+ event happens => lambda_handler is invoked => dynamo DB is updated w/ submission & score
+ > Examine event
+ > Determine scope (single vs.
+ all students)
+ > Return http response
+"""
+
import sys
sys.path.append('/opt')
import os
@@ -31,6 +45,16 @@
def lambda_handler(event, context):
+ """ This lambda handler is meant to take in an event and context, then:
+
+ 1. Parse out the event details, then get the ID of the homework: body, homework_id
+ 2. Get all associated metadata for the homework: deadline, max_daily_submissions, max_score
+ 3. Check the request type:
+ > ALL_STUDENTS_REQUEST: validate the secret, then retrieve the grades and return http response of
+ (all_grades, deadline)
+ > STUDENT_REQUEST: retrieve a single student's grade for an assignment, return http response of
+ (grades, deadline, max_daily_submissions, max_score)
+ """
try:
body = parse_event(event)
homework_id = body['homework_id']
@@ -51,6 +75,9 @@ def lambda_handler(event, context):
def parse_event(event):
+ """ Simple helper method to parse an event as a dict.
+ Returns: entire event
+ """
try:
return ast.literal_eval(event['body'])
except:
@@ -58,6 +85,8 @@ def parse_event(event):
def validate_secret_key(secret_key):
+ """ Simple method to confirm that the secret_key passed in is valid
+ """
try:
response = dynamo.get_item(TableName = 'Classes', Key={'secret_key': {'S': secret_key}})
return response['Item']['course_id']['S']
@@ -66,6 +95,8 @@ def validate_secret_key(secret_key):
def get_homework_metadata(homework_id):
+ """ Retrieves the metadata for a homework, including deadline, max_daily_subimssions, and the total_score
+ """
try:
response = dynamo.get_item(TableName = METADATA_TABLE, Key={'homework_id': {'S': homework_id}})
return response['Item']['deadline']['S'], \
@@ -76,6 +107,11 @@ def get_homework_metadata(homework_id):
def get_grades(homework_id, student_id = None):
+ """ Retrieve the grades for a given homework using the student's ID. This will hit the
+ dynamo DB and pull down the student's current score on a particular assignment.
+
+ Note: if student_id is None, grades for all students are returned
+ """
table = dynamo_resource.Table(GRADEBOOK_TABLE)
if student_id is not None:
filtering_exp = Key('homework_id').eq(homework_id) & Attr('student_submission_id').begins_with(student_id)
@@ -87,11 +123,21 @@ def get_grades(homework_id, student_id = None):
def serialize(obj):
+ """ Simple helper method to encode a serialized object
+
+ Code is duplicated here. Possibly because the other file may not always be
+ accessible?
+ """
byte_serialized = dill.dumps(obj, recurse = True)
return base64.b64encode(byte_serialized).decode("utf-8")
def build_http_response(status_code, message):
+ """ Build formatted http response as string
+
+ Code is duplicated here. Possibly because the other file may not always be
+ accessible?
+ """
return {
'statusCode': status_code,
'body': str(message),
diff --git a/Backend/homework_config_lambda.py b/Backend/homework_config_lambda.py
index 69061a6..4fe4b98 100644
--- a/Backend/homework_config_lambda.py
+++ b/Backend/homework_config_lambda.py
@@ -1,3 +1,17 @@
+"""
+ The grades_lambda is the AWS lambda class that will handle grade-related events. Its primary responsibility
+ is to take in an event, parse out key details, such as which homework it's relevant to and whether an
+ individual student or all students, then return grading details for that homework.
+
+ The basic flow is:
+
+ event happens => lambda_handler is invoked => dynamo DB is updated w/ submission & score
+ > Examine event
+ > Determine intent
+ > Attempt to act on it
+ > Provide status response
+"""
+
import sys
sys.path.append('/opt')
import os
@@ -23,9 +37,19 @@
HOMEWORK_ID_REQUEST = 'GET_HOMEWORK_ID'
UPDATE_METADATA_REQUEST = 'UPDATE_METADATA'
UPDATE_TESTS_REQUEST = 'UPDATE_TESTS'
+ADD_NEW_COURSE_REQUEST = 'ADD_NEW_COURSE'
def lambda_handler(event, context):
+ """ This lambda handler is meant to take in an event and context, then:
+
+ 1. Parse out the homework details: homework_number, secret_key, request_type, payload
+ 2. Overwrite secret_key and homework_id with their textual versions
+ 3. Determine which type of request is inbound
+ > HOMEWORK_ID_REQUEST: return the homework ID in string form
+ > UPDATE_METADATA_REQUEST: update the assignment metadata in the dynamo db
+ > UPDATE_TESTS_REQUEST: update the test cases in the dynamo db
+ """
try:
homework_number, secret_key, request_type, payload = parse_event(event)
secret_key = get_course_id(secret_key)
@@ -44,23 +68,33 @@ def lambda_handler(event, context):
test_cases = payload['test_cases']
update_tests(homework_id, test_cases, libraries)
response = 'Success: Test cases updated successfully.'
+ elif request_type == ADD_NEW_COURSE_REQUEST:
+ add_course(payload['secret_key'], payload['course_id'])
+ response = 'Success: Course created successfully.'
return build_http_response(SUCCESS, response)
except Exception as exception:
return build_http_response(ERROR, exception)
def parse_event(event):
+ """ Simple helper method to parse an event as a dict.
+ Returns: (homework_number, secret_key, request_type, payload)
+
+ Payload should contain metadata
+ """
try:
body = ast.literal_eval(event['body'])
return body['homework_number'], \
body['secret_key'], \
body['request_type'], \
- deserialize(body['payload'])
+ deserialize(body['payload'])
except:
raise Exception('Malformed payload.')
-
-
+
+
def get_course_id(secret_key):
+ """ Returns the course ID based on a secret key. Response would look something like: CIS545_Spring_2019
+ """
try:
response = dynamo.get_item(TableName = CLASSES_TABLE, Key={'secret_key': {'S': secret_key}})
return response['Item']['course_id']['S']
@@ -69,10 +103,45 @@ def get_course_id(secret_key):
def get_homework_id(course_id, homework_number):
+ """ Helper function to get the unique key of a given homework based on where it falls in the
+ assignment schedule of a course. Gets appended to the course_id, then returned in long string
+ in the fashion of: CIS545_Spring_2019_HW1
+ """
return '{}_HW{}'.format(course_id, homework_number)
+def add_course(secret_key, course_id):
+ """ Function to create a new course with an ID and secret
+ """
+ try:
+ db_entry = {
+ 'TableName': CLASSES_TABLE,
+ 'Item': {
+ 'secret_key': {
+ 'S': secret_key
+ },
+ 'course_id': {
+ 'S': course_id
+ }
+ }
+ }
+ dynamo.put_item(**db_entry)
+ except Exception as e:
+ raise Exception('ERROR: Could not create new course. {}'.format(e))
+
+
def update_metadata(homework_id, payload):
+ """ Function to configure the metadata of a homework. The actual setup
+ of the metadata is contained in the payload, but must be in the form:
+
+ {
+ max_daily_submissions: -1,
+ total_score: -1,
+ deadline: ...
+ }
+
+ Once parsed, it gets added to the DB or updated if already in existence
+ """
try:
db_entry = {
'TableName': METADATA_TABLE,
@@ -98,6 +167,8 @@ def update_metadata(homework_id, payload):
def update_tests(homework_id, test_cases, libraries):
+ """ This adds a test case to the dynamo DB to be returned later in grading
+ """
try:
db_entry = {
'TableName': TEST_CASES_TABLE,
@@ -119,6 +190,8 @@ def update_tests(homework_id, test_cases, libraries):
def get_additional_libraries(libraries): # TO-FINISH #
+ """ Appears unfinished per comment, but imports libraries required for use in grading
+ """
try:
packages = libraries['packages']
imports = libraries['imports']
@@ -145,16 +218,31 @@ def get_additional_libraries(libraries): # TO-FINISH #
def serialize(obj):
+ """ Simple helper method to encode a serialized object
+
+ Code is duplicated here. Possibly because the other file may not always be
+ accessible?
+ """
byte_serialized = dill.dumps(obj, recurse = True)
return base64.b64encode(byte_serialized).decode("utf-8")
def deserialize(obj):
+ """ Simple helper method to decode a base64 encoded object
+
+ Code is duplicated here. Possibly because the other file may not always be
+ accessible?
+ """
byte_decoded = base64.b64decode(obj)
return dill.loads(byte_decoded)
def build_http_response(status_code, message):
+ """ Build formatted http response as string
+
+ Code is duplicated here. Possibly because the other file may not always be
+ accessible?
+ """
return {
'statusCode': status_code,
'body': str(message),
diff --git a/pip/penngrader/backend.py b/pip/penngrader/backend.py
index ae69b71..0e08452 100644
--- a/pip/penngrader/backend.py
+++ b/pip/penngrader/backend.py
@@ -15,6 +15,7 @@
UPDATE_METADATA_REQUEST = 'UPDATE_METADATA'
UPDATE_TESTS_REQUEST = 'UPDATE_TESTS'
GRADES_REQUEST = 'ALL_STUDENTS_GRADES'
+ADD_NEW_COURSE_REQUEST = 'ADD_NEW_COURSE'
# Lambda endpoints
config_api_url = 'https://uhbuar7r8e.execute-api.us-east-1.amazonaws.com/default/HomeworkConfig'
@@ -23,21 +24,32 @@
grades_api_key = 'lY1O5NDRML9zEyRvWhf0c1GeEYFe3BE710Olbh3R'
def is_function(val):
+ """ Simple helper to confirm an arg is a function
+ """
return type(val) == types.FunctionType
def is_module(val):
+ """ Simple helper to confirm an arg is a module
+ """
return type(val) == types.ModuleType
def is_external(name):
+ """ Simple helper to determine whether the module is already installed
+ """
return name not in ['__builtin__','__builtins__', 'penngrader','_sh', '__main__'] and 'penngrader' not in name
class PennGraderBackend:
+ """ The backened is responsible for initializing test cases, homeworks, courses, etc. This manages those things
+ that a student using the tool would not have access to. Only those with the secret key can manage the backend
+ """
def __init__(self, secret_key, homework_number):
+ """ Initialization function to start up the backend via grades_lambda for a particular homework in a course
+ """
self.secret_key = secret_key
self.homework_number = homework_number
self.homework_id = self._get_homework_id()
@@ -47,8 +59,24 @@ def __init__(self, secret_key, homework_number):
print(response)
else:
print(self.homework_id)
+
+
+ def create_new_course(self, valid_current_secret, new_secret, course_id):
+ request = {
+ 'secret_key': valid_current_secret,
+ 'request_type': ADD_NEW_COURSE_REQUEST,
+ 'payload': self._serialize({
+ 'course_id': course_id,
+ 'secret_key': new_secret
+ })
+ }
+ print(self._send_request(request, config_api_url, config_api_key))
+
def update_metadata(self, deadline, total_score, max_daily_submissions):
+ """ Updates the metadata of an assignment, including deadline, total score,
+ and maximum number of daily submissions, if desired
+ """
request = {
'homework_number' : self.homework_number,
'secret_key' : self.secret_key,
@@ -63,6 +91,9 @@ def update_metadata(self, deadline, total_score, max_daily_submissions):
def update_test_cases(self):
+ """ Updates the test cases in an assignment broadly across the entire
+ assignment
+ """
request = {
'homework_number' : self.homework_number,
'secret_key' : self.secret_key,
@@ -76,6 +107,9 @@ def update_test_cases(self):
def get_raw_grades(self, with_deadline = False):
+ """ Retrieve all of the grades for all students that have attempted a given homework. Returns
+ a dataframe containing the results. Helper function leveraged by the get_grades func
+ """
request = {
'homework_id' : self.homework_id,
'secret_key' : self.secret_key,
@@ -93,6 +127,8 @@ def get_raw_grades(self, with_deadline = False):
return pd.DataFrame(grades)
def get_grades(self):
+ """ Retrieves grade details for the current assignment
+ """
grades_df, deadline = self.get_raw_grades(with_deadline = True)
if grades_df is not None:
@@ -131,6 +167,8 @@ def get_grades(self):
def _get_homework_id(self):
+ """ Hits the homework_config_lambda for the homework id: ex. CIS545_Spring_2019_HW1
+ """
request = {
'homework_number' : self.homework_number,
'secret_key' : self.secret_key,
@@ -139,8 +177,10 @@ def _get_homework_id(self):
}
return self._send_request(request, config_api_url, config_api_key)
-
+
def _send_request(self, request, api_url, api_key):
+ """ Function to send all requests, regardless of type, to their appropriate lambda
+ """
params = json.dumps(request).encode('utf-8')
headers = {'content-type': 'application/json', 'x-api-key': api_key}
request = urllib.request.Request(api_url, data=params, headers=headers)
@@ -148,10 +188,12 @@ def _send_request(self, request, api_url, api_key):
response = urllib.request.urlopen(request)
return '{}'.format(response.read().decode('utf-8'))
except HTTPError as error:
- return 'Error: {}'.format(error.read().decode("utf-8"))
+ return 'Error: {}'.format(error.read().decode("utf-8"))
+
-
def _get_imported_libraries(self):
+ """ Returns a list of all packages, imports, functions currently imported
+ """
# Get all externally imported base packages
packages = set() # (package, shortname)
for shortname, val in list(globals().items()):
@@ -180,8 +222,10 @@ def _get_imported_libraries(self):
'functions' : list(functions)
}
-
+
def _get_test_cases(self):
+ """ Gets all test cases that are currently on file
+ """
# Get all function imports
test_cases = {}
for shortname, val in list(globals().items()):
@@ -192,13 +236,16 @@ def _get_test_cases(self):
pass
return test_cases
-
+
def _serialize(self, obj):
- '''Dill serializes Python object into a UTF-8 string'''
+ """ Dill serializes Python object into a UTF-8 string
+ """
byte_serialized = dill.dumps(obj, recurse = False)
return base64.b64encode(byte_serialized).decode("utf-8")
-
+
def _deserialize(self, obj):
+ """ Dill deserializes UTF-8 string into python object
+ """
byte_decoded = base64.b64decode(obj)
return dill.loads(byte_decoded)
\ No newline at end of file
diff --git a/pip/penngrader/grader.py b/pip/penngrader/grader.py
index d243e34..74374bd 100644
--- a/pip/penngrader/grader.py
+++ b/pip/penngrader/grader.py
@@ -14,8 +14,13 @@
STUDENT_GRADE_REQUEST = 'STUDENT_GRADE'
class PennGrader:
+ """ The grader is responsible for the actual testing. This is what the student will use
+ to test their code
+ """
def __init__(self, homework_id, student_id):
+ """ Initialization function to start up the grader instance
+ """
if '_' in str(student_id):
raise Exception("Student ID cannot contain '_'")
self.homework_id = homework_id
@@ -25,6 +30,8 @@ def __init__(self, homework_id, student_id):
def grade(self, test_case_id, answer):
+ """ This function will hit the grader_lambda to test a student's function
+ """
request = {
'homework_id' : self.homework_id,
'student_id' : self.student_id,
@@ -35,6 +42,8 @@ def grade(self, test_case_id, answer):
print(response)
def _send_request(self, request, api_url, api_key):
+ """ Function to send requests, regardless of type
+ """
params = json.dumps(request).encode('utf-8')
headers = {'content-type': 'application/json', 'x-api-key': api_key}
request = urllib.request.Request(api_url, data=params, headers=headers)
@@ -45,5 +54,7 @@ def _send_request(self, request, api_url, api_key):
return 'Error: {}'.format(error.read().decode("utf-8"))
def _serialize(self, obj):
+ """ Encodes an object to UTF-8
+ """
byte_serialized = dill.dumps(obj, recurse = True)
return base64.b64encode(byte_serialized).decode("utf-8")
\ No newline at end of file