def test_suggest_participant_hidden_rig(mock_dynamodb, mock_participant_table, mock_wheel_table): participants = [{ 'id': get_uuid(), 'wheel_id': WHEEL_ID, 'name': name, } for name in ['Rig me!', 'I cannot win!']] with mock_participant_table.batch_writer() as batch: for participant in participants: batch.put_item(Item=participant) mock_wheel_table.update_item(Key={'id': WHEEL_ID}, **to_update_kwargs({ 'rigging': { 'hidden': True, 'participant_id': participants[0]['id'] } })) response = wheel_participant.suggest_participant({ 'body': {}, 'pathParameters': { 'wheel_id': WHEEL_ID } }) body = json.loads(response['body']) assert response['statusCode'] == 200 assert body['participant_id'] == participants[0]['id'] assert 'rigged' not in body
def update_participant(event): """ Update a participant's name and/or url :param event: Lambda event containing the API Gateway request body including updated name or url and the 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) }, "body": { "id": string ID of the participant (DDB Hash Key), "name": string name of the wheel (optional), "url: Valid URL for the participant (optional), } } :return: response dictionary containing the updated 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'] participant_id = event['pathParameters']['participant_id'] # Check that the participant exists participant = WheelParticipant.get_existing_item(Key={ 'id': participant_id, 'wheel_id': wheel_id }) body = event['body'] params = {'updated_at': get_utc_timestamp()} if not check_string(body.get('name', 'Not Specified')) or not check_string( body.get('url', 'Not Specified')): raise base.BadRequestError( "Participants names and urls must be at least 1 character in length" ) if 'name' in body: params['name'] = body['name'] if 'url' in body: params['url'] = body['url'] WheelParticipant.update_item(Key={ 'id': participant_id, 'wheel_id': wheel_id }, **to_update_kwargs(params)) participant.update(params) return participant
def test_select_participant_removes_rigging(mock_dynamodb, mock_participant_table, mock_wheel_table): mock_wheel_table.update_item(Key={'id': WHEEL_ID}, **to_update_kwargs({'rigging': {}})) participant = { 'id': get_uuid(), 'wheel_id': WHEEL_ID, 'name': 'Pick me!', 'url': 'https://amazon.com', 'weight': 1 } mock_participant_table.put_item(Item=participant) event = { 'body': {}, 'pathParameters': { 'wheel_id': WHEEL_ID, 'participant_id': participant['id'] } } response = wheel_participant.select_participant(event) assert response['statusCode'] == 201 assert 'rigging' not in mock_wheel_table.get_existing_item( Key={'id': WHEEL_ID})
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 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 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 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 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