diff --git a/src/aria2p/__init__.py b/src/aria2p/__init__.py index eee2e2b..1482b66 100644 --- a/src/aria2p/__init__.py +++ b/src/aria2p/__init__.py @@ -1,7 +1,18 @@ +""" +Aria2p package. + +This package provides a command-line tool and a Python library to interact with an `aria2c` daemon process through +JSON-RPC. + +If you read this message, you probably want to learn about the library and not the command-line tool: +please refer to the README.md included in this package to get the link to the official documentation. +""" + + from .api import API from .client import JSONRPCClient, JSONRPCError -from .downloads import Download, Bittorrent, File +from .downloads import Download, BitTorrent, File from .options import Options from .stats import Stats -__all__ = ["API", "JSONRPCError", "JSONRPCClient", "Download", "Bittorrent", "File", "Options", "Stats"] +__all__ = ["API", "JSONRPCError", "JSONRPCClient", "Download", "BitTorrent", "File", "Options", "Stats"] diff --git a/src/aria2p/__main__.py b/src/aria2p/__main__.py index 93aac9d..6238ee3 100644 --- a/src/aria2p/__main__.py +++ b/src/aria2p/__main__.py @@ -1,5 +1,5 @@ """ -Entrypoint module, in case you use `python -maria2p`. +Entry-point module, in case you use `python -maria2p`. Why does this file exist, and why __main__? For more info, read: diff --git a/src/aria2p/api.py b/src/aria2p/api.py index dd16a5a..91484cd 100644 --- a/src/aria2p/api.py +++ b/src/aria2p/api.py @@ -1,9 +1,25 @@ +""" +This module defines the API class, which makes use of a JSON-RPC client to provide higher-level methods to +interact easily with a remote aria2c process. +""" + + from .downloads import Download from .options import Options from .stats import Stats class API: + """ + A class providing high-level methods to interact with a remote aria2c process. + + This class is instantiated with a reference to a :class:`client.JSONRPCClient` instance. It then uses this client + to call remote procedures, or remote methods. The client methods reflect exactly what aria2c is providing + through JSON-RPC, while this class's methods allow for easier / faster control of the remote process. It also + wraps the information the client retrieves in Python object, like :class:`downloads.Download`, allowing for + even more Pythonic interactions, without worrying about payloads, responses, JSON, etc.. + """ + def __init__(self, json_rpc_client): self.client = json_rpc_client self.downloads = {} @@ -22,7 +38,7 @@ def fetch_options(self): self.options = Options(self, self.client.get_global_option) def fetch_stats(self): - self.stats = Stats(self, self.client.get_global_stat()) + self.stats = Stats(self.client.get_global_stat()) def add_magnet(self, magnet_uri): pass diff --git a/src/aria2p/cli.py b/src/aria2p/cli.py index 9f5a96a..76da5e5 100644 --- a/src/aria2p/cli.py +++ b/src/aria2p/cli.py @@ -25,6 +25,7 @@ def get_method(name, default=None): + """Return the actual method name from a differently formatted name.""" methods = {} for method in JSONRPCClient.METHODS: methods[method.lower()] = method @@ -36,6 +37,7 @@ def get_method(name, default=None): def get_parser(): + """Return a parser for the command-line options and arguments.""" parser = argparse.ArgumentParser() mutually_exclusive = parser.add_mutually_exclusive_group() @@ -55,6 +57,7 @@ def get_parser(): def main(args=None): + """The main function, which is executed when you type ``aria2p`` or ``python -m aria2p``.""" client = JSONRPCClient() parser = get_parser() diff --git a/src/aria2p/client.py b/src/aria2p/client.py index 1a64ca8..b7df132 100644 --- a/src/aria2p/client.py +++ b/src/aria2p/client.py @@ -1,3 +1,9 @@ +""" +This module defines the JSONRPCError and JSONRPCClient classes, which are used to communicate with a remote aria2c +process through the JSON-RPC protocol. +""" + + import json import requests @@ -22,6 +28,8 @@ class JSONRPCError(Exception): + """An exception specific to JSON-RPC errors.""" + def __init__(self, code, message): if code in JSONRPC_CODES: message = f"{JSONRPC_CODES[code]}\n{message}" @@ -31,6 +39,31 @@ def __init__(self, code, message): class JSONRPCClient: + """ + The JSON-RPC client class. + + In this documentation, all the following terms refer to the same entity, the remote aria2c process: + remote process, remote server, server, daemon process, background process, remote. + + This class implements method to communicate with a daemon aria2c process through the JSON-RPC protocol. + Each method offered by the aria2c process is implemented in this class, in snake_case instead of camelCase + (example: add_uri instead of addUri). + + The class defines a ``METHODS`` variable which contains the names of the available methods. + + The class is instantiated using an address and port, and optionally a secret token. The token is never passed + as a method argument. + + The class provides utility methods: + + - call, which performs a JSON-RPC call for a single method; + - batch_call, which performs a JSON-RPC call for a list of methods; + - multicall2, which is an equivalent of multicall, but easier to use; + - post, which is responsible for actually sending a payload to the remote process using a POST request; + - get_payload, which is used to build payloads; + - get_params, which is used to build list of parameters. + """ + ADD_URI = "aria2.addUri" ADD_TORRENT = "aria2.addTorrent" ADD_METALINK = "aria2.addMetalink" @@ -107,7 +140,15 @@ class JSONRPCClient: LIST_NOTIFICATIONS, ] - def __init__(self, host="http://localhost", port=6800, secret=None): + def __init__(self, host=DEFAULT_HOST, port=DEFAULT_PORT, secret=""): + """ + Initialization method. + + Args: + host (str): the remote process address. + port (int): the remote process port. + secret (str): the secret token. + """ host = host.rstrip("/") self.host = host @@ -119,10 +160,23 @@ def __str__(self): @property def server(self): + """Property to return the full remote process / server address.""" return f"{self.host}:{self.port}/jsonrpc" # utils def call(self, method, params=None, msg_id=None, insert_secret=True): + """ + Call a single JSON-RPC method. + + Args: + method (str): the method name. You can use the constant defined in :class:`client.JSONRPCClient`. + params (list of str): a list of parameters, as strings. + msg_id (int/str): the ID of the call, sent back with the server's answer. + insert_secret (bool): whether to insert the secret token in the parameters or not. + + Returns: + The answer from the server, as a Python object (dict / list / str / int). + """ params = self.get_params(*(params or [])) if insert_secret and self.secret: @@ -135,6 +189,24 @@ def call(self, method, params=None, msg_id=None, insert_secret=True): return self.post(self.get_payload(method, params, msg_id=msg_id)) def batch_call(self, calls, insert_secret=True): + """ + Call multiple methods in one request. + + A batch call is simply a list of full payloads, sent at once to the remote process. The differences with a + multicall are: + + - multicall is defined in the JSON-RPC protocol specification, whereas batch_call is not + - multicall is a special "system" method, whereas batch_call is simply the concatenation of several methods + - multicall payloads define the "jsonrpc" and "id" keys only once, whereas these keys are repeated in + each part of the batch_call method + + Args: + calls (list): a list of tuples composed of method name, parameters and ID. + insert_secret (bool): whether to insert the secret token in the parameters or not. + + Returns: + The answer from the server, as a Python object (dict / list / str / int). + """ payloads = [] for method, params, msg_id in calls: @@ -148,6 +220,37 @@ def batch_call(self, calls, insert_secret=True): return self.post(payload) def multicall2(self, calls, insert_secret=True): + """ + An method equivalent to multicall, but with a simplified usage. + + Instead of providing dictionaries with "methodName" and "params" keys and values, this method allows you + to provide the values only, in tuples of length 2. + + With a classic multicall, you would write your params like: + + [ + {"methodName": client.REMOVE, "params": ["2089b05ecca3d829"]}, + {"methodName": client.REMOVE, "params": ["2fa07b6e85c40205"]}, + ] + + With multicall2, you can reduce the verbosity: + + [ + (client.REMOVE, ["2089b05ecca3d829"]), + (client.REMOVE, ["2fa07b6e85c40205"]), + ] + + Note: + multicall2 is not part of the JSON-RPC protocol specification. + It is implemented here as a simple convenience method. + + Args: + calls (list): list of tuples composed of method name and parameters. + insert_secret (bool): whether to insert the secret token in the parameters or not. + + Returns: + The answer from the server, as a Python object (dict / list / str / int). + """ multicall_params = [] for method, params in calls: @@ -161,6 +264,22 @@ def multicall2(self, calls, insert_secret=True): return self.post(payload) def post(self, payload): + """ + Send a POST request to the server. + + The response is a JSON string, which we then load as a Python object. + + Args: + payload (dict): the payload / data to send to the remote process. It contains the following key-value pairs: + "jsonrpc": "2.0", "method": method, "id": id, "params": params (optional). + + Returns: + The answer from the server, as a Python object (dict / list / str / int). + + Raises: + JSONRPCError: when the server returns an error (client/server error). + See the :class:`client.JSONRPCError` class. + """ response = requests.post(self.server, data=payload).json() if "result" in response: return response["result"] @@ -168,6 +287,18 @@ def post(self, payload): @staticmethod def get_payload(method, params=None, msg_id=None, as_json=True): + """ + Build a payload. + + Args: + method (str): the method name. You can use the constant defined in :class:`client.JSONRPCClient`. + params (list): the list of parameters. + msg_id (int/str): the ID of the call, sent back with the server's answer. + as_json (bool): whether to return the payload as a JSON-string or Python dictionary. + + Returns: + + """ payload = {"jsonrpc": "2.0", "method": method} if msg_id is not None: @@ -182,6 +313,16 @@ def get_payload(method, params=None, msg_id=None, as_json=True): @staticmethod def get_params(*args): + """ + Build the list of parameters. + + This method simply removes the ``None`̀` values from the given arguments. + Args: + *args: list of parameters. + + Returns: + A new list, with ``None``s filtered out. + """ return [p for p in args if p is not None] # aria2 diff --git a/src/aria2p/downloads.py b/src/aria2p/downloads.py index ddffbe4..863c1ed 100644 --- a/src/aria2p/downloads.py +++ b/src/aria2p/downloads.py @@ -1,5 +1,19 @@ -class Bittorrent: +""" +This module defines the BitTorrent, File and Download classes, which respectively hold structured information about +torrent files, files and downloads in aria2c. +""" + + +class BitTorrent: + """Information retrieved from a .torrent file.""" + def __init__(self, struct): + """ + Initialization method. + + Args: + struct (dict): a dictionary Python object returned by the JSON-RPC client. + """ self._struct = struct def __str__(self): @@ -7,34 +21,61 @@ def __str__(self): @property def announce_list(self): - # List of lists of announce URIs. If the torrent contains announce and no announce-list, announce - # is converted to the announce-list format. + """ + List of lists of announce URIs. + + If the torrent contains announce and no announce-list, announce is converted to the announce-list format. + """ return self._struct.get("announceList") @property def comment(self): - # The comment of the torrent. comment.utf-8 is used if available. + """ + The comment of the torrent. + + comment.utf-8 is used if available. + """ return self._struct.get("comment") @property def creation_date(self): - # The creation time of the torrent. The value is an integer since the epoch, measured in seconds. + """ + The creation time of the torrent. + + The value is an integer since the epoch, measured in seconds. + """ return self._struct.get("creationDate") @property def mode(self): - # File mode of the torrent. The value is either single or multi. + """ + File mode of the torrent. + + The value is either single or multi. + """ return self._struct.get("mode") @property def info(self): - # Struct which contains data from Info dictionary. It contains following keys. - # name name in info dictionary. name.utf-8 is used if available. + """ + Struct which contains data from Info dictionary. + + It contains following keys: + name name in info dictionary. name.utf-8 is used if available. + """ return self._struct.get("info") class File: + """Information about a download's file.""" + def __init__(self, struct): + """ + Initialization method. + + Args: + struct (dict): a dictionary Python object returned by the JSON-RPC client. + """ self._struct = struct def __str__(self): @@ -42,42 +83,61 @@ def __str__(self): @property def index(self): - # Index of the file, starting at 1, in the same order as files appear in the multi-file torrent. + """Index of the file, starting at 1, in the same order as files appear in the multi-file torrent.""" return self._struct.get("index") @property def path(self): - # File path. + """File path.""" return self._struct.get("path") @property def length(self): - # File size in bytes. + """File size in bytes.""" return self._struct.get("length") @property def completed_length(self): - # Completed length of this file in bytes. Please note that it is possible that sum of completedLength - # is less than the completedLength returned by the aria2.tellStatus() method. This is because - # completedLength in aria2.getFiles() only includes completed pieces. On the other hand, - # completedLength in aria2.tellStatus() also includes partially completed pieces. + """ + Completed length of this file in bytes. + + Please note that it is possible that sum of completedLength is less than the completedLength returned by the + aria2.tellStatus() method. This is because completedLength in aria2.getFiles() only includes completed + pieces. On the other hand, completedLength in aria2.tellStatus() also includes partially completed pieces. + """ return self._struct.get("completedLength") @property def selected(self): - # true if this file is selected by --select-file option. If --select-file is not specified or this is - # single-file torrent or not a torrent download at all, this value is always true. Otherwise false. + """ + True if this file is selected by --select-file option. + + If --select-file is not specified or this is single-file torrent or not a torrent download at all, this value + is always true. Otherwise false. + """ return self._struct.get("selected") @property def uris(self): - # Returns a list of URIs for this file. The element type is the same struct used in the aria2.getUris() - # method. + """ + Return a list of URIs for this file. + + The element type is the same struct used in the aria2.getUris() method. + """ return self._struct.get("uris") class Download: + """Class containing all information about a download, as retrieved with the client.""" + def __init__(self, api, struct): + """ + Initialization method. + + Args: + api (:class:`api.API`): the reference to an :class:`api.API` instance. + struct (dict): a dictionary Python object returned by the JSON-RPC client. + """ self.api = api self._struct = struct self._files = [] @@ -90,162 +150,237 @@ def __str__(self): @property def name(self): + """ + The name of the download. + + Name is the name of the file if single-file, first file's directory name if multi-file. + """ if not self._name: self._name = self.files[0].path.replace(self.dir, "").lstrip("/").split("/")[0] return self._name @property def options(self): + """ + Options specific to this download. + + The returned object is an instance of :class:`options.Options`. + """ if not self._options: self._options = self.api.get_options(gids=[self.gid]).get(self.gid) return self._options @property def gid(self): + """GID of the download.""" return self._struct.get("gid") @property def status(self): - # active waiting paused error complete removed + """Status of the download: active, waiting, paused, error, complete or removed.""" return self._struct.get("status") @property def total_length(self): - # Total length of the download in bytes. + """Total length of the download in bytes.""" return self._struct.get("totalLength") @property def completed_length(self): - # Completed length of the download in bytes. + """Completed length of the download in bytes.""" return self._struct.get("completedLength") @property def upload_length(self): - # Uploaded length of the download in bytes. + """Uploaded length of the download in bytes.""" return self._struct.get("uploadLength") @property def bitfield(self): - # Hexadecimal representation of the download progress. The highest bit corresponds to the piece at - # index 0. Any set bits indicate loaded pieces, while unset bits indicate not yet loaded and/or missing - # pieces. Any overflow bits at the end are set to zero. When the download was not started yet, this key - # will not be included in the response. + """ + Hexadecimal representation of the download progress. + + The highest bit corresponds to the piece at index 0. Any set bits indicate loaded pieces, while unset bits + indicate not yet loaded and/or missing pieces. Any overflow bits at the end are set to zero. When the + download was not started yet, this key will not be included in the response. + """ return self._struct.get("bitfield") @property def download_speed(self): - # Download speed of this download measured in bytes/sec. + """Download speed of this download measured in bytes/sec.""" return self._struct.get("downloadSpeed") @property def upload_speed(self): - # Upload speed of this download measured in bytes/sec. + """Upload speed of this download measured in bytes/sec.""" return self._struct.get("uploadSpeed") @property def info_hash(self): - # InfoHash. BitTorrent only. + """ + InfoHash. + + BitTorrent only. + """ return self._struct.get("infoHash") @property def num_seeders(self): - # The number of seeders aria2 has connected to. BitTorrent only. + """ + The number of seeders aria2 has connected to. + + BitTorrent only. + """ return self._struct.get("numSeeders") @property def seeder(self): - # true if the local endpoint is a seeder. Otherwise false. BitTorrent only. + """ + True if the local endpoint is a seeder, otherwise false. + + BitTorrent only. + """ return self._struct.get("seeder ") @property def piece_length(self): - # Piece length in bytes. + """Piece length in bytes.""" return self._struct.get("pieceLength") @property def num_pieces(self): - # The number of pieces. + """The number of pieces.""" return self._struct.get("numPieces") @property def connections(self): - # The number of peers/servers aria2 has connected to. + """The number of peers/servers aria2 has connected to.""" return self._struct.get("connections") @property def error_code(self): - # The code of the last error for this item, if any. The value is a string. The error codes are defined - # in the EXIT STATUS section. This value is only available for stopped/completed downloads. + """ + The code of the last error for this item, if any. + + The value is a string. The error codes are defined in the EXIT STATUS section. This value is only available + for stopped/completed downloads. + """ return self._struct.get("errorCode") @property def error_message(self): - # The (hopefully) human readable error message associated to errorCode. + """The (hopefully) human readable error message associated to errorCode.""" return self._struct.get("errorMessage") @property def followed_by_ids(self): - # List of GIDs which are generated as the result of this download. For example, when aria2 downloads a - # Metalink file, it generates downloads described in the Metalink (see the --follow-metalink - # option). This value is useful to track auto-generated downloads. If there are no such downloads, - # this key will not be included in the response. + """ + List of GIDs which are generated as the result of this download. + + For example, when aria2 downloads a Metalink file, it generates downloads described in the Metalink (see the + --follow-metalink option). This value is useful to track auto-generated downloads. If there are no such + downloads, this key will not be included in the response. + """ return self._struct.get("followedBy") @property def followed_by(self): + """ + List of downloads generated as the result of this download. + + Returns a list of instances of :class:`downloads.Download`. + """ return [self.api.get_download(gid) for gid in self.followed_by_ids] @property def following_id(self): - # The reverse link for followedBy. A download included in followedBy has this object's GID in its - # following value. + """ + The reverse link for followedBy. + + A download included in followedBy has this object's GID in its following value. + """ return self._struct.get("following") @property def following(self): + """ + The download this download is following. + + Returns an instance of :class:`downloads.Download`. + """ return self.api.get_download(self.following_id) @property def belongs_to_id(self): - # GID of a parent download. Some downloads are a part of another download. For example, if a file in a - # Metalink has BitTorrent resources, the downloads of ".torrent" files are parts of that parent. If - # this download has no parent, this key will not be included in the response. + """ + GID of a parent download. + + Some downloads are a part of another download. For example, if a file in a Metalink has BitTorrent resources, + The downloads of ".torrent" files are parts of that parent. If this download has no parent, this key will not + be included in the response. + """ return self._struct.get("belongsTo") @property def belongs_to(self): + """ + Parent download. + + Returns an instance of :class:`downloads.Download`. + """ return self.api.get_download(self.belongs_to_id) @property def dir(self): - # Directory to save files. + """Directory to save files.""" return self._struct.get("dir") @property def files(self): - # Return the list of files. The elements of this list are the same structs used in aria2.getFiles() method. + """ + Return the list of files. + + The elements of this list are the same structs used in aria2.getFiles() method. + """ if not self._files: self._files = [File(s) for s in self._struct.get("files")] return self._files @property def bittorrent(self): - # Struct which contains information retrieved from the .torrent (file). BitTorrent only. + """ + Struct which contains information retrieved from the .torrent (file). + + BitTorrent only. + """ if not self._bittorrent: - self._bittorrent = Bittorrent(self._struct.get("bittorrent")) + self._bittorrent = BitTorrent(self._struct.get("bittorrent")) return self._bittorrent @property def verified_length(self): - # The number of verified number of bytes while the files are being hash checked. This key exists only - # when this download is being hash checked. + """ + The number of verified number of bytes while the files are being hash checked. + + This key exists only when this download is being hash checked. + """ return self._struct.get("verifiedLength") @property def verify_integrity_pending(self): - # true if this download is waiting for the hash check in a queue. - # This key exists only when this download is in the queue. + """ + True if this download is waiting for the hash check in a queue. + + This key exists only when this download is in the queue. + """ return self._struct.get("verifyIntegrityPending") def update(self, struct): + """ + Method to update the internal values of the download with more recent values. + + Args: + struct (dict): a dictionary Python object returned by the JSON-RPC client. + """ self._struct.update(struct) diff --git a/src/aria2p/options.py b/src/aria2p/options.py index 934ce4f..aa6daa5 100644 --- a/src/aria2p/options.py +++ b/src/aria2p/options.py @@ -1,5 +1,29 @@ +""" +This module defines the Options class, which holds information retrieved with the ``get_option`` or +``get_global_option`` methods of the client. +""" + + class Options: + """ + This class holds information retrieved with the ``get_option`` or ``get_global_option`` methods of the client. + + Instances are given a reference to an :class:`api.API` instance to be able to change their values both locally + and remotely, by using the API client and calling remote methods to change options. + + Please refer to aria2c documentation or man pages to see all the available options. + """ + def __init__(self, api, struct, gid=None): + """ + Initialization method. + + Args: + api (:class:`api.API`): the reference to an :class:`api.API` instance. + struct (dict): a dictionary Python object returned by the JSON-RPC client. + gid (str): an optional GID to inform about the owner (:class:`downloads.Download`), or None to tell they + are global options. + """ __setattr = super().__setattr__ __setattr("api", api) __setattr("_gids", [gid] if gid else []) @@ -18,6 +42,7 @@ def __setattr__(self, key, value): # Append _ to continue because it is a reserved keyword @property def continue_(self): + """Because ``continue`` is a reserved keyword in Python.""" return self._struct.get("continue") @continue_.setter diff --git a/src/aria2p/stats.py b/src/aria2p/stats.py index 54ccd45..080e472 100644 --- a/src/aria2p/stats.py +++ b/src/aria2p/stats.py @@ -1,36 +1,50 @@ +""" +This module defines the Stats class, which holds information retrieved with the ``get_global_stat`` method of the +client. +""" + + class Stats: - def __init__(self, api, struct): - self.api = api + """This class holds information retrieved with the ``get_global_stat`` method of the client.""" + + def __init__(self, struct): + """ + Initialization method. + + Args: + struct (dict): a dictionary Python object returned by the JSON-RPC client. + """ self._struct = struct @property def download_speed(self): - # Overall download speed (byte/sec). + """Overall download speed (byte/sec).""" return self._struct.get("downloadSpeed") @property def upload_speed(self): - # Overall upload speed(byte/sec). + """Overall upload speed(byte/sec).""" return self._struct.get("uploadSpeed") @property def num_active(self): - # The number of active downloads. + """The number of active downloads.""" return self._struct.get("numActive") @property def num_waiting(self): - # The number of waiting downloads. + """The number of waiting downloads.""" return self._struct.get("numWaiting") @property def num_stopped(self): - # The number of stopped downloads in the current session. This value is capped by the - # --max-download-result option. + """ + The number of stopped downloads in the current session. This value is capped by the --max-download-result + option. + """ return self._struct.get("numStopped") @property def num_stopped_total(self): - # The number of stopped downloads in the current session and not capped by the - # --max-download-result option. + """The number of stopped downloads in the current session and not capped by the --max-download-result option.""" return self._struct.get("numStoppedTotal")