Example #1
0
def test_get_twice_returns_same_instance():
    data = {"examples/1": {"name": "Paula"}}
    session = Session(data)

    ex1 = session.get(Example, 1)
    ex2 = session.get(Example, 1)
    assert ex1 is ex2
Example #2
0
def get_settings(user_id, storage, directory=None):
    """
    Look up the current settings dict for a given user ID.

    This takes, in order of preference:

    1. The global defaults,
    2. Any experiment settings for experiments the user is in,
    3. User-specific overrides.
    """
    with storage.transaction(read_only=True) as store:
        session = Session(store)

        defaults = store.get("defaults", {})
        bucket_id = user_bucket(user_id)
        bucket = session.get(Bucket, bucket_id, default=EMPTY)

        if bucket.needs_constraints():
            user_entry = directory.lookup(user_id)
        else:
            user_entry = None

        bucket_settings = bucket.get_settings(user_entry)

        overrides = store.get("overrides/{user_id}".format(user_id=user_id),
                              {})

    return {**defaults, **bucket_settings, **overrides}
Example #3
0
def test_error_when_removing_from_different_instance():
    session1 = Session({"examples/1": {}})
    session2 = Session({})

    ex1 = session1.get(Example, 1)
    with pytest.raises(RuntimeError):
        session2.remove(ex1)
Example #4
0
def test_remove_is_idempotent():
    data = {"examples/1": {"name": "Paula"}}
    session = Session(data)

    ex1 = session.get(Example, 1)
    session.remove(ex1)
    session.remove(ex1)
    session.flush()
Example #5
0
def test_get_then_remove_entity():
    data = {"examples/1": {"name": "Paula"}}
    session = Session(data)

    ex1 = session.get(Example, 1)
    session.remove(ex1)
    session.flush()

    assert data == {}
Example #6
0
def test_get_then_edit_entity():
    data = {"examples/1": {"name": "Paula"}}
    session = Session(data)

    ex1 = session.get(Example, 1)
    ex1.name = "Paul"
    session.flush()

    assert data == {"examples/1": {"name": "Paul"}}
Example #7
0
def test_sessions_are_not_global():
    instance_session1 = Example(pk=1)
    session = Session({})
    session.add(instance_session1)
    session.flush()

    session2 = Session({})
    with pytest.raises(KeyError):
        instance_session2 = session2.get(Example, 1)
Example #8
0
def release(store, name, constraints, branches):
    """
    Release a given configuration.

    This is the main utility for launching experiments. `store` is the storage
    transaction mapping. `name` is a free-form name (generally an experiment
    ID), `constraints` is the constraints covering this release, and `branches`
    is an iterable of (branch ID, num buckets, settings) triples.

    The utility will select buckets which are not already covering the given
    settings, which allows for partial rollout before running a test.
    """
    session = Session(store)

    # Branches is a list of (name, n_buckets, settings) tuples
    all_buckets = [
        session.get(Bucket, x, default=CREATE) for x in range(NUM_BUCKETS)
    ]

    edited_settings = set.union(*[set(x[2].keys()) for x in branches])

    conflicting_experiments = set()
    valid_bucket_indices = []

    for idx, bucket in enumerate(all_buckets):
        if is_valid_bucket(bucket, edited_settings, constraints):
            valid_bucket_indices.append(idx)
        else:
            for entry in bucket.entries:
                # Determine if this entry is a potential conflict
                if set(entry.settings.keys()).isdisjoint(edited_settings):
                    continue
                conflicting_experiment_id, _ = entry.key
                conflicting_experiments.add(conflicting_experiment_id)

    random.shuffle(valid_bucket_indices)

    for branch_name, n_buckets, settings in branches:
        key = [name, branch_name]
        bucket_indices = valid_bucket_indices[:n_buckets]

        if len(bucket_indices) < n_buckets:
            raise NotEnoughBucketsException(conflicts=conflicting_experiments)

        valid_bucket_indices = valid_bucket_indices[n_buckets:]

        for bucket_idx in bucket_indices:
            bucket = all_buckets[bucket_idx]

            bucket.add(key, settings, constraints)

    session.flush()
Example #9
0
    def handle(self, experiment):
        """Dispatch request."""
        if self.request.method != "POST":
            raise MethodNotAllowed()

        user_ids = self.request.form.getlist("u")

        with self.config.storage.transaction(read_only=True) as store:
            session = Session(store)

            try:
                experiment_config = Experiment.from_store(store, experiment)
            except LookupError:
                raise NotFound(
                    "No experiment with ID {experiment_id!r}".format(
                        experiment_id=experiment
                    )
                )

            buckets = [
                session.get(Bucket, idx, default=EMPTY) for idx in range(NUM_BUCKETS)
            ]

            branch_ids = [branch["id"] for branch in experiment_config.branches]
            branches = {x: [] for x in branch_ids}

            relevant_settings = set()

            for branch_config in experiment_config.branches:
                relevant_settings.update(branch_config["settings"].keys())

            for user_id in user_ids:
                user_entry = self.config.directory.lookup(user_id)

                if not experiment_config.includes_user(user_entry):
                    continue

                user_overrides = store.get(
                    "overrides/{user_id}".format(user_id=user_id), {}
                )

                if any(x in relevant_settings for x in user_overrides.keys()):
                    continue

                bucket = buckets[user_bucket(user_id)]

                for branch_id, members in branches.items():
                    if bucket.covers([experiment_config.id, branch_id]):
                        members.append(user_id)

        return {"branches": branches}
Example #10
0
def close(store, name, constraints, branches):
    """
    Close a given configuration.

    This is the main utility for ending experiments. `store` is the storage
    transaction mapping. `name` is a free-form name (generally an experiment
    ID), `constraints` is the constraints covering this release, and `branches`
    is an iterable of (branch ID, num buckets, settings) triples.

    Deliberately looks like `release` and works to counteract its effects.
    """
    session = Session(store)

    keys = [[name, x[0]] for x in branches]

    for idx in range(NUM_BUCKETS):
        bucket = session.get(Bucket, idx, default=None)
        if bucket is not None:
            for key in keys:
                bucket.remove(key)

    session.flush()
Example #11
0
def test_get_missing_entity_raises_keyerror():
    data = {"examples/1": {"name": "Paula"}}
    session = Session(data)

    with pytest.raises(KeyError):
        session.get(Example, 2)
Example #12
0
def test_get():
    data = {"examples/1": {"name": "Paula"}}
    session = Session(data)

    ex1 = session.get(Example, 1)
    assert ex1.name == "Paula"
Example #13
0
def test_can_get_empty_bucket_from_old_format():
    session = Session({"buckets/1": []})
    bucket = session.get(Bucket, 1)
    # Force bucket to a string in order to reify the fields. This validates
    # that the fields are accessible.
    str(bucket)