diff --git a/demos/python/sdk_wireless_camera_control/docs/changelog.rst b/demos/python/sdk_wireless_camera_control/docs/changelog.rst index d34046d0..283e4358 100644 --- a/demos/python/sdk_wireless_camera_control/docs/changelog.rst +++ b/demos/python/sdk_wireless_camera_control/docs/changelog.rst @@ -12,6 +12,7 @@ and this project adheres to `Semantic Versioning GoProResp: - """Execute the command by sending it via HTTP + def build_url(self, **kwargs: Any) -> str: + """Build the URL string from the passed in components and arguments Args: - __communicator__ (GoProHttp): HTTP communicator - rules (Optional[dict[MessageRules, RuleSignature]], optional): rules to apply when executing this - message. Defaults to None. - **kwargs (Any): arguments to message + **kwargs (Any): additional entries for the dict Returns: - GoProResp: Response received via HTTP + str: built URL """ - # Append components url = self._endpoint for component in self._components or []: url += "/" + kwargs.pop(component) @@ -835,7 +825,30 @@ async def __call__( ) ): url += "?" + arg_part + return url + + +class HttpGetJsonCommand(HttpCommand): + """An HTTP command that performs a GET operation and receives JSON as response""" + async def __call__( + self, + __communicator__: GoProHttp, + rules: list[MessageRules] | None = None, + **kwargs: Any, + ) -> GoProResp: + """Execute the command by sending it via HTTP + + Args: + __communicator__ (GoProHttp): HTTP communicator + rules (Optional[dict[MessageRules, RuleSignature]], optional): rules to apply when executing this + message. Defaults to None. + **kwargs (Any): arguments to message + + Returns: + GoProResp: Response received via HTTP + """ + url = self.build_url(**kwargs) # Send to camera logger.info(Logger.build_log_tx_str(pretty_print(self._as_dict(**kwargs, endpoint=url)))) response = await __communicator__._http_get(url, self._parser, rules=rules) @@ -849,7 +862,11 @@ class HttpGetBinary(HttpCommand): """An HTTP command that performs a GET operation and receives a binary stream as response""" async def __call__( # type: ignore - self, __communicator__: GoProHttp, *, camera_file: str, local_file: Path | None = None + self, + __communicator__: GoProHttp, + *, + camera_file: str, + local_file: Path | None = None, ) -> GoProResp: """Execute the command by getting the binary data from the communicator @@ -863,8 +880,8 @@ async def __call__( # type: ignore GoProResp: location on local device that file was written to """ # The method that will actually send the command and receive the stream - local_file = local_file or Path(".") / camera_file - url = self._endpoint + "/" + camera_file + local_file = local_file or Path(".") / Path(camera_file).name + url = self.build_url(path=camera_file) logger.info( Logger.build_log_tx_str( pretty_print(self._as_dict(endpoint=url, camera_file=camera_file, local_file=local_file)) diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/api/http_commands.py b/demos/python/sdk_wireless_camera_control/open_gopro/api/http_commands.py index 61b4ea6c..7ebb2b4f 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/api/http_commands.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/api/http_commands.py @@ -100,7 +100,6 @@ async def get_media_metadata(self, *, file: str) -> GoProResp[MediaMetadata]: Returns: GoProResp: Media metadata JSON structure """ - return {"path": f"100GOPRO/{file}"} # type: ignore @http_get_json_command( endpoint="gopro/media/list", @@ -249,7 +248,6 @@ async def set_date_time( "dst": int(is_dst), } - # TODO @http_get_json_command(endpoint="gopro/camera/get_date_time") async def get_date_time(self) -> GoProResp[datetime.datetime]: """Get the date and time of the camera (Non timezone / DST aware) @@ -285,7 +283,7 @@ async def add_file_hilight( Returns: GoProResp: command status """ - return {"path": f"100GOPRO/{file}", "ms": offset or None} # type: ignore + return {"path": file, "ms": offset or None} # type: ignore @http_get_json_command( endpoint="gopro/media/hilight/remove", @@ -306,7 +304,7 @@ async def remove_file_hilight( Returns: GoProResp: command status """ - return {"path": f"100GOPRO/{file}", "ms": offset} # type: ignore + return {"path": file, "ms": offset} # type: ignore @http_get_json_command( endpoint="gopro/webcam/exit", @@ -396,7 +394,7 @@ async def wired_usb_control(self, *, control: Params.Toggle) -> GoProResp[None]: # HTTP GET BINARY COMMANDS ###################################################################################################### - @http_get_binary_command(endpoint="gopro/media/gpmf?path=100GOPRO") + @http_get_binary_command(endpoint="gopro/media/gpmf", arguments=["path"]) async def get_gpmf_data(self, *, camera_file: str, local_file: Path | None = None) -> GoProResp[Path]: """Get GPMF data for a file. @@ -410,7 +408,7 @@ async def get_gpmf_data(self, *, camera_file: str, local_file: Path | None = Non Path: Path to local_file that output was written to """ - @http_get_binary_command(endpoint="gopro/media/screennail?path=100GOPRO") + @http_get_binary_command(endpoint="gopro/media/screennail", arguments=["path"]) async def get_screennail__call__(self, *, camera_file: str, local_file: Path | None = None) -> GoProResp[Path]: """Get screennail for a file. @@ -424,7 +422,7 @@ async def get_screennail__call__(self, *, camera_file: str, local_file: Path | N Path: Path to local_file that output was written to """ - @http_get_binary_command(endpoint="gopro/media/thumbnail?path=100GOPRO") + @http_get_binary_command(endpoint="gopro/media/thumbnail", arguments=["path"]) async def get_thumbnail(self, *, camera_file: str, local_file: Path | None = None) -> GoProResp[Path]: """Get thumbnail for a file. @@ -438,7 +436,7 @@ async def get_thumbnail(self, *, camera_file: str, local_file: Path | None = Non Path: Path to local_file that output was written to """ - @http_get_binary_command(endpoint="gopro/media/telemetry?path=100GOPRO") + @http_get_binary_command(endpoint="gopro/media/telemetry", arguments=["path"]) async def get_telemetry(self, *, camera_file: str, local_file: Path | None = None) -> GoProResp[Path]: """Download the telemetry data for a camera file and store in a local file. @@ -452,7 +450,7 @@ async def get_telemetry(self, *, camera_file: str, local_file: Path | None = Non Path: Path to local_file that output was written to """ - @http_get_binary_command(endpoint="videos/DCIM/100GOPRO", identifier="Download File") + @http_get_binary_command(endpoint="videos/DCIM", components=["path"], identifier="Download File") async def download_file(self, *, camera_file: str, local_file: Path | None = None) -> GoProResp[Path]: """Download a video from the camera to a local file. diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/demos/photo.py b/demos/python/sdk_wireless_camera_control/open_gopro/demos/photo.py index 634ec962..98bc4c84 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/demos/photo.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/demos/photo.py @@ -46,7 +46,7 @@ async def main(args: argparse.Namespace) -> None: # The photo is (most likely) the difference between the two sets photo = media_set_after.difference(media_set_before).pop() # Download the photo - console.print("Downloading the photo...") + console.print(f"Downloading {photo.filename}...") await gopro.http_command.download_file(camera_file=photo.filename, local_file=args.output) console.print(f"Success!! :smiley: File has been downloaded to {args.output}") except Exception as e: # pylint: disable = broad-except diff --git a/demos/python/sdk_wireless_camera_control/open_gopro/models/media_list.py b/demos/python/sdk_wireless_camera_control/open_gopro/models/media_list.py index 00f4fd46..7c8bfb88 100644 --- a/demos/python/sdk_wireless_camera_control/open_gopro/models/media_list.py +++ b/demos/python/sdk_wireless_camera_control/open_gopro/models/media_list.py @@ -6,9 +6,9 @@ from __future__ import annotations from abc import ABC -from typing import Optional +from typing import Any, Optional -from pydantic import Field, validator +from pydantic import Field, PrivateAttr, validator from open_gopro import types from open_gopro.models.bases import CustomBaseModel @@ -139,6 +139,16 @@ class MediaList(CustomBaseModel): identifier: str = Field(alias="id") #: String identifier of this media list media: list[MediaFileSystem] #: Media filesystem(s) + _files: list[MediaItem] = PrivateAttr(default_factory=list) + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + # self.thing = 1 + # Modify each file name to use full path + for directory in self.media: + for media in directory.file_system: + media.filename = f"{directory.directory}/{media.filename}" + self._files.append(media) @property def files(self) -> list[MediaItem]: @@ -147,4 +157,4 @@ def files(self) -> list[MediaItem]: Returns: list[MediaItem]: all media items in this media list """ - return [item for media in self.media for item in media.file_system] + return self._files diff --git a/demos/python/sdk_wireless_camera_control/pyproject.toml b/demos/python/sdk_wireless_camera_control/pyproject.toml index dfc1b184..4b4e578e 100644 --- a/demos/python/sdk_wireless_camera_control/pyproject.toml +++ b/demos/python/sdk_wireless_camera_control/pyproject.toml @@ -183,7 +183,7 @@ log_file_level = "DEBUG" log_file_format = "%(threadName)13s: %(name)40s:%(lineno)5d %(asctime)s.%(msecs)03d %(levelname)-8s | %(message)s" log_file_date_format = "%H:%M:%S" filterwarnings = "ignore::DeprecationWarning" -timeout = 10 +# timeout = 10 addopts = [ "-s", "--capture=tee-sys", diff --git a/demos/python/sdk_wireless_camera_control/tests/unit/test_http_commands.py b/demos/python/sdk_wireless_camera_control/tests/unit/test_http_commands.py index f9a8eb84..35bc0b3f 100644 --- a/demos/python/sdk_wireless_camera_control/tests/unit/test_http_commands.py +++ b/demos/python/sdk_wireless_camera_control/tests/unit/test_http_commands.py @@ -7,39 +7,46 @@ import pytest -from open_gopro.communicator_interface import GoProWifi +from open_gopro.gopro_base import GoProBase + +camera_file = "100GOPRO/XXX.mp4" @pytest.mark.asyncio -async def test_get_with_no_params(mock_wifi_communicator: GoProWifi): +async def test_get_with_no_params(mock_wifi_communicator: GoProBase): response = await mock_wifi_communicator.http_command.get_media_list() assert response.url == "gopro/media/list" @pytest.mark.asyncio -async def test_get_with_params(mock_wifi_communicator: GoProWifi): +async def test_get_with_params(mock_wifi_communicator: GoProBase): zoom = 99 response = await mock_wifi_communicator.http_command.set_digital_zoom(percent=zoom) assert response.url == f"gopro/camera/digital_zoom?percent={zoom}" @pytest.mark.asyncio -async def test_with_multiple_params(mock_wifi_communicator: GoProWifi): - media_file = "XXX.mp4" - offset_ms = 2500 - response = await mock_wifi_communicator.http_command.add_file_hilight(file=media_file, offset=offset_ms) - assert response.url == "gopro/media/hilight/file?path=100GOPRO/XXX.mp4&ms=2500" +async def test_get_binary(mock_wifi_communicator: GoProBase): + url, file = await mock_wifi_communicator.http_command.get_gpmf_data(camera_file=camera_file) + assert url == f"gopro/media/gpmf?path={camera_file}" + assert file == Path("XXX.mp4") + + +@pytest.mark.asyncio +async def test_get_binary_with_component(mock_wifi_communicator: GoProBase): + url, file = await mock_wifi_communicator.http_command.download_file(camera_file=camera_file) + assert url == f"videos/DCIM/{camera_file}" + assert file == Path("XXX.mp4") @pytest.mark.asyncio -async def test_get_binary(mock_wifi_communicator: GoProWifi): - file = await mock_wifi_communicator.http_command.download_file( - camera_file="test_file", local_file=Path("local_file") - ) - assert str(file[1]) == "local_file" +async def test_with_multiple_params(mock_wifi_communicator: GoProBase): + offset_ms = 2500 + response = await mock_wifi_communicator.http_command.add_file_hilight(file=camera_file, offset=offset_ms) + assert response.url == f"gopro/media/hilight/file?path={camera_file}&ms=2500" -def test_ensure_no_positional_args(mock_wifi_communicator: GoProWifi): +def test_ensure_no_positional_args(mock_wifi_communicator: GoProBase): for command in mock_wifi_communicator.http_command.values(): if inspect.getfullargspec(command).args != ["self"]: logging.error("All arguments to commands must be keyword-only") diff --git a/demos/python/sdk_wireless_camera_control/tests/unit/test_models.py b/demos/python/sdk_wireless_camera_control/tests/unit/test_models.py index 3e1942bf..4dd2867d 100644 --- a/demos/python/sdk_wireless_camera_control/tests/unit/test_models.py +++ b/demos/python/sdk_wireless_camera_control/tests/unit/test_models.py @@ -136,6 +136,7 @@ def test_media_list(): items = media_list.files assert len(items) == 12 assert len([item for item in items if isinstance(item, GroupedMediaItem)]) == 2 + assert media_list.files[0].filename == "100GOPRO/GX010001.MP4" VIDEO_METADATA: Final = {