We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[BUG] Group sample in modal missing attributes
In `0.21.6`, a group sample in the modal will be missing attribute data unless the fields are added via `add_dynamic_sample_fields()` or `add_dynamic_frame_fields()`. This is due a subtle detail in the `relay` cache, which is straightforward to resolve.

```py
import fiftyone as fo

dataset = foz.load_zoo_dataset("quickstart-groups")

for sample in dataset:
    for detection in sample.ground_truth.detections:
        detection["my_attr"] = "value"
    sample.save()

# tooltip will not contain `my_attr` on detections
session = fo.launch_app(dataset)


# will force `my_attr` to appear
dataset.add_dynamic_sample_fields()
```

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `fiftyone/server/routes/media.py`
Content:
```
1 """
2 FiftyOne Server /media route
3 
4 | Copyright 2017-2023, Voxel51, Inc.
5 | `voxel51.com <https://voxel51.com/>`_
6 |
7 """
8 import typing as t
9 
10 import aiofiles
11 from aiofiles.threadpool.binary import AsyncBufferedReader
12 from aiofiles.os import stat as aio_stat
13 from starlette.endpoints import HTTPEndpoint
14 from starlette.requests import Request
15 from starlette.responses import (
16     FileResponse,
17     Response,
18     StreamingResponse,
19     guess_type,
20 )
21 
22 
23 async def ranged(
24     file: AsyncBufferedReader,
25     start: int = 0,
26     end: int = None,
27     block_size: int = 8192,
28 ) -> t.AsyncGenerator:
29     consumed = 0
30 
31     await file.seek(start)
32 
33     while True:
34         data_length = (
35             min(block_size, end - start - consumed) if end else block_size
36         )
37 
38         if data_length <= 0:
39             break
40 
41         data = await file.read(data_length)
42 
43         if not data:
44             break
45 
46         consumed += data_length
47 
48         yield data
49 
50     if hasattr(file, "close"):
51         await file.close()
52 
53 
54 class Media(HTTPEndpoint):
55     async def get(
56         self, request: Request
57     ) -> t.Union[FileResponse, StreamingResponse]:
58         path = request.query_params["filepath"]
59 
60         response: t.Union[FileResponse, StreamingResponse]
61         if request.headers.get("range"):
62             response = await self.ranged_file_response(path, request)
63         else:
64             response = FileResponse(
65                 path,
66             )
67         response.headers["Accept-Ranges"] = "bytes"
68 
69         return response
70 
71     async def ranged_file_response(
72         self, path: str, request: Request
73     ) -> StreamingResponse:
74         file = await aiofiles.open(path, "rb")
75         file_size = (await aio_stat(path)).st_size
76         content_range = request.headers.get("range")
77         content_length = file_size
78         status_code = 200
79         headers = {}
80 
81         if content_range is not None:
82             content_range = content_range.strip().lower()
83 
84             content_ranges = content_range.split("=")[-1]
85 
86             range_start, range_end, *_ = map(
87                 str.strip, (content_ranges + "-").split("-")
88             )
89 
90             start, end = (
91                 int(range_start) if range_start else 0,
92                 int(range_end) if range_end else file_size - 1,
93             )
94             range_start = max(0, start)
95             range_end = min(file_size - 1, int(end))
96 
97             content_length = (end - start) + 1
98 
99             file_response = ranged(file, start=start, end=end + 1)
100 
101             status_code = 206
102 
103             headers["Content-Range"] = f"bytes {start}-{end}/{file_size}"
104 
105         response = StreamingResponse(
106             file_response,
107             media_type=guess_type(path)[0],
108             status_code=status_code,
109         )
110 
111         response.headers.update(
112             {
113                 "Accept-Ranges": "bytes",
114                 "Content-Length": str(content_length),
115                 **headers,
116             }
117         )
118 
119         return response
120 
121     async def head(self, request: Request) -> Response:
122         path = request.query_params["filepath"]
123         response = Response()
124         size = (await aio_stat(path)).st_size
125         response.headers.update(
126             {
127                 "Accept-Ranges": "bytes",
128                 "Content-Type": guess_type(path)[0],
129                 "Content-Length": size,
130             }
131         )
132         return response
133 
134     async def options(self, request: Request) -> Response:
135         response = Response()
136         response.headers["Accept-Ranges"] = "bytes"
137         response.headers["Allow"] = "OPTIONS, GET, HEAD"
138         return response
139
```

Path: `fiftyone/server/samples.py`
Content:
```
1 """
2 FiftyOne Server samples pagination
3 
4 | Copyright 2017-2023, Voxel51, Inc.
5 | `voxel51.com <https://voxel51.com/>`_
6 |
7 """
8 import asyncio
9 import strawberry as gql
10 import typing as t
11 
12 
13 from fiftyone.core.collections import SampleCollection
14 import fiftyone.core.media as fom
15 import fiftyone.core.odm as foo
16 from fiftyone.core.utils import run_sync_task
17 
18 from fiftyone.server.filters import SampleFilter
19 import fiftyone.server.metadata as fosm
20 from fiftyone.server.paginator import Connection, Edge, PageInfo
21 from fiftyone.server.scalars import BSON, JSON, BSONArray
22 from fiftyone.server.utils import from_dict
23 import fiftyone.server.view as fosv
24 
25 
26 @gql.type
27 class MediaURL:
28     field: str
29     url: t.Optional[str]
30 
31 
32 @gql.interface
33 class Sample:
34     id: gql.ID
35     sample: JSON
36     urls: t.List[MediaURL]
37     aspect_ratio: float
38 
39 
40 @gql.type
41 class ImageSample(Sample):
42     pass
43 
44 
45 @gql.type
46 class PointCloudSample(Sample):
47     pass
48 
49 
50 @gql.type
51 class VideoSample(Sample):
52     frame_number: int
53     frame_rate: float
54 
55 
56 SampleItem = gql.union(
57     "SampleItem", types=(ImageSample, PointCloudSample, VideoSample)
58 )
59 
60 MEDIA_TYPES = {
61     fom.IMAGE: ImageSample,
62     fom.POINT_CLOUD: PointCloudSample,
63     fom.VIDEO: VideoSample,
64 }
65 
66 
67 async def paginate_samples(
68     dataset: str,
69     stages: BSONArray,
70     filters: JSON,
71     first: int,
72     after: t.Optional[str] = None,
73     extended_stages: t.Optional[BSON] = None,
74     sample_filter: t.Optional[SampleFilter] = None,
75     pagination_data: t.Optional[bool] = False,
76 ) -> Connection[t.Union[ImageSample, VideoSample], str]:
77     run = lambda reload: fosv.get_view(
78         dataset,
79         stages=stages,
80         filters=filters,
81         pagination_data=pagination_data,
82         extended_stages=extended_stages,
83         sample_filter=sample_filter,
84         reload=reload,
85     )
86     try:
87         view = await run_sync_task(run, False)
88     except:
89         view = await run_sync_task(run, True)
90 
91     # check frame field schema explicitly, media type is not reliable for groups
92     has_frames = view.get_frame_field_schema() is not None
93 
94     # TODO: Remove this once we have a better way to handle large videos. This
95     # is a temporary fix to reduce the $lookup overhead for sample frames on
96     # full datasets.
97     full_lookup = has_frames and (filters or stages)
98     support = [1, 1] if not full_lookup else None
99     if after is None:
100         after = "-1"
101 
102     if int(after) > -1:
103         view = view.skip(int(after) + 1)
104 
105     pipeline = view._pipeline(
106         attach_frames=has_frames,
107         detach_frames=False,
108         manual_group_select=sample_filter
109         and sample_filter.group
110         and (sample_filter.group.id and not sample_filter.group.slices),
111         support=support,
112     )
113 
114     # Only return the first frame of each video sample for the grid thumbnail
115     if has_frames:
116         pipeline.append({"$addFields": {"frames": {"$slice": ["$frames", 1]}}})
117 
118     samples = await foo.aggregate(
119         foo.get_async_db_conn()[view._dataset._sample_collection_name],
120         pipeline,
121     ).to_list(first + 1)
122 
123     more = False
124     if len(samples) > first:
125         samples = samples[:first]
126         more = True
127 
128     metadata_cache = {}
129     url_cache = {}
130     nodes = await asyncio.gather(
131         *[
132             _create_sample_item(view, sample, metadata_cache, url_cache)
133             for sample in samples
134         ]
135     )
136 
137     edges = []
138     for idx, node in enumerate(nodes):
139         edges.append(
140             Edge(
141                 node=node,
142                 cursor=str(idx + int(after) + 1),
143             )
144         )
145 
146     return Connection(
147         page_info=PageInfo(
148             has_previous_page=False,
149             has_next_page=more,
150             start_cursor=edges[0].cursor if edges else None,
151             end_cursor=edges[-1].cursor if len(edges) > 1 else None,
152         ),
153         edges=edges,
154     )
155 
156 
157 async def _create_sample_item(
158     dataset: SampleCollection,
159     sample: t.Dict,
160     metadata_cache: t.Dict[str, t.Dict],
161     url_cache: t.Dict[str, str],
162 ) -> SampleItem:
163     media_type = fom.get_media_type(sample["filepath"])
164 
165     if media_type == fom.IMAGE:
166         cls = ImageSample
167     elif media_type == fom.VIDEO:
168         cls = VideoSample
169     elif media_type == fom.POINT_CLOUD:
170         cls = PointCloudSample
171     else:
172         raise ValueError(f"unknown media type '{media_type}'")
173 
174     metadata = await fosm.get_metadata(
175         dataset, sample, media_type, metadata_cache, url_cache
176     )
177 
178     if cls == VideoSample:
179         metadata = dict(**metadata, frame_number=sample.get("frame_number", 1))
180 
181     return from_dict(cls, {"id": sample["_id"], "sample": sample, **metadata})
182
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/fiftyone/server/routes/media.py b/fiftyone/server/routes/media.py
--- a/fiftyone/server/routes/media.py
+++ b/fiftyone/server/routes/media.py
@@ -5,8 +5,10 @@
 | `voxel51.com <https://voxel51.com/>`_
 |
 """
+import os
 import typing as t
 
+import anyio
 import aiofiles
 from aiofiles.threadpool.binary import AsyncBufferedReader
 from aiofiles.os import stat as aio_stat
@@ -58,6 +60,12 @@
         path = request.query_params["filepath"]
 
         response: t.Union[FileResponse, StreamingResponse]
+
+        try:
+            await anyio.to_thread.run_sync(os.stat, path)
+        except FileNotFoundError:
+            return Response(content="Not found", status_code=404)
+
         if request.headers.get("range"):
             response = await self.ranged_file_response(path, request)
         else:
diff --git a/fiftyone/server/samples.py b/fiftyone/server/samples.py
--- a/fiftyone/server/samples.py
+++ b/fiftyone/server/samples.py
@@ -129,7 +129,9 @@
     url_cache = {}
     nodes = await asyncio.gather(
         *[
-            _create_sample_item(view, sample, metadata_cache, url_cache)
+            _create_sample_item(
+                view, sample, metadata_cache, url_cache, pagination_data
+            )
             for sample in samples
         ]
     )
@@ -159,6 +161,7 @@
     sample: t.Dict,
     metadata_cache: t.Dict[str, t.Dict],
     url_cache: t.Dict[str, str],
+    pagination_data: bool,
 ) -> SampleItem:
     media_type = fom.get_media_type(sample["filepath"])
 
@@ -178,4 +181,9 @@
     if cls == VideoSample:
         metadata = dict(**metadata, frame_number=sample.get("frame_number", 1))
 
-    return from_dict(cls, {"id": sample["_id"], "sample": sample, **metadata})
+    _id = sample["_id"]
+
+    if not pagination_data:
+        _id = f"{_id}-modal"
+
+    return from_dict(cls, {"id": _id, "sample": sample, **metadata})

{"golden_diff": "diff --git a/fiftyone/server/routes/media.py b/fiftyone/server/routes/media.py\n--- a/fiftyone/server/routes/media.py\n+++ b/fiftyone/server/routes/media.py\n@@ -5,8 +5,10 @@\n | `voxel51.com <https://voxel51.com/>`_\n |\n \"\"\"\n+import os\n import typing as t\n \n+import anyio\n import aiofiles\n from aiofiles.threadpool.binary import AsyncBufferedReader\n from aiofiles.os import stat as aio_stat\n@@ -58,6 +60,12 @@\n         path = request.query_params[\"filepath\"]\n \n         response: t.Union[FileResponse, StreamingResponse]\n+\n+        try:\n+            await anyio.to_thread.run_sync(os.stat, path)\n+        except FileNotFoundError:\n+            return Response(content=\"Not found\", status_code=404)\n+\n         if request.headers.get(\"range\"):\n             response = await self.ranged_file_response(path, request)\n         else:\ndiff --git a/fiftyone/server/samples.py b/fiftyone/server/samples.py\n--- a/fiftyone/server/samples.py\n+++ b/fiftyone/server/samples.py\n@@ -129,7 +129,9 @@\n     url_cache = {}\n     nodes = await asyncio.gather(\n         *[\n-            _create_sample_item(view, sample, metadata_cache, url_cache)\n+            _create_sample_item(\n+                view, sample, metadata_cache, url_cache, pagination_data\n+            )\n             for sample in samples\n         ]\n     )\n@@ -159,6 +161,7 @@\n     sample: t.Dict,\n     metadata_cache: t.Dict[str, t.Dict],\n     url_cache: t.Dict[str, str],\n+    pagination_data: bool,\n ) -> SampleItem:\n     media_type = fom.get_media_type(sample[\"filepath\"])\n \n@@ -178,4 +181,9 @@\n     if cls == VideoSample:\n         metadata = dict(**metadata, frame_number=sample.get(\"frame_number\", 1))\n \n-    return from_dict(cls, {\"id\": sample[\"_id\"], \"sample\": sample, **metadata})\n+    _id = sample[\"_id\"]\n+\n+    if not pagination_data:\n+        _id = f\"{_id}-modal\"\n+\n+    return from_dict(cls, {\"id\": _id, \"sample\": sample, **metadata})\n", "issue": "[BUG] Group sample in modal missing attributes\nIn `0.21.6`, a group sample in the modal will be missing attribute data unless the fields are added via `add_dynamic_sample_fields()` or `add_dynamic_frame_fields()`. This is due a subtle detail in the `relay` cache, which is straightforward to resolve.\r\n\r\n```py\r\nimport fiftyone as fo\r\n\r\ndataset = foz.load_zoo_dataset(\"quickstart-groups\")\r\n\r\nfor sample in dataset:\r\n    for detection in sample.ground_truth.detections:\r\n        detection[\"my_attr\"] = \"value\"\r\n    sample.save()\r\n\r\n# tooltip will not contain `my_attr` on detections\r\nsession = fo.launch_app(dataset)\r\n\r\n\r\n# will force `my_attr` to appear\r\ndataset.add_dynamic_sample_fields()\r\n```\n", "before_files": [{"content": "\"\"\"\nFiftyOne Server /media route\n\n| Copyright 2017-2023, Voxel51, Inc.\n| `voxel51.com <https://voxel51.com/>`_\n|\n\"\"\"\nimport typing as t\n\nimport aiofiles\nfrom aiofiles.threadpool.binary import AsyncBufferedReader\nfrom aiofiles.os import stat as aio_stat\nfrom starlette.endpoints import HTTPEndpoint\nfrom starlette.requests import Request\nfrom starlette.responses import (\n    FileResponse,\n    Response,\n    StreamingResponse,\n    guess_type,\n)\n\n\nasync def ranged(\n    file: AsyncBufferedReader,\n    start: int = 0,\n    end: int = None,\n    block_size: int = 8192,\n) -> t.AsyncGenerator:\n    consumed = 0\n\n    await file.seek(start)\n\n    while True:\n        data_length = (\n            min(block_size, end - start - consumed) if end else block_size\n        )\n\n        if data_length <= 0:\n            break\n\n        data = await file.read(data_length)\n\n        if not data:\n            break\n\n        consumed += data_length\n\n        yield data\n\n    if hasattr(file, \"close\"):\n        await file.close()\n\n\nclass Media(HTTPEndpoint):\n    async def get(\n        self, request: Request\n    ) -> t.Union[FileResponse, StreamingResponse]:\n        path = request.query_params[\"filepath\"]\n\n        response: t.Union[FileResponse, StreamingResponse]\n        if request.headers.get(\"range\"):\n            response = await self.ranged_file_response(path, request)\n        else:\n            response = FileResponse(\n                path,\n            )\n        response.headers[\"Accept-Ranges\"] = \"bytes\"\n\n        return response\n\n    async def ranged_file_response(\n        self, path: str, request: Request\n    ) -> StreamingResponse:\n        file = await aiofiles.open(path, \"rb\")\n        file_size = (await aio_stat(path)).st_size\n        content_range = request.headers.get(\"range\")\n        content_length = file_size\n        status_code = 200\n        headers = {}\n\n        if content_range is not None:\n            content_range = content_range.strip().lower()\n\n            content_ranges = content_range.split(\"=\")[-1]\n\n            range_start, range_end, *_ = map(\n                str.strip, (content_ranges + \"-\").split(\"-\")\n            )\n\n            start, end = (\n                int(range_start) if range_start else 0,\n                int(range_end) if range_end else file_size - 1,\n            )\n            range_start = max(0, start)\n            range_end = min(file_size - 1, int(end))\n\n            content_length = (end - start) + 1\n\n            file_response = ranged(file, start=start, end=end + 1)\n\n            status_code = 206\n\n            headers[\"Content-Range\"] = f\"bytes {start}-{end}/{file_size}\"\n\n        response = StreamingResponse(\n            file_response,\n            media_type=guess_type(path)[0],\n            status_code=status_code,\n        )\n\n        response.headers.update(\n            {\n                \"Accept-Ranges\": \"bytes\",\n                \"Content-Length\": str(content_length),\n                **headers,\n            }\n        )\n\n        return response\n\n    async def head(self, request: Request) -> Response:\n        path = request.query_params[\"filepath\"]\n        response = Response()\n        size = (await aio_stat(path)).st_size\n        response.headers.update(\n            {\n                \"Accept-Ranges\": \"bytes\",\n                \"Content-Type\": guess_type(path)[0],\n                \"Content-Length\": size,\n            }\n        )\n        return response\n\n    async def options(self, request: Request) -> Response:\n        response = Response()\n        response.headers[\"Accept-Ranges\"] = \"bytes\"\n        response.headers[\"Allow\"] = \"OPTIONS, GET, HEAD\"\n        return response\n", "path": "fiftyone/server/routes/media.py"}, {"content": "\"\"\"\nFiftyOne Server samples pagination\n\n| Copyright 2017-2023, Voxel51, Inc.\n| `voxel51.com <https://voxel51.com/>`_\n|\n\"\"\"\nimport asyncio\nimport strawberry as gql\nimport typing as t\n\n\nfrom fiftyone.core.collections import SampleCollection\nimport fiftyone.core.media as fom\nimport fiftyone.core.odm as foo\nfrom fiftyone.core.utils import run_sync_task\n\nfrom fiftyone.server.filters import SampleFilter\nimport fiftyone.server.metadata as fosm\nfrom fiftyone.server.paginator import Connection, Edge, PageInfo\nfrom fiftyone.server.scalars import BSON, JSON, BSONArray\nfrom fiftyone.server.utils import from_dict\nimport fiftyone.server.view as fosv\n\n\n@gql.type\nclass MediaURL:\n    field: str\n    url: t.Optional[str]\n\n\n@gql.interface\nclass Sample:\n    id: gql.ID\n    sample: JSON\n    urls: t.List[MediaURL]\n    aspect_ratio: float\n\n\n@gql.type\nclass ImageSample(Sample):\n    pass\n\n\n@gql.type\nclass PointCloudSample(Sample):\n    pass\n\n\n@gql.type\nclass VideoSample(Sample):\n    frame_number: int\n    frame_rate: float\n\n\nSampleItem = gql.union(\n    \"SampleItem\", types=(ImageSample, PointCloudSample, VideoSample)\n)\n\nMEDIA_TYPES = {\n    fom.IMAGE: ImageSample,\n    fom.POINT_CLOUD: PointCloudSample,\n    fom.VIDEO: VideoSample,\n}\n\n\nasync def paginate_samples(\n    dataset: str,\n    stages: BSONArray,\n    filters: JSON,\n    first: int,\n    after: t.Optional[str] = None,\n    extended_stages: t.Optional[BSON] = None,\n    sample_filter: t.Optional[SampleFilter] = None,\n    pagination_data: t.Optional[bool] = False,\n) -> Connection[t.Union[ImageSample, VideoSample], str]:\n    run = lambda reload: fosv.get_view(\n        dataset,\n        stages=stages,\n        filters=filters,\n        pagination_data=pagination_data,\n        extended_stages=extended_stages,\n        sample_filter=sample_filter,\n        reload=reload,\n    )\n    try:\n        view = await run_sync_task(run, False)\n    except:\n        view = await run_sync_task(run, True)\n\n    # check frame field schema explicitly, media type is not reliable for groups\n    has_frames = view.get_frame_field_schema() is not None\n\n    # TODO: Remove this once we have a better way to handle large videos. This\n    # is a temporary fix to reduce the $lookup overhead for sample frames on\n    # full datasets.\n    full_lookup = has_frames and (filters or stages)\n    support = [1, 1] if not full_lookup else None\n    if after is None:\n        after = \"-1\"\n\n    if int(after) > -1:\n        view = view.skip(int(after) + 1)\n\n    pipeline = view._pipeline(\n        attach_frames=has_frames,\n        detach_frames=False,\n        manual_group_select=sample_filter\n        and sample_filter.group\n        and (sample_filter.group.id and not sample_filter.group.slices),\n        support=support,\n    )\n\n    # Only return the first frame of each video sample for the grid thumbnail\n    if has_frames:\n        pipeline.append({\"$addFields\": {\"frames\": {\"$slice\": [\"$frames\", 1]}}})\n\n    samples = await foo.aggregate(\n        foo.get_async_db_conn()[view._dataset._sample_collection_name],\n        pipeline,\n    ).to_list(first + 1)\n\n    more = False\n    if len(samples) > first:\n        samples = samples[:first]\n        more = True\n\n    metadata_cache = {}\n    url_cache = {}\n    nodes = await asyncio.gather(\n        *[\n            _create_sample_item(view, sample, metadata_cache, url_cache)\n            for sample in samples\n        ]\n    )\n\n    edges = []\n    for idx, node in enumerate(nodes):\n        edges.append(\n            Edge(\n                node=node,\n                cursor=str(idx + int(after) + 1),\n            )\n        )\n\n    return Connection(\n        page_info=PageInfo(\n            has_previous_page=False,\n            has_next_page=more,\n            start_cursor=edges[0].cursor if edges else None,\n            end_cursor=edges[-1].cursor if len(edges) > 1 else None,\n        ),\n        edges=edges,\n    )\n\n\nasync def _create_sample_item(\n    dataset: SampleCollection,\n    sample: t.Dict,\n    metadata_cache: t.Dict[str, t.Dict],\n    url_cache: t.Dict[str, str],\n) -> SampleItem:\n    media_type = fom.get_media_type(sample[\"filepath\"])\n\n    if media_type == fom.IMAGE:\n        cls = ImageSample\n    elif media_type == fom.VIDEO:\n        cls = VideoSample\n    elif media_type == fom.POINT_CLOUD:\n        cls = PointCloudSample\n    else:\n        raise ValueError(f\"unknown media type '{media_type}'\")\n\n    metadata = await fosm.get_metadata(\n        dataset, sample, media_type, metadata_cache, url_cache\n    )\n\n    if cls == VideoSample:\n        metadata = dict(**metadata, frame_number=sample.get(\"frame_number\", 1))\n\n    return from_dict(cls, {\"id\": sample[\"_id\"], \"sample\": sample, **metadata})\n", "path": "fiftyone/server/samples.py"}], "after_files": [{"content": "\"\"\"\nFiftyOne Server /media route\n\n| Copyright 2017-2023, Voxel51, Inc.\n| `voxel51.com <https://voxel51.com/>`_\n|\n\"\"\"\nimport os\nimport typing as t\n\nimport anyio\nimport aiofiles\nfrom aiofiles.threadpool.binary import AsyncBufferedReader\nfrom aiofiles.os import stat as aio_stat\nfrom starlette.endpoints import HTTPEndpoint\nfrom starlette.requests import Request\nfrom starlette.responses import (\n    FileResponse,\n    Response,\n    StreamingResponse,\n    guess_type,\n)\n\n\nasync def ranged(\n    file: AsyncBufferedReader,\n    start: int = 0,\n    end: int = None,\n    block_size: int = 8192,\n) -> t.AsyncGenerator:\n    consumed = 0\n\n    await file.seek(start)\n\n    while True:\n        data_length = (\n            min(block_size, end - start - consumed) if end else block_size\n        )\n\n        if data_length <= 0:\n            break\n\n        data = await file.read(data_length)\n\n        if not data:\n            break\n\n        consumed += data_length\n\n        yield data\n\n    if hasattr(file, \"close\"):\n        await file.close()\n\n\nclass Media(HTTPEndpoint):\n    async def get(\n        self, request: Request\n    ) -> t.Union[FileResponse, StreamingResponse]:\n        path = request.query_params[\"filepath\"]\n\n        response: t.Union[FileResponse, StreamingResponse]\n\n        try:\n            await anyio.to_thread.run_sync(os.stat, path)\n        except FileNotFoundError:\n            return Response(content=\"Not found\", status_code=404)\n\n        if request.headers.get(\"range\"):\n            response = await self.ranged_file_response(path, request)\n        else:\n            response = FileResponse(\n                path,\n            )\n        response.headers[\"Accept-Ranges\"] = \"bytes\"\n\n        return response\n\n    async def ranged_file_response(\n        self, path: str, request: Request\n    ) -> StreamingResponse:\n        file = await aiofiles.open(path, \"rb\")\n        file_size = (await aio_stat(path)).st_size\n        content_range = request.headers.get(\"range\")\n        content_length = file_size\n        status_code = 200\n        headers = {}\n\n        if content_range is not None:\n            content_range = content_range.strip().lower()\n\n            content_ranges = content_range.split(\"=\")[-1]\n\n            range_start, range_end, *_ = map(\n                str.strip, (content_ranges + \"-\").split(\"-\")\n            )\n\n            start, end = (\n                int(range_start) if range_start else 0,\n                int(range_end) if range_end else file_size - 1,\n            )\n            range_start = max(0, start)\n            range_end = min(file_size - 1, int(end))\n\n            content_length = (end - start) + 1\n\n            file_response = ranged(file, start=start, end=end + 1)\n\n            status_code = 206\n\n            headers[\"Content-Range\"] = f\"bytes {start}-{end}/{file_size}\"\n\n        response = StreamingResponse(\n            file_response,\n            media_type=guess_type(path)[0],\n            status_code=status_code,\n        )\n\n        response.headers.update(\n            {\n                \"Accept-Ranges\": \"bytes\",\n                \"Content-Length\": str(content_length),\n                **headers,\n            }\n        )\n\n        return response\n\n    async def head(self, request: Request) -> Response:\n        path = request.query_params[\"filepath\"]\n        response = Response()\n        size = (await aio_stat(path)).st_size\n        response.headers.update(\n            {\n                \"Accept-Ranges\": \"bytes\",\n                \"Content-Type\": guess_type(path)[0],\n                \"Content-Length\": size,\n            }\n        )\n        return response\n\n    async def options(self, request: Request) -> Response:\n        response = Response()\n        response.headers[\"Accept-Ranges\"] = \"bytes\"\n        response.headers[\"Allow\"] = \"OPTIONS, GET, HEAD\"\n        return response\n", "path": "fiftyone/server/routes/media.py"}, {"content": "\"\"\"\nFiftyOne Server samples pagination\n\n| Copyright 2017-2023, Voxel51, Inc.\n| `voxel51.com <https://voxel51.com/>`_\n|\n\"\"\"\nimport asyncio\nimport strawberry as gql\nimport typing as t\n\n\nfrom fiftyone.core.collections import SampleCollection\nimport fiftyone.core.media as fom\nimport fiftyone.core.odm as foo\nfrom fiftyone.core.utils import run_sync_task\n\nfrom fiftyone.server.filters import SampleFilter\nimport fiftyone.server.metadata as fosm\nfrom fiftyone.server.paginator import Connection, Edge, PageInfo\nfrom fiftyone.server.scalars import BSON, JSON, BSONArray\nfrom fiftyone.server.utils import from_dict\nimport fiftyone.server.view as fosv\n\n\n@gql.type\nclass MediaURL:\n    field: str\n    url: t.Optional[str]\n\n\n@gql.interface\nclass Sample:\n    id: gql.ID\n    sample: JSON\n    urls: t.List[MediaURL]\n    aspect_ratio: float\n\n\n@gql.type\nclass ImageSample(Sample):\n    pass\n\n\n@gql.type\nclass PointCloudSample(Sample):\n    pass\n\n\n@gql.type\nclass VideoSample(Sample):\n    frame_number: int\n    frame_rate: float\n\n\nSampleItem = gql.union(\n    \"SampleItem\", types=(ImageSample, PointCloudSample, VideoSample)\n)\n\nMEDIA_TYPES = {\n    fom.IMAGE: ImageSample,\n    fom.POINT_CLOUD: PointCloudSample,\n    fom.VIDEO: VideoSample,\n}\n\n\nasync def paginate_samples(\n    dataset: str,\n    stages: BSONArray,\n    filters: JSON,\n    first: int,\n    after: t.Optional[str] = None,\n    extended_stages: t.Optional[BSON] = None,\n    sample_filter: t.Optional[SampleFilter] = None,\n    pagination_data: t.Optional[bool] = False,\n) -> Connection[t.Union[ImageSample, VideoSample], str]:\n    run = lambda reload: fosv.get_view(\n        dataset,\n        stages=stages,\n        filters=filters,\n        pagination_data=pagination_data,\n        extended_stages=extended_stages,\n        sample_filter=sample_filter,\n        reload=reload,\n    )\n    try:\n        view = await run_sync_task(run, False)\n    except:\n        view = await run_sync_task(run, True)\n\n    # check frame field schema explicitly, media type is not reliable for groups\n    has_frames = view.get_frame_field_schema() is not None\n\n    # TODO: Remove this once we have a better way to handle large videos. This\n    # is a temporary fix to reduce the $lookup overhead for sample frames on\n    # full datasets.\n    full_lookup = has_frames and (filters or stages)\n    support = [1, 1] if not full_lookup else None\n    if after is None:\n        after = \"-1\"\n\n    if int(after) > -1:\n        view = view.skip(int(after) + 1)\n\n    pipeline = view._pipeline(\n        attach_frames=has_frames,\n        detach_frames=False,\n        manual_group_select=sample_filter\n        and sample_filter.group\n        and (sample_filter.group.id and not sample_filter.group.slices),\n        support=support,\n    )\n\n    # Only return the first frame of each video sample for the grid thumbnail\n    if has_frames:\n        pipeline.append({\"$addFields\": {\"frames\": {\"$slice\": [\"$frames\", 1]}}})\n\n    samples = await foo.aggregate(\n        foo.get_async_db_conn()[view._dataset._sample_collection_name],\n        pipeline,\n    ).to_list(first + 1)\n\n    more = False\n    if len(samples) > first:\n        samples = samples[:first]\n        more = True\n\n    metadata_cache = {}\n    url_cache = {}\n    nodes = await asyncio.gather(\n        *[\n            _create_sample_item(\n                view, sample, metadata_cache, url_cache, pagination_data\n            )\n            for sample in samples\n        ]\n    )\n\n    edges = []\n    for idx, node in enumerate(nodes):\n        edges.append(\n            Edge(\n                node=node,\n                cursor=str(idx + int(after) + 1),\n            )\n        )\n\n    return Connection(\n        page_info=PageInfo(\n            has_previous_page=False,\n            has_next_page=more,\n            start_cursor=edges[0].cursor if edges else None,\n            end_cursor=edges[-1].cursor if len(edges) > 1 else None,\n        ),\n        edges=edges,\n    )\n\n\nasync def _create_sample_item(\n    dataset: SampleCollection,\n    sample: t.Dict,\n    metadata_cache: t.Dict[str, t.Dict],\n    url_cache: t.Dict[str, str],\n    pagination_data: bool,\n) -> SampleItem:\n    media_type = fom.get_media_type(sample[\"filepath\"])\n\n    if media_type == fom.IMAGE:\n        cls = ImageSample\n    elif media_type == fom.VIDEO:\n        cls = VideoSample\n    elif media_type == fom.POINT_CLOUD:\n        cls = PointCloudSample\n    else:\n        raise ValueError(f\"unknown media type '{media_type}'\")\n\n    metadata = await fosm.get_metadata(\n        dataset, sample, media_type, metadata_cache, url_cache\n    )\n\n    if cls == VideoSample:\n        metadata = dict(**metadata, frame_number=sample.get(\"frame_number\", 1))\n\n    _id = sample[\"_id\"]\n\n    if not pagination_data:\n        _id = f\"{_id}-modal\"\n\n    return from_dict(cls, {\"id\": _id, \"sample\": sample, **metadata})\n", "path": "fiftyone/server/samples.py"}]}