From c11bc8557fee98eb1b2c9529b0400258f0c980a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Srokosz?= Date: Wed, 23 Feb 2022 15:57:43 +0100 Subject: [PATCH] Expose APIClientOptions in library interface and documentation (#67) --- docs/mwdblib.rst | 3 +++ docs/requirements.txt | 4 ++-- mwdblib/__init__.py | 3 ++- mwdblib/api/__init__.py | 3 ++- mwdblib/api/api.py | 53 ++++++++++++++++++++++++++++++++++++++++- mwdblib/api/options.py | 24 +++++++++++++++---- mwdblib/core.py | 30 +++++++++++++---------- 7 files changed, 99 insertions(+), 21 deletions(-) diff --git a/docs/mwdblib.rst b/docs/mwdblib.rst index 764f3b1..9b8a4ac 100644 --- a/docs/mwdblib.rst +++ b/docs/mwdblib.rst @@ -8,3 +8,6 @@ Core interface (MWDB) .. autoclass:: APIClient :members: + +.. autoclass:: APIClientOptions + :members: \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt index 0d95810..3b3c6e1 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ -Sphinx==3.0.3 +Sphinx==4.4.0 sphinx-autodoc-annotation==1.0.post1 sphinx-autodoc-typehints==1.10.3 -sphinx-rtd-theme==0.5.0 +sphinx-rtd-theme==1.0.0 diff --git a/mwdblib/__init__.py b/mwdblib/__init__.py index 356ed83..ebbd6d8 100644 --- a/mwdblib/__init__.py +++ b/mwdblib/__init__.py @@ -1,5 +1,5 @@ from .__version__ import __version__ -from .api import APIClient +from .api import APIClient, APIClientOptions from .blob import MWDBBlob from .comment import MWDBComment from .config import MWDBConfig @@ -11,6 +11,7 @@ __all__ = [ "MWDB", "APIClient", + "APIClientOptions", "MWDBFile", "MWDBObject", "MWDBConfig", diff --git a/mwdblib/api/__init__.py b/mwdblib/api/__init__.py index dcdcb5d..e28bf58 100644 --- a/mwdblib/api/__init__.py +++ b/mwdblib/api/__init__.py @@ -1,3 +1,4 @@ from .api import APIClient +from .options import APIClientOptions -__all__ = ["APIClient"] +__all__ = ["APIClient", "APIClientOptions"] diff --git a/mwdblib/api/api.py b/mwdblib/api/api.py index 0523b41..849272e 100644 --- a/mwdblib/api/api.py +++ b/mwdblib/api/api.py @@ -91,18 +91,36 @@ def __init__(self, _auth_token: Optional[str] = None, **api_options: Any) -> Non @property def server_metadata(self) -> dict: + """ + Information about MWDB Core server from ``/api/server`` endpoint. + """ if self._server_metadata is None: self._server_metadata = self.get("server", noauth=True) return self._server_metadata @property def server_version(self) -> str: + """ + MWDB Core server version + """ return str(self.server_metadata["server_version"]) @property def logged_user(self) -> Optional[str]: + """ + Username of logged-in user or the owner of used API key. + Returns None if no credentials are provided + """ return self.auth_token.username if self.auth_token else None + def supports_version(self, required_version: str) -> bool: + """ + Checks if server version is higher or equal than provided. + + .. versionadded:: 4.1.0 + """ + return parse_version(self.server_version) >= parse_version(required_version) + def set_auth_token(self, auth_key: str) -> None: self.auth_token = JWTAuthToken(auth_key) self.session.headers.update( @@ -110,6 +128,16 @@ def set_auth_token(self, auth_key: str) -> None: ) def login(self, username: str, password: str) -> None: + """ + Performs authentication using provided credentials + + .. note: + It's not recommended to use this method directly. + Use :py:meth:`MWDB.login` instead. + + :param username: Account username + :param password: Account password + """ token = self.post( "auth/login", json={"login": username, "password": password}, noauth=True )["token"] @@ -119,11 +147,23 @@ def login(self, username: str, password: str) -> None: self.options.password = password def set_api_key(self, api_key: str) -> None: + """ + Sets API key to be used for authorization + + .. note: + It's not recommended to use this method directly. + Pass ``api_key`` argument to ``MWDB`` constructor. + + :param api_key: API key to set + """ self.set_auth_token(api_key) # Store credentials in API options self.options.api_key = api_key def logout(self) -> None: + """ + Removes authorization token from APIClient instance + """ self.auth_token = None self.session.headers.pop("Authorization") @@ -150,10 +190,21 @@ def request( **kwargs: Any, ) -> Any: """ - Sends request to MWDB API. + Sends request to MWDB API. This method can be used for accessing features that + are not directly supported by mwdblib library. Other keyword arguments are the same as in requests library. + .. seealso:: + + Use functions specific for HTTP methods instead of passing + ``method`` argument on your own: + + - :py:meth:`APIClient.get` + - :py:meth:`APIClient.post` + - :py:meth:`APIClient.put` + - :py:meth:`APIClient.delete` + :param method: HTTP method :param url: Relative url of API endpoint :param noauth: | diff --git a/mwdblib/api/options.py b/mwdblib/api/options.py index 30341cf..b26cbc0 100644 --- a/mwdblib/api/options.py +++ b/mwdblib/api/options.py @@ -56,6 +56,20 @@ def __set__(self, instance: Any, value: Any) -> None: class APIClientOptions: + """ + Options bag that contains configuration for APIClient. + + Field values are loaded using the following precedence: + + - built-in defaults accessible via class properties e.g. + ``APIClientOptions.api_url`` + - values from ``~/.mwdb`` configuration file + - values passed as an arguments to the ``APIClientOptions`` constructor + + Configuration may depend on ``api_url`` value, so remember to set it if you want to + talk with specific MWDB Core instance. + """ + # Register fields and defaults api_url = OptionsField("https://mwdb.cert.pl/api/") api_key = OptionsField(value_type=str) @@ -146,8 +160,9 @@ def __init__( def clear_stored_credentials(self, config_writeback: bool = True) -> None: """ - Clears stored credentials in configuration for current user - Used by `mwdb logout` CLI command + Clears stored credentials in configuration for current user. + + Used by ``mwdb logout`` CLI command. """ if not self.username: return @@ -176,8 +191,9 @@ def clear_stored_credentials(self, config_writeback: bool = True) -> None: def store_credentials(self) -> None: """ - Stores current credentials in configuration for current user - Used by `mwdb login` CLI command + Stores current credentials in configuration for current user. + + Used by ``mwdb login`` CLI command. """ if not self.username or (not self.api_key and not self.password): return diff --git a/mwdblib/core.py b/mwdblib/core.py index a77d7c6..f6b7a7c 100644 --- a/mwdblib/core.py +++ b/mwdblib/core.py @@ -32,26 +32,31 @@ class MWDB: """ Main object used for communication with MWDB REST API - :param api_url: MWDB API URL that ends with '/api/'. + :param api_url: MWDB API URL (that ends with '/api/'). :param api_key: MWDB API key :param username: MWDB account username :param password: MWDB account password :param verify_ssl: Verify SSL certificate correctness (default: True) - :param obey_ratelimiter: If false, HTTP 429 errors will cause an exception + :param obey_ratelimiter: If ``False``, HTTP 429 errors will cause an exception like all other error codes. - If true (default), library will transparently handle them by sleeping - for a specified duration. - :param retry_on_downtime: If true, requests will be automatically retried - after 10 seconds on HTTP 502/504 and ConnectionError. + If ``True``, library will transparently handle them by sleeping + for a specified duration. Default is ``True``. + :param retry_on_downtime: If ``True``, requests will be automatically retried + after ``downtime_timeout`` seconds on HTTP 502/504 and ConnectionError. + Default is ``False``. :param max_downtime_retries: Number of retries caused by temporary downtime - :param downtime_timeout: How long we need to wait between retries (in seconds) + :param downtime_timeout: How long we need to wait between retries (in seconds). + Default is 10. :param retry_idempotent: Retry idempotent POST requests (default). The only thing that is really non-idempotent in current API is :meth:`MWDBObject.add_comment`, so it's not a big deal. You can turn it off if possible doubled comments are problematic in your MWDB instance. - :param use_keyring: If true (default), APIClient uses keyring to fetch + Default is ``True``. + :param use_keyring: If ``True``, APIClient uses keyring to fetch stored credentials. If not, they're fetched from plaintext configuration. - :param emit_warnings: If true (default), warnings are emitted by APIClient + Default is ``True``. + :param emit_warnings: If ``True``, warnings are emitted by APIClient. + Default is ``True``. :param config_path: Path to the configuration file (default is `~/.mwdb`). If None, configuration file will not be used by APIClient :param api: Custom :class:`APIClient` to be used for communication with MWDB @@ -83,7 +88,6 @@ class MWDB: mwdb.login("example", "") file = mwdb.query_file("3629344675705286607dd0f680c66c19f7e310a1") - """ def __init__(self, api: Optional[APIClient] = None, **api_options: Any): @@ -105,9 +109,11 @@ def login( """ Performs user authentication using provided username and password. + If credentials are not set, asks interactively for credentials. + .. warning:: - Keep in mind that password-authenticated sessions are short lived, + Keep in mind that password-authenticated sessions are short-lived, so password needs to be stored in :class:`APIClient` object. Consider generating a new API key in your MWDB profile. @@ -125,7 +131,7 @@ def login( :py:meth:`MWDB.login` no longer warns about password-authenticated sessions or credentials that are already set up. - :param username: User name + :param username: Username :type username: str :param password: Password :type password: str