def select_participant(event): """ Indicates selection of a participant by the wheel. This will cause updates to the weights for all participants or removal of rigging if the wheel is rigged. :param event: Lambda event containing the API Gateway request path parameters wheel_id and participant_id { "pathParameters": { "wheel_id": string ID of the wheel to rig (DDB Hash Key) "participant_id": string ID of the participant to rig (DDB Hash Key) }, } :return: response dictionary """ wheel_id = event['pathParameters']['wheel_id'] participant_id = event['pathParameters']['participant_id'] wheel = Wheel.get_existing_item(Key={'id': wheel_id}) participant = WheelParticipant.get_existing_item(Key={ 'id': participant_id, 'wheel_id': wheel_id }) choice_algorithm.select_participant(wheel, participant) # Undo any rigging that has been set up Wheel.update_item(Key={'id': wheel['id']}, UpdateExpression='remove rigging')
def rig_participant(event): """ Rig the specified wheel for the specified participant. Default behavior is comical rigging (hidden == False) but hidden can be specified to indicate deceptive rigging (hidden == True) :param event: Lambda event containing the API Gateway request path parameters wheel_id and participant_id { "pathParameters": { "wheel_id": string ID of the wheel to rig (DDB Hash Key) "participant_id": string ID of the participant to rig (DDB Hash Key) }, "body": { "hidden": boolean indicates deceptive rigging if True, comical if False } } :return: response dictionary """ # By default, rigging the wheel isn't hidden but they can be wheel_id = event['pathParameters']['wheel_id'] participant_id = event['pathParameters']['participant_id'] hidden = bool(event['body'].get('hidden', False)) update = {'rigging': {'participant_id': participant_id, 'hidden': hidden}} Wheel.update_item(Key={'id': wheel_id}, **to_update_kwargs(update))
def delete_wheel(event): """ Deletes the wheel and all of its participants :param event: Lambda event containing the API Gateway request path parameter wheel_id { "pathParameters": { "wheel_id": string ID of the wheel (DDB Hash Key) } } :return: response dictionary """ wheel_id = event['pathParameters']['wheel_id'] # DynamoDB always succeeds for delete_item, Wheel.delete_item(Key={'id': wheel_id}) # Clear out all participants of the wheel. Query will be empty if it was already deleted with WheelParticipant.batch_writer() as batch: query_params = { 'KeyConditionExpression': Key('wheel_id').eq(wheel_id), 'ProjectionExpression': 'id' } # We don't use the default generator here because we don't want the deletes to change the query results for p in list(WheelParticipant.iter_query(**query_params)): batch.delete_item(Key={'id': p['id'], 'wheel_id': wheel_id})
def list_participants(event): """ Gets the participants for the specified wheel_id :param event: Lambda event containing the API Gateway request path parameter wheel_id { "pathParameters": { "wheel_id": string ID of the wheel (DDB Hash Key) }, } :return: response dictionary containing a list of participants { "body": [ participant1, participant2, ... participantn, ] } """ wheel_id = event['pathParameters']['wheel_id'] # Make sure the wheel exists Wheel.get_existing_item(Key={'id': wheel_id}) return list( WheelParticipant.iter_query( KeyConditionExpression=Key('wheel_id').eq(wheel_id)))
def wrap_participant_creation(wheel, participant): participant['weight'] = 1 yield Wheel.update_item(Key={'id': wheel['id']}, **to_update_kwargs({ 'participant_count': wheel['participant_count'] + 1 }))
def wrap_participant_creation(wheel, participant): participant['weight'] = 1 yield count = 0 with WheelParticipant.batch_writer() as batch: for p in WheelParticipant.iter_query( KeyConditionExpression=Key('wheel_id').eq(wheel['id'])): count += 1 Wheel.update_item(Key={'id': wheel['id']}, **to_update_kwargs({'participant_count': count}))
def get_wheel(event): """ Returns the wheel object corresponding to the given wheel_id :param event: Lambda event containing the API Gateway request path parameter wheel_id { "pathParameters": { "wheel_id": string ID of the wheel (DDB Hash Key) } } :return: response dictionary containing the requested wheel object if successful { "body": { "id": string ID of the wheel (DDB Hash Key), "name": string name of the wheel, "participant_count": number of participants in the wheel, "created_at": creation timestamp, "updated_at": updated timestamp, } } """ return Wheel.get_existing_item( Key={'id': event['pathParameters']['wheel_id']})
def delete_participant(event): """ Deletes the participant from the wheel and redistributes wheel weights :param event: Lambda event containing the API Gateway request path parameters wheel_id and participant_id { "pathParameters": { "wheel_id": string ID of the wheel (DDB Hash Key) "participant_id": string ID of the participant (DDB Hash Key) }, } :return: response dictionary """ wheel_id = event['pathParameters']['wheel_id'] participant_id = event['pathParameters']['participant_id'] # Make sure the wheel exists wheel = Wheel.get_existing_item(Key={'id': wheel_id}) # REST-ful Deletes are idempotent and should not error if it's already been deleted response = WheelParticipant.delete_item(Key={ 'wheel_id': wheel_id, 'id': participant_id }, ReturnValues='ALL_OLD') if 'Attributes' in response: choice_algorithm.on_participant_deletion(wheel, response['Attributes'])
def suggest_participant(event): """ Returns a suggested participant to be selected by the next wheel spin :param event: Lambda event containing the API Gateway request path parameter wheel_id { "pathParameters": { "wheel_id": string ID of the wheel (DDB Hash Key) }, } :return: response dictionary containing a selected participant_id { "body": { "participant_id": string ID of the suggested participant (DDB Hash Key), "rigged": True (if rigged, otherwise this key is not present) } } """ wheel_id = event['pathParameters']['wheel_id'] wheel = Wheel.get_existing_item(Key={'id': wheel_id}) if 'rigging' in wheel: participant_id = wheel['rigging']['participant_id'] # Use rigging only if the rigged participant is still available if 'Item' in WheelParticipant.get_item(Key={ 'wheel_id': wheel_id, 'id': participant_id }): return_value = {'participant_id': participant_id} # Only return rigged: True if we're not using hidden rigging if not wheel['rigging'].get('hidden', False): return_value['rigged'] = True return return_value return {'participant_id': choice_algorithm.suggest_participant(wheel)}
def list_wheels(event): """ Get all available wheels :param event: Lambda event containing query string parameters that are passed to Boto's scan() API for the wheel table { "queryStringParameters": { ... } } :return: List of wheels { "body": "Count": number of wheels, "Items": [ wheel1, wheel2, wheeln, ], "ScannedCount": number of items before queryStringParameters were applied, } } """ parameters = event.get('queryStringParameters', None) or {} return Wheel.scan(**parameters)
def on_participant_deletion(wheel, participant): """ Normalize the remaining participant weights to account for participant removal. The ratio is based on the following: 1) The participant should be at weight=1 when it leaves the system (which is the same as it arrived) 2) That difference should be split by the remaining participants proportional by weight This ensures that 'weight=0' participants are still at weight=0 and that the sum of all weights is equal to the number of participants, so new additions are treated fairly :param wheel: Wheel dictionary: { "id": string ID of the wheel (DDB Hash Key), "name": string name of the wheel, "participant_count": number of participants in the wheel, } :param participant: Participant dictionary: { "id": string ID of the wheel (DDB Hash Key), "name": string name of the wheel, "url": Participant's URL, "wheel_id": string ID of the wheel the participant belongs to, } :return: None """ total_weight = participant['weight'] for p in WheelParticipant.iter_query( KeyConditionExpression=Key('wheel_id').eq(wheel['id'])): total_weight += p['weight'] weight = participant['weight'] remaining_weight = total_weight - weight # <-- no longer presumes existing weight balance via 'int(participant_count)' ratio = (1 + ((weight - 1) / remaining_weight)) if ( remaining_weight != 0) else 1 num_participants = Decimal(0) with WheelParticipant.batch_writer() as batch: for p in WheelParticipant.iter_query( KeyConditionExpression=Key('wheel_id').eq(wheel['id'])): if p['id'] != participant['id']: # This is cast to a string before turning into a decimal because of rounding/inexact guards in boto3 p['weight'] = Decimal( str(float(p['weight']) * float(ratio))) if (remaining_weight != 0) else 1 batch.put_item(Item=p) num_participants = num_participants + 1 Wheel.update_item(Key={'id': wheel['id']}, **to_update_kwargs( {'participant_count': num_participants}))
def select_participant(wheel, participant): """ Register the selection of a participant by updating the weights of all participants for a given wheel :param wheel: Wheel dictionary: { "id": string ID of the wheel (DDB Hash Key), "name": string name of the wheel, "participant_count": number of participants in the wheel, } :param participant: Participant dictionary: { "id": string ID of the participant (DDB Hash Key), "name": string name of the participant, "url": Participant's URL, "wheel_id": string ID of the wheel the participant belongs to, "weight": participant's weight in the selection algorithm } :return: None """ num_participants = 0 total_weight = Decimal(0) for p in WheelParticipant.iter_query( KeyConditionExpression=Key('wheel_id').eq(wheel['id'])): num_participants = num_participants + 1 total_weight += p['weight'] # Factor is the number by which all weights must be multiplied # so total weight will be equal to the number of participants. factor = Decimal(num_participants) / total_weight if num_participants > 1: weight_share = participant['weight'] / Decimal(num_participants - 1) with WheelParticipant.batch_writer() as batch: # Redistribute and normalize the weights. for p in WheelParticipant.iter_query( KeyConditionExpression=Key('wheel_id').eq(wheel['id'])): if p['id'] == participant['id']: p['weight'] = 0 else: p['weight'] += Decimal(weight_share) p['weight'] *= factor batch.put_item(Item=p) Wheel.update_item(Key={'id': wheel['id']}, **to_update_kwargs( {'participant_count': num_participants}))
def unrig_participant(event): """ Remove rigging for the specified wheel :param event: Lambda event containing the API Gateway request path parameter wheel_id { "pathParameters": { "wheel_id": string ID of the wheel (DDB Hash Key) } } :return: response dictionary """ # By default, rigging the wheel isn't hidden but they can be wheel_id = event['pathParameters']['wheel_id'] Wheel.update_item(Key={'id': wheel_id}, UpdateExpression='remove rigging')
def get_sub_wheel_size(wheel_name): resp = Wheel.query( IndexName='name_index', KeyConditionExpression=Key('name').eq(wheel_name) ) if len(resp['Items']): # if a matching wheel is found return int(resp['Items'][0]['participant_count']) or 1 # if wheel size is 0, default to 1 return 1 # default to 1 if no matching wheel is found
def reset_wheel(wheel): """ Resets the weights of all participants in the wheel and updates the wheel's participant count :param wheel: Wheel dictionary: { "id": string ID of the wheel (DDB Hash Key), "name": string name of the wheel, "participant_count": number of participants in the wheel, } :return: None """ count = 0 with WheelParticipant.batch_writer() as batch: for p in WheelParticipant.iter_query(KeyConditionExpression=Key('wheel_id').eq(wheel['id'])): p['weight'] = get_sub_wheel_size(p['name']) batch.put_item(Item=p) count += 1 Wheel.update_item(Key={'id': wheel['id']}, **to_update_kwargs({'participant_count': count}))
def update_wheel(event): """ Update the name of the wheel and/or refresh its participant count :param event: Lambda event containing the API Gateway request path parameter wheel_id { "pathParameters": { "wheel_id": string ID of the wheel (DDB Hash Key) }, "body": { "id": string ID of the wheel (DDB Hash Key), "name": string name of the wheel, } } :return: response dictionary containing the updated wheel object if successful { "body": { "id": string ID of the wheel (DDB Hash Key), "name": string name of the wheel, "participant_count": number of participants in the wheel, "created_at": creation timestamp, "updated_at": updated timestamp, } } """ wheel_id = event['pathParameters']['wheel_id'] key = {'id': wheel_id} # Make sure wheel exists wheel = Wheel.get_existing_item(Key=key) name = event['body'].get('name', None) if not check_string(name): raise base.BadRequestError( "Updating a wheel requires a new name of at least 1 character in length" ) update = {'name': name, 'updated_at': get_utc_timestamp()} Wheel.update_item(Key=key, **to_update_kwargs(update)) # Represent the change locally for successful responses wheel.update(update) return wheel
def create_participant(event): """ Create a participant :param event: Lambda event containing the API Gateway request body including a name and a url and the path parameter wheel_id { "pathParameters": { "wheel_id": string ID of the wheel (DDB Hash Key) }, "body": { "name": participant name string, "url: Valid URL for the participant, } } :return: response dictionary containing new participant object if successful { "body": { "id": string ID of the participant (DDB Hash Key), "wheel_id": string ID of the wheel (DDB Hash Key), "name": string name of the wheel, "url: URL for the participant, "created_at": creation timestamp, "updated_at": updated timestamp, } } """ wheel_id = event['pathParameters']['wheel_id'] body = event['body'] if not check_string(body.get('name', None)) or not check_string( body.get('url', None)): raise base.BadRequestError( "Participants require a name and url which must be at least 1 character in length" ) wheel = Wheel.get_existing_item(Key={'id': wheel_id}) create_timestamp = get_utc_timestamp() participant = { 'wheel_id': wheel_id, 'id': get_uuid(), 'name': body['name'], 'url': body['url'], 'created_at': create_timestamp, 'updated_at': create_timestamp, } with choice_algorithm.wrap_participant_creation(wheel, participant): WheelParticipant.put_item(Item=participant) return participant
def create_wheel(event): """ Create a wheel. Requires a name :param event: Lambda event containing the API Gateway request body including a name { "body": { "name": string wheel name, } } :return: response dictionary containing new wheel object if successful { "body": { "id": string ID of the wheel (DDB Hash Key), "name": string name of the wheel, "participant_count": number of participants in the wheel, "created_at": creation timestamp, "updated_at": updated timestamp, } } """ create_timestamp = get_utc_timestamp() body = event['body'] if body is None or not check_string(body.get('name', None)): raise base.BadRequestError( f"New wheels require a name that must be a string with a length of at least 1. Got: {body}" ) wheel = { 'id': get_uuid(), 'name': body['name'], 'created_at': create_timestamp, 'updated_at': create_timestamp, } with choice_algorithm.wrap_wheel_creation(wheel): Wheel.put_item(Item=wheel) return wheel
def reset_wheel(event): """ Resets the weights of all participants of the wheel :param event: Lambda event containing the API Gateway request path parameter wheel_id { "pathParameters": { "wheel_id": string ID of the wheel (DDB Hash Key) } } :return: response dictionary """ # Ensure that the wheel exists wheel_id = event['pathParameters']['wheel_id'] wheel = Wheel.get_existing_item(Key={'id': wheel_id}) choice_algorithm.reset_wheel(wheel)
def test_fix_incorrect_participant_count(mock_dynamodb, setup_data, mock_wheel_table): out_of_whack = 999 wheel = setup_data['wheel'] wheel_id = wheel['id'] proper_participant_count = wheel['participant_count'] # # # # We will first test this on a select_participant operation. # Throw the participant count way out of whack. mock_wheel_table.update_item(Key={'id': wheel['id']}, **to_update_kwargs( {'participant_count': out_of_whack})) participant_count = mock_wheel_table.query(KeyConditionExpression=Key( 'id').eq(wheel['id']))['Items'][0].get('participant_count') # # Ensure it's out of whack. assert abs(out_of_whack - participant_count) < epsilon # Select a participant to cause correction of participant count. wheel = Wheel.get_existing_item(Key={'id': wheel_id}) choice_algorithm.select_participant(wheel, setup_data['participants'][0]) # ...and ensure it's back into whack. participant_count = mock_wheel_table.query(KeyConditionExpression=Key( 'id').eq(wheel['id']))['Items'][0].get('participant_count') assert abs(Decimal(proper_participant_count) - participant_count) < epsilon # # # # We will next test this on a delete_participant operation. # Throw the participant count way out of whack. mock_wheel_table.update_item(Key={'id': wheel['id']}, **to_update_kwargs( {'participant_count': out_of_whack})) participant_count = mock_wheel_table.query(KeyConditionExpression=Key( 'id').eq(wheel['id']))['Items'][0].get('participant_count') # # Ensure it's out of whack. assert abs(out_of_whack - participant_count) < epsilon # Delete a participant to cause correction of participant count. event = { 'body': {}, 'pathParameters': { 'wheel_id': wheel_id, 'participant_id': setup_data['participants'][0]['id'] } } wheel_participant.delete_participant(event) # # ...and ensure it's back into whack. participant_count = mock_wheel_table.query(KeyConditionExpression=Key( 'id').eq(wheel['id']))['Items'][0].get('participant_count') assert abs((Decimal(proper_participant_count) - 1) - participant_count) < epsilon # # # # We will next test this on a create_participant operation. # Throw the participant count way out of whack. mock_wheel_table.update_item(Key={'id': wheel['id']}, **to_update_kwargs( {'participant_count': out_of_whack})) participant_count = mock_wheel_table.query(KeyConditionExpression=Key( 'id').eq(wheel['id']))['Items'][0].get('participant_count') # # Ensure it's out of whack. assert abs(out_of_whack - participant_count) < epsilon # Add a participant to cause correction of participant count. event = { 'pathParameters': { 'wheel_id': wheel_id }, 'body': { 'name': 'Ishmael-on-the-Sea', 'url': 'https://amazon.com' } } wheel_participant.create_participant(event) # # ...and ensure it's back into whack. participant_count = mock_wheel_table.query(KeyConditionExpression=Key( 'id').eq(wheel['id']))['Items'][0].get('participant_count') assert abs((Decimal(proper_participant_count)) - participant_count) < epsilon