Skip to content

Commit

Permalink
Add methods to add packages and upload container (#48)
Browse files Browse the repository at this point in the history
* Add add_package and upload_container methods

* Bump version 1.3.2 → 1.4.0
  • Loading branch information
tr4nt0r authored Jul 11, 2024
1 parent 7e7d27e commit cf96816
Show file tree
Hide file tree
Showing 7 changed files with 575 additions and 11 deletions.
2 changes: 1 addition & 1 deletion src/pyloadapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""PyLoadAPI package."""

__version__ = "1.3.2"
__version__ = "1.4.0"

from .api import PyLoadAPI
from .exceptions import CannotConnect, InvalidAuth, ParserError
Expand Down
192 changes: 191 additions & 1 deletion src/pyloadapi/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"""

from http import HTTPStatus
import json
from json import JSONDecodeError
import logging
import traceback
Expand All @@ -15,7 +16,7 @@
import aiohttp

from .exceptions import CannotConnect, InvalidAuth, ParserError
from .types import LoginResponse, PyLoadCommand, StatusServerResponse
from .types import Destination, LoginResponse, PyLoadCommand, StatusServerResponse

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -167,6 +168,91 @@ async def get(
"Executing command {command} failed due to request exception"
) from e

async def post(
self,
command: PyLoadCommand,
data: dict[str, Any],
) -> Any:
"""Execute a pyLoad API command using a POST request.
Parameters
----------
command : PyLoadCommand
The pyLoad command to execute.
data : dict[str, Any]
Data to include in the request body. The values in the dictionary
will be JSON encoded.
Returns
-------
Any
The response data from the API.
Raises
------
CannotConnect
If the request to the API fails due to a connection issue.
InvalidAuth
If the request fails due to invalid or expired authentication.
ParserError
If there's an error parsing the API response.
Notes
-----
This method sends an asynchronous POST request to the pyLoad API endpoint
specified by `command`, with the provided `data` dictionary. It handles
authentication errors, HTTP status checks, and parses the JSON response.
Example
-------
To add a new package to pyLoad, use:
```python
status = await pyload_api.post(PyLoadCommand.ADD_PACKAGE, data={...}
```
"""
url = f"{self.api_url}api/{command}"
data = {
k: str(v) if isinstance(v, bytes) else json.dumps(v)
for k, v in data.items()
}

try:
async with self._session.post(url, data=data) as r:
_LOGGER.debug(
"Response from %s [%s]: %s", r.url, r.status, await r.text()
)

if r.status == HTTPStatus.UNAUTHORIZED:
raise InvalidAuth(
"Request failed due invalid or expired authentication cookie."
)
r.raise_for_status()
try:
data = await r.json()
except JSONDecodeError as e:
_LOGGER.debug(
"Exception: Cannot parse response for %s:\n %s",
command,
traceback.format_exc(),
)
raise ParserError(
f"Get {command} failed during parsing of request response."
) from e

return data

except (TimeoutError, aiohttp.ClientError) as e:
_LOGGER.debug(
"Exception: Cannot execute command %s:\n %s",
command,
traceback.format_exc(),
)
raise CannotConnect(
f"Executing command {command} failed due to request exception"
) from e

async def get_status(self) -> StatusServerResponse:
"""Get general status information of pyLoad.
Expand Down Expand Up @@ -529,3 +615,107 @@ async def free_space(self) -> int:
return int(r)
except CannotConnect as e:
raise CannotConnect("Get free space failed due to request exception") from e

async def add_package(
self,
name: str,
links: list[str],
destination: Destination = Destination.COLLECTOR,
) -> int:
"""Add a new package to pyLoad from a list of links.
Parameters
----------
name : str
The name of the package to be added.
links : list[str]
A list of download links to be included in the package.
destination : Destination, optional
The destination where the package should be stored, by default Destination.COLLECTOR.
Returns
-------
int
The ID of the newly created package.
Raises
------
CannotConnect
If the request to add the package fails due to a connection issue.
InvalidAuth
If the request fails due to invalid or expired authentication.
ParserError
If there's an issue parsing the response from the server.
Example
-------
To add a new package with a couple of links to the pyLoad collector:
```python
package_id = await pyload_api.add_package(
"test_package",
[
"https://example.com/file1.zip",
"https://example.com/file2.iso",
]
)
```
"""

try:
r = await self.post(
PyLoadCommand.ADD_PACKAGE,
data={
"name": name,
"links": links,
"dest": destination,
},
)
return int(r)
except CannotConnect as e:
raise CannotConnect("Adding package failed due to request exception") from e

async def upload_container(
self,
filename: str,
binary_data: bytes,
) -> None:
"""Upload a container file to pyLoad.
Parameters
----------
filename : str
The name of the file to be uploaded.
binary_data : bytes
The binary content of the file.
Returns
-------
None
Raises
------
CannotConnect
If the request to upload the container fails due to a connection issue.
InvalidAuth
If the request fails due to invalid or expired authentication.
Example
-------
To upload a container file to pyLoad:
```python
await pyload_api.upload_container(
"example_container.dlc",
b"binary data of the file"
)
```
"""
try:
await self.post(
PyLoadCommand.UPLOAD_CONTAINER,
data={"filename": filename, "data": binary_data},
)

except CannotConnect as e:
raise CannotConnect(
"Uploading container to pyLoad failed due to request exception"
) from e
99 changes: 97 additions & 2 deletions src/pyloadapi/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
import click

from pyloadapi import CannotConnect, InvalidAuth, ParserError, PyLoadAPI
from pyloadapi.types import Destination

logging.basicConfig(level=logging.INFO)
_LOGGER = logging.getLogger(__name__)

CONFIG_FILE_PATH = Path("~/.config/pyloadapi/.pyload_config.json").expanduser()
Expand Down Expand Up @@ -46,7 +46,7 @@ async def init_api(
return api


@click.group()
@click.group(invoke_without_command=True)
@click.option("--api-url", help="Base URL of pyLoad")
@click.option("--username", help="Username for pyLoad")
@click.option("--password", help="Password for pyLoad")
Expand All @@ -59,6 +59,9 @@ def cli(
) -> None:
"""CLI for interacting with pyLoad."""

if not any([api_url, username, password, ctx.invoked_subcommand]):
click.echo(ctx.get_help())

config = load_config()

if api_url:
Expand Down Expand Up @@ -307,3 +310,95 @@ async def _toggle_reconnect() -> None:
raise click.ClickException("Unable to parse response from pyLoad") from e

asyncio.run(_toggle_reconnect())


@cli.command()
@click.pass_context
@click.argument(
"container",
type=click.Path(
exists=True,
readable=True,
path_type=Path,
),
)
def upload_container(ctx: click.Context, container: Path) -> None:
"""Upload a container file to pyLoad."""

with open(container, "rb") as f:
binary_data = f.read()

async def _upload_container() -> None:
try:
async with aiohttp.ClientSession() as session:
api = await init_api(
session,
ctx.obj["api_url"],
ctx.obj["username"],
ctx.obj["password"],
)

await api.upload_container(
filename=click.format_filename(container, shorten=True),
binary_data=binary_data,
)

except CannotConnect as e:
raise click.ClickException("Unable to connect to pyLoad") from e
except InvalidAuth as e:
raise click.ClickException(
"Authentication failed, verify username and password"
) from e
except ParserError as e:
raise click.ClickException("Unable to parse response from pyLoad") from e

asyncio.run(_upload_container())


@cli.command()
@click.pass_context
@click.argument("package_name")
@click.option(
"--queue/--collector",
default=True,
help="Add package to queue or collector. Defaults to queue",
)
def add_package(ctx: click.Context, package_name: str, queue: bool) -> None:
"""Add a package to pyLoad."""

links = []

while value := click.prompt(
"Please enter a link", type=str, default="", show_default=False
):
links.append(value)

if not links:
raise click.ClickException("No links entered")

async def _add_package() -> None:
try:
async with aiohttp.ClientSession() as session:
api = await init_api(
session,
ctx.obj["api_url"],
ctx.obj["username"],
ctx.obj["password"],
)

await api.add_package(
name=package_name,
links=links,
destination=Destination.QUEUE if queue else Destination.COLLECTOR,
)

except CannotConnect as e:
raise click.ClickException("Unable to connect to pyLoad") from e
except InvalidAuth as e:
raise click.ClickException(
"Authentication failed, verify username and password"
) from e
except ParserError as e:
raise click.ClickException("Unable to parse response from pyLoad") from e

asyncio.run(_add_package())
11 changes: 6 additions & 5 deletions src/pyloadapi/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"""

from enum import StrEnum
from enum import IntEnum, StrEnum
from typing import Any, NotRequired, TypedDict, TypeVar

T = TypeVar("T")
Expand Down Expand Up @@ -139,11 +139,12 @@ class PyLoadCommand(StrEnum):
RESTART = "restart"
VERSION = "getServerVersion"
FREESPACE = "freeSpace"
ADDPACKAGE = "addPackage"
ADD_PACKAGE = "addPackage"
UPLOAD_CONTAINER = "uploadContainer"


class Destination(StrEnum):
class Destination(IntEnum):
"""Destination for new Packages."""

QUEUE = "queue"
COLLECTOR = "collector"
COLLECTOR = 0
QUEUE = 1
2 changes: 2 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
"reconnect": False,
}

BYTE_DATA = b"BYTE_DATA"


@pytest.fixture(name="session")
async def aiohttp_client_session() -> AsyncGenerator[aiohttp.ClientSession, Any]:
Expand Down
Loading

0 comments on commit cf96816

Please sign in to comment.