예제 #1
0
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
    """
    participant_count = wheel['participant_count']
    # All other participants get a slice of that participant's weight while the participant is weighted to 0
    if participant_count > 1:
        weight_share = participant['weight'] / (participant_count - 1)
        with WheelParticipant.batch_writer() as batch:
            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)
                batch.put_item(Item=p)
예제 #2
0
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})
예제 #3
0
def suggest_participant(wheel):
    """
    Suggest a participant given weights of all participants with randomization.
    This is weighted selection where all participants start with a weight of 1,
    so the sum of the weights will always equal the number of participants
    :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: ID of the suggested participant
    """
    if wheel['participant_count'] == 0:
        raise BadRequestError("Cannot suggest a participant when the wheel doesn't have any!")

    query_params = {'KeyConditionExpression': Key('wheel_id').eq(wheel['id'])}

    participants = WheelParticipant.iter_query(**query_params)
    selected_total_weight = random.random() * float(sum([participant['weight'] for participant in participants]))

    # We do potentially want to return the last participant just as a safeguard for rounding errors
    participant = None
    for participant in WheelParticipant.iter_query(**query_params):
        selected_total_weight -= float(participant['weight'])
        if selected_total_weight <= 0:
            return participant['id']
    return participant['id']
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
예제 #5
0
def test_selection_cycle(mock_dynamodb, setup_data, mock_participant_table):
    def get_participant_with_id(participants, target_id):
        for p in participants:
            if p['id'] == target_id:
                return p
        return None

    rngstate = random.getstate()
    random.seed(0)  # Make the (otherwise pseudorandom) test repeatable.

    participants = WheelParticipant.scan({})['Items']
    wheel = setup_data['wheel']
    total_weight_of_chosens = 0
    num_iterations = 200

    distro = {}
    for participant in participants:
        distro[participant['name']] = 0

    for _ in range(0, num_iterations):

        chosen_id = choice_algorithm.suggest_participant(wheel)

        chosen_was = get_participant_with_id(participants, chosen_id)
        chosen_was_weight = chosen_was['weight']

        distro[chosen_was['name']] = distro[chosen_was['name']] + 1

        choice_algorithm.select_participant(wheel, chosen_was)

        participants = WheelParticipant.scan({})['Items']

        chosen_now = get_participant_with_id(participants, chosen_id)
        chosen_now_weight = chosen_now['weight']

        assert chosen_was_weight > 0.0
        assert chosen_now_weight == 0
        total_weight_of_chosens += chosen_was_weight

        total_weight = sum(
            [participant['weight'] for participant in participants])
        assert abs(total_weight - len(participants)) < epsilon

    # Must match human-inspected reasonable values for the RNG seed defined
    # above for number of times each participant was chosen, and the total
    # weight of participants selected. These are a rough equivalent to
    # ensuring that the sequence of chosen participants matches the observed
    #  test run.
    dv = list(distro.values())
    list.sort(dv)
    human_observed_selection_counts = [26, 27, 27, 29, 29, 31, 31]
    human_observed_total_weight = 317.8786415239279
    assert dv == human_observed_selection_counts
    assert abs(float(total_weight_of_chosens) -
               human_observed_total_weight) < epsilon

    # Put things back the way they were.
    random.setstate(rngstate)
예제 #6
0
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 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 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 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 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)))
예제 #12
0
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}))
예제 #13
0
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}))
예제 #14
0
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}))
예제 #15
0
    def set_up_test(setup_data, mock_participant_table):
        #  Select a participant to take everyone off their 1.0 scores.
        choice_algorithm.select_participant(setup_data['wheel'],
                                            setup_data['participants'][0])

        # Adjust participants to different weights to take the wheel out of balance.
        participants = mock_participant_table.query(KeyConditionExpression=Key(
            'wheel_id').eq(setup_data['wheel']['id']))['Items']
        with WheelParticipant.batch_writer() as batch:
            for p in participants:
                p['weight'] += Decimal(.15)
                batch.put_item(Item=p)

        # Confirm that the wheel is out of balance.
        participants = mock_participant_table.query(KeyConditionExpression=Key(
            'wheel_id').eq(setup_data['wheel']['id']))['Items']
        participant_weights = [
            participant['weight'] for participant in participants
        ]

        total_weight = Decimal(0)
        for weight in participant_weights:
            total_weight += weight
        assert abs(total_weight - Decimal(8.05)) < epsilon