diff --git a/pyproject.toml b/pyproject.toml index d7f15f16..12548d60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,13 @@ classifiers = [ "Typing :: Typed", ] dynamic = ["version"] -dependencies = ["requests>=2.31.0,<3"] +dependencies = [ + "click", + "requests>=2.31.0,<3", +] + +[project.scripts] +posit = "posit:main" [project.urls] Source = "https://github.com/posit-dev/posit-sdk-py" diff --git a/src/posit/__init__.py b/src/posit/__init__.py index 1791d60e..970426b5 100644 --- a/src/posit/__init__.py +++ b/src/posit/__init__.py @@ -1,3 +1,4 @@ """The Posit SDK.""" from . import connect as connect +from .cli import main as main diff --git a/src/posit/cli.py b/src/posit/cli.py new file mode 100644 index 00000000..163777d1 --- /dev/null +++ b/src/posit/cli.py @@ -0,0 +1,40 @@ +import io +import tarfile + +import click + +from . import connect + + +@click.group() +def main(): + """The Posit command line interface.""" + pass + + +@main.group(name="connect") +def _connect(): + """Work with Posit Connect.""" + pass + + +@_connect.command() +@click.argument("content") +@click.option("--output", "-o", default=".", help="Output directory for the downloaded content.") +def download(content, output): + """Download content from Connect.""" + client = connect.Client() + bundle = client.content.get(content).bundles.find_one() + + if bundle is None: + return + + buffer = io.BytesIO() + bundle.download(buffer) + buffer.seek(0) + with tarfile.open(fileobj=buffer, mode="r:gz") as tar: + tar.extractall(path=output, filter="data") + + +if __name__ == "__main__": + main() diff --git a/src/posit/connect/bundles.py b/src/posit/connect/bundles.py index 81ddc3f0..ec5823f2 100644 --- a/src/posit/connect/bundles.py +++ b/src/posit/connect/bundles.py @@ -3,7 +3,7 @@ from __future__ import annotations import io -from typing import List +from typing import BinaryIO, List from . import resources, tasks @@ -46,7 +46,7 @@ def deploy(self) -> tasks.Task: ts = tasks.Tasks(self.params) return ts.get(result["task_id"]) - def download(self, output: io.BufferedWriter | str) -> None: + def download(self, output: BinaryIO | str) -> None: """Download a bundle. Download a bundle to a file or memory. @@ -67,26 +67,19 @@ def download(self, output: io.BufferedWriter | str) -> None: >>> bundle.download("bundle.tar.gz") None - Write to an io.BufferedWriter. + Write to BinaryIO. >>> with open('bundle.tar.gz', 'wb') as file: >>> bundle.download(file) None """ - if not isinstance(output, (io.BufferedWriter, str)): - raise TypeError( - f"download() expected argument type 'io.BufferedWriter` or 'str', but got '{type(output).__name__}'" - ) + if isinstance(output, str): + output = open(output, "wb") path = f"v1/content/{self.content_guid}/bundles/{self.id}/download" url = self.params.url + path response = self.params.session.get(url, stream=True) - if isinstance(output, io.BufferedWriter): - for chunk in response.iter_content(): - output.write(chunk) - elif isinstance(output, str): - with open(output, "wb") as file: - for chunk in response.iter_content(): - file.write(chunk) + for chunk in response.iter_content(): + output.write(chunk) class Bundles(resources.Resources): diff --git a/tests/posit/connect/test_bundles.py b/tests/posit/connect/test_bundles.py index f79076bc..b11f7276 100644 --- a/tests/posit/connect/test_bundles.py +++ b/tests/posit/connect/test_bundles.py @@ -1,7 +1,6 @@ import io from unittest import mock -import pytest import responses from responses import matchers @@ -226,40 +225,6 @@ def test_output_as_io(self): assert mock_bundle_download.call_count == 1 assert file.read() == path.read_bytes() - @responses.activate - def test_invalid_arguments(self): - content_guid = "f2f37341-e21d-3d80-c698-a935ad614066" - bundle_id = "101" - path = get_path(f"v1/content/{content_guid}/bundles/{bundle_id}/download/bundle.tar.gz") - - # behavior - mock_content_get = responses.get( - f"https://connect.example/__api__/v1/content/{content_guid}", - json=load_mock(f"v1/content/{content_guid}.json"), - ) - - mock_bundle_get = responses.get( - f"https://connect.example/__api__/v1/content/{content_guid}/bundles/{bundle_id}", - json=load_mock(f"v1/content/{content_guid}/bundles/{bundle_id}.json"), - ) - - mock_bundle_download = responses.get( - f"https://connect.example/__api__/v1/content/{content_guid}/bundles/{bundle_id}/download", - body=path.read_bytes(), - ) - - # setup - c = Client("https://connect.example", "12345") - bundle = c.content.get(content_guid).bundles.get(bundle_id) - - # invoke - with pytest.raises(TypeError): - bundle.download(None) - - # assert - assert mock_content_get.call_count == 1 - assert mock_bundle_get.call_count == 1 - class TestBundlesCreate: @responses.activate @@ -326,24 +291,6 @@ def test_kwargs_pathname(self): assert mock_content_get.call_count == 1 assert mock_bundle_post.call_count == 1 - @responses.activate - def test_invalid_arguments(self): - content_guid = "f2f37341-e21d-3d80-c698-a935ad614066" - - # behavior - responses.get( - f"https://connect.example/__api__/v1/content/{content_guid}", - json=load_mock(f"v1/content/{content_guid}.json"), - ) - - # setup - c = Client("https://connect.example", "12345") - content = c.content.get(content_guid) - - # invoke - with pytest.raises(TypeError): - content.bundles.create(None) - class TestBundlesFind: @responses.activate