Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions examples/datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,22 @@
token = "<YOUR API TOKEN HERE>"
project_id = "<YOUR PROJECT ID HERE>"
recording_id = "<YOUR RECORDING ID HERE>"
event_id = "<YOUR EVENT ID HERE>"

client = Client(token=token)

# An event episode uses the event's time range and its device's overlapping recordings.
episodes = client.create_episodes(
project_id=project_id,
episodes=[{"recordings": [recording_id]}],
episodes=[
{"recordings": [recording_id]},
{"event_id": event_id, "metadata": {"label": "example"}},
],
)
dataset = client.create_dataset(
project_id=project_id,
name="Training candidates",
episode_ids=[episodes[0]["id"]],
episode_ids=[episode["id"] for episode in episodes],
)
commit = client.commit_dataset(dataset_id=dataset["id"])

Expand Down
25 changes: 21 additions & 4 deletions foxglove/client/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1525,17 +1525,34 @@ def create_episodes(self, *, project_id: str, episodes: List[Dict[str, Any]]):
"""Create episodes, reusing any with identical membership and bounds.

:param project_id: Project in which to create the episodes.
:param episodes: Episode definitions. Each dictionary requires ``recordings`` and
may include ``start_time``, ``end_time``, and ``metadata``.
:param episodes: Episode definitions. Each dictionary requires exactly one of
``recordings`` or ``event_id``, and may include ``metadata``. Recording inputs may
also include ``start_time`` and ``end_time``. An ``event_id`` input snapshots the
event's current time range and the completed recordings from its device that
overlap it; later edits to the event or new imports do not change the episode.
"""
serialized = []
for episode in episodes:
for index, episode in enumerate(episodes):
event_id = episode.get("event_id")
recordings = episode.get("recordings")
start_time = episode.get("start_time")
end_time = episode.get("end_time")
if (event_id is None) == (recordings is None):
raise ValueError(
f"episodes[{index}]: provide exactly one of recordings or event_id"
)
if event_id is not None and (
start_time is not None or end_time is not None
):
raise ValueError(
f"episodes[{index}]: event_id cannot be combined with "
"start_time or end_time"
)
serialized.append(
without_nulls(
{
"recordings": episode["recordings"],
"eventId": event_id,
"recordings": recordings,
"startTime": (
start_time.astimezone().isoformat() if start_time else None
),
Expand Down
47 changes: 47 additions & 0 deletions tests/test_episodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,53 @@ def test_create_episodes_serializes_inputs():
}


@responses.activate
def test_create_episodes_serializes_event_inputs():
responses.add(
responses.POST,
api_url("/v1/episodes"),
json={"episodes": [{"id": "ep_1", "created": False}]},
)

result = Client("test").create_episodes(
project_id="prj_1",
episodes=[{"event_id": "evt_1", "metadata": {"label": "merge"}}],
)

assert result == [{"id": "ep_1", "created": False}]
assert json.loads(responses.calls[0].request.body) == {
"projectId": "prj_1",
"episodes": [{"eventId": "evt_1", "metadata": {"label": "merge"}}],
}


@pytest.mark.parametrize(
"episode,message",
[
({}, "episodes[1]: provide exactly one of recordings or event_id"),
(
{"event_id": "evt_1", "recordings": ["rec_1"]},
"episodes[1]: provide exactly one of recordings or event_id",
),
(
{"event_id": "evt_1", "start_time": NOW},
"episodes[1]: event_id cannot be combined with start_time or end_time",
),
(
{"event_id": "evt_1", "end_time": NOW},
"episodes[1]: event_id cannot be combined with start_time or end_time",
),
],
)
def test_create_episodes_rejects_incompatible_inputs(episode, message):
with pytest.raises(ValueError) as raised:
Client("test").create_episodes(
project_id="prj_1", episodes=[{"event_id": "evt_0"}, episode]
)

assert str(raised.value) == message


@responses.activate
def test_get_episodes_maps_response_and_filters():
responses.add(
Expand Down
Loading