diff --git a/examples/connect/dash/app.py b/examples/connect/dash/app.py index 7af29885..bbea72dd 100644 --- a/examples/connect/dash/app.py +++ b/examples/connect/dash/app.py @@ -37,7 +37,6 @@ def update_page(_): Dash example application that shows user information and the first few rows from a table hosted in Databricks. """ - session_token = flask.request.headers.get("Posit-Connect-User-Session-Token") credentials_provider = viewer_credentials_provider(user_session_token=session_token) diff --git a/examples/connect/shiny-python/app.py b/examples/connect/shiny-python/app.py index fe18377b..121194d6 100644 --- a/examples/connect/shiny-python/app.py +++ b/examples/connect/shiny-python/app.py @@ -24,7 +24,6 @@ def server(input: Inputs, output: Outputs, session: Session): Shiny for Python example application that shows user information and the first few rows from a table hosted in Databricks. """ - session_token = session.http_conn.headers.get("Posit-Connect-User-Session-Token") credentials_provider = viewer_credentials_provider(user_session_token=session_token) diff --git a/pyproject.toml b/pyproject.toml index bf2c4dc8..2dd030c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,9 +19,7 @@ classifiers = [ "Typing :: Typed", ] dynamic = ["version"] -dependencies = [ - "requests>=2.31.0,<3" -] +dependencies = ["requests>=2.31.0,<3"] [project.urls] Source = "https://github.com/posit-dev/posit-sdk-py" @@ -29,9 +27,46 @@ Issues = "https://github.com/posit-dev/posit-sdk-py/issues" [tool.pytest.ini_options] testpaths = ["tests"] -addopts = [ - "--import-mode=importlib", -] +addopts = ["--import-mode=importlib"] [tool.setuptools_scm] version_file = "src/posit/_version.py" + +[tool.ruff.format] +docstring-code-format = true +docstring-code-line-length = 20 + +[tool.ruff.lint] +select = ["D"] +ignore = [ + # NumPy style docstring convention with noted exceptions. + # https://docs.astral.sh/ruff/faq/#does-ruff-support-numpy-or-google-style-docstrings + # + # This docstring style works with [quarotdoc](https://machow.github.io/quartodoc/get-started/overview.html). + # + 'D101', # TODO(#135) implement docstring for public class + 'D103', # TODO(#135) implement docstring for public functions + 'D104', # TODO(#135) implement docstring for public package + 'D105', # TODO(#135) implement docstring for magic methods + 'D107', + 'D203', + 'D212', + 'D213', + 'D100', # TODO(#135) implement docstring for public modules + 'D102', # TODO(#135) implement docstring for public methods + 'D401', # TODO(#135) fix imperative mood warnings + 'D402', + 'D413', + 'D415', + 'D416', + 'D417', + 'D418', # The Python Language Server can accomdate documentation for individual methods. + # TODO(#135) resarch D418 and determine if we should continue ignoring it. +] + +[tool.ruff.lint.per-file-ignores] +"examples/*" = ["D"] +"tests/*" = ["D"] + +[tool.ruff.lint.pydocstyle] +convention = "numpy" diff --git a/src/posit/__init__.py b/src/posit/__init__.py index e69de29b..32a10a33 100644 --- a/src/posit/__init__.py +++ b/src/posit/__init__.py @@ -0,0 +1 @@ +"""The Posit SDK.""" diff --git a/src/posit/connect/auth.py b/src/posit/connect/auth.py index d4a72d04..1bb61f4e 100644 --- a/src/posit/connect/auth.py +++ b/src/posit/connect/auth.py @@ -1,3 +1,5 @@ +"""Provides authentication functionality.""" + from requests import PreparedRequest from requests.auth import AuthBase @@ -5,9 +7,12 @@ class Auth(AuthBase): + """Handles authentication for API requests.""" + def __init__(self, config: Config) -> None: self._config = config def __call__(self, r: PreparedRequest) -> PreparedRequest: + """Add authorization header to the request.""" r.headers["Authorization"] = f"Key {self._config.api_key}" return r diff --git a/src/posit/connect/client.py b/src/posit/connect/client.py index 7f6ad606..412ab0fe 100644 --- a/src/posit/connect/client.py +++ b/src/posit/connect/client.py @@ -1,3 +1,5 @@ +"""Contains the Client class.""" + from __future__ import annotations from requests import Response, Session @@ -13,6 +15,8 @@ class Client: + """Main interface for Posit Connect.""" + def __init__( self, api_key: Optional[str] = None, @@ -42,42 +46,67 @@ def __init__( @property def connect_version(self): + """The server version. + + Return: + str + """ if self._server_settings is None: self._server_settings = self.get("server_settings").json() return self._server_settings["version"] @property def me(self) -> User: + """The connected user. + + Returns + ------- + User + """ return me.get(self.config, self.session) @property def oauth(self) -> OAuthIntegration: + """An OAuthIntegration. + + Returns + ------- + OAuthIntegration + """ return OAuthIntegration(config=self.config, session=self.session) @property def users(self) -> Users: + """The users resource interface. + + Returns + ------- + Users + """ return Users(config=self.config, session=self.session) @property def content(self) -> Content: + """The content resource interface. + + Returns + ------- + Content + """ return Content(config=self.config, session=self.session) def __del__(self): - """ - Close the session when the Client instance is deleted. - """ + """Close the session when the Client instance is deleted.""" if hasattr(self, "session") and self.session is not None: self.session.close() def __enter__(self): - """ - Enter method for using the client as a context manager. - """ + """Enter method for using the client as a context manager.""" return self def __exit__(self, exc_type, exc_value, exc_tb): """ - Closes the session if it exists. + Close the session if it exists. Args: exc_type: The type of the exception raised (if any). @@ -89,29 +118,33 @@ def __exit__(self, exc_type, exc_value, exc_tb): def request(self, method: str, path: str, **kwargs) -> Response: """ - Sends an HTTP request to the specified path using the given method. + Send an HTTP request. + + A facade for requests.Session.request. Args: method (str): The HTTP method to use for the request. - path (str): The path to send the request to. - **kwargs: Additional keyword arguments to pass to the underlying session's request method. + path (str): Appended to the url object attribute. + **kwargs: Additional keyword arguments passed to requests.Session.post. - Returns: - Response: The response object containing the server's response to the request. + Returns + ------- + Response: A requests.Response object. """ url = urls.append_path(self.config.url, path) return self.session.request(method, url, **kwargs) def get(self, path: str, **kwargs) -> Response: """ - Send a GET request to the specified path. + Send a GET request. Args: - path (str): The path to send the request to. - **kwargs: Additional keyword arguments to be passed to the underlying session's `get` method. + path (str): Appended to the configured base url. + **kwargs: Additional keyword arguments passed to requests.Session.get. - Returns: - Response: The response object. + Returns + ------- + Response: A requests.Response object. """ url = urls.append_path(self.config.url, path) @@ -119,14 +152,15 @@ def get(self, path: str, **kwargs) -> Response: def post(self, path: str, **kwargs) -> Response: """ - Send a POST request to the specified path. + Send a POST request. Args: - path (str): The path to send the request to. - **kwargs: Additional keyword arguments to be passed to the underlying session's `post` method. + path (str): Appended to the configured base url. + **kwargs: Additional keyword arguments passed to requests.Session.post. - Returns: - Response: The response object. + Returns + ------- + Response: A requests.Response object. """ url = urls.append_path(self.config.url, path) @@ -134,14 +168,15 @@ def post(self, path: str, **kwargs) -> Response: def put(self, path: str, **kwargs) -> Response: """ - Send a PUT request to the specified path. + Send a PUT request. Args: - path (str): The path to send the request to. - **kwargs: Additional keyword arguments to be passed to the underlying session's `put` method. + path (str): Appended to the configured base url. + **kwargs: Additional keyword arguments passed to requests.Session.put. - Returns: - Response: The response object. + Returns + ------- + Response: A requests.Response object. """ url = urls.append_path(self.config.url, path) @@ -149,14 +184,15 @@ def put(self, path: str, **kwargs) -> Response: def patch(self, path: str, **kwargs) -> Response: """ - Send a PATCH request to the specified path. + Send a PATCH request. Args: - path (str): The path to send the request to. - **kwargs: Additional keyword arguments to be passed to the underlying session's `patch` method. + path (str): Appended to the configured base url. + **kwargs: Additional keyword arguments passed to requests.Session.patch. - Returns: - Response: The response object. + Returns + ------- + Response: A requests.Response object. """ url = urls.append_path(self.config.url, path) @@ -164,14 +200,15 @@ def patch(self, path: str, **kwargs) -> Response: def delete(self, path: str, **kwargs) -> Response: """ - Send a DELETE request to the specified path. + Send a DELETE request. Args: - path (str): The path to send the request to. - **kwargs: Additional keyword arguments to be passed to the underlying session's `delete` method. + path (str): Appended to the configured base url. + **kwargs: Additional keyword arguments passed to requests.Session.delete. - Returns: - Response: The response object. + Returns + ------- + Response: A requests.Response object. """ url = urls.append_path(self.config.url, path) diff --git a/src/posit/connect/config.py b/src/posit/connect/config.py index 706404a4..c82c4260 100644 --- a/src/posit/connect/config.py +++ b/src/posit/connect/config.py @@ -1,3 +1,5 @@ +"""Client configuration.""" + import os from typing import Optional @@ -6,13 +8,17 @@ def _get_api_key() -> str: - """Gets the API key from the environment variable 'CONNECT_API_KEY'. + """Return the system configured api key. + + Reads the environment variable 'CONNECT_API_KEY'. - Raises: - ValueError: if CONNECT_API_KEY is not set or invalid + Raises + ------ + ValueError: If CONNECT_API_KEY is not set or invalid - Returns: - The API key + Returns + ------- + str """ value = os.environ.get("CONNECT_API_KEY") if not value: @@ -23,15 +29,17 @@ def _get_api_key() -> str: def _get_url() -> str: - """Gets the endpoint from the environment variable 'CONNECT_SERVER'. + """Return the system configured url. - The `requests` library uses 'endpoint' instead of 'server'. We will use 'endpoint' from here forward for consistency. + Reads the environment variable 'CONNECT_SERVER'. - Raises: - ValueError: if CONNECT_SERVER is not set or invalid. + Raises + ------ + ValueError: If CONNECT_SERVER is not set or invalid - Returns: - The endpoint. + Returns + ------- + str """ value = os.environ.get("CONNECT_SERVER") if not value: @@ -42,7 +50,7 @@ def _get_url() -> str: class Config: - """Derived configuration properties""" + """Configuration object.""" def __init__( self, api_key: Optional[str] = None, url: Optional[str] = None diff --git a/src/posit/connect/content.py b/src/posit/connect/content.py index b5bd9a7c..16d132e3 100644 --- a/src/posit/connect/content.py +++ b/src/posit/connect/content.py @@ -1,3 +1,5 @@ +"""Provides the Content resource interface.""" + from __future__ import annotations from typing import List, Optional, overload @@ -12,6 +14,19 @@ class ContentItem(Resource): + """A piece of content. + + Parameters + ---------- + Resource : _type_ + _description_ + + Returns + ------- + _type_ + _description_ + """ + @property def guid(self) -> str: return self.get("guid") # type: ignore @@ -243,7 +258,8 @@ def update( default_py_environment_management (Optional[bool]): Whether to use default Python environment management. service_account_name (Optional[str]): The service account name. - Returns: + Returns + ------- None """ ... @@ -257,7 +273,8 @@ def update(self, *args, **kwargs) -> None: *args **kwargs - Returns: + Returns + ------- None """ ... @@ -270,7 +287,8 @@ def update(self, *args, **kwargs) -> None: *args **kwargs - Returns: + Returns + ------- None """ body = dict(*args, **kwargs) diff --git a/src/posit/connect/external/databricks.py b/src/posit/connect/external/databricks.py index da4e7265..186dce0e 100644 --- a/src/posit/connect/external/databricks.py +++ b/src/posit/connect/external/databricks.py @@ -12,8 +12,10 @@ # https://github.com/databricks/databricks-sql-python/blob/v3.1.0/src/databricks/sql/auth/authenticators.py # In order to keep compatibility with the Databricks SDK class CredentialsProvider(abc.ABC): - """CredentialsProvider is the protocol (call-side interface) - for authenticating requests to Databricks REST APIs""" + """Protocol Databricks authentication. + + A call-side interface for the Databricks Rest API. + """ @abc.abstractmethod def auth_type(self) -> str: diff --git a/src/posit/connect/me.py b/src/posit/connect/me.py index 71ad7e5b..1d04a42b 100644 --- a/src/posit/connect/me.py +++ b/src/posit/connect/me.py @@ -14,7 +14,8 @@ def get(config: Config, session: requests.Session) -> User: config (Config): The configuration object containing the URL. session (requests.Session): The session object used for making HTTP requests. - Returns: + Returns + ------- User: The current user. """ url = urls.append_path(config.url, "v1/user") diff --git a/src/posit/connect/paginator.py b/src/posit/connect/paginator.py index e0271065..ece8a6f0 100644 --- a/src/posit/connect/paginator.py +++ b/src/posit/connect/paginator.py @@ -13,7 +13,8 @@ class Page: """ Represents a page of results returned by the paginator. - Attributes: + Attributes + ---------- current_page (int): The current page number. total (int): The total number of results. results (List[dict]): The list of results on the current page. @@ -32,7 +33,8 @@ class Paginator: session (requests.Session): The session object to use for making API requests. url (str): The URL of the paginated API endpoint. - Attributes: + Attributes + ---------- session (requests.Session): The session object to use for making API requests. url (str): The URL of the paginated API endpoint. """ @@ -46,7 +48,8 @@ def fetch_results(self) -> List[dict]: """ Fetches and returns all the results from the paginated API endpoint. - Returns: + Returns + ------- A list of dictionaries representing the fetched results. """ results = [] @@ -58,7 +61,8 @@ def fetch_pages(self) -> Generator[Page, None, None]: """ Fetches pages of results from the API. - Yields: + Yields + ------ Page: A page of results from the API. """ count = 0 @@ -87,7 +91,8 @@ def fetch_page(self, page_number: int) -> Page: Args: page_number (int): The page number to fetch. - Returns: + Returns + ------- Page: The fetched page object. """ diff --git a/src/posit/connect/urls.py b/src/posit/connect/urls.py index 68a16f96..4d177462 100644 --- a/src/posit/connect/urls.py +++ b/src/posit/connect/urls.py @@ -12,7 +12,8 @@ def server_to_api_url(url: str) -> str: Args: url (str): The URL to fix. - Returns: + Returns + ------- str: The fixed URL. """ url = url.rstrip("/") @@ -28,10 +29,12 @@ def validate(url: str) -> None: Args: url (str): The URL to be validated. - Returns: + Returns + ------- bool: True if the URL is valid, False otherwise. - Raises: + Raises + ------ ValueError: If the URL is missing a scheme or is not absolute. """ split = urlsplit(url, allow_fragments=False) @@ -54,7 +57,8 @@ def append_path(url: str, path: str) -> str: url (str): The original URL. path (str): The path to append. - Returns: + Returns + ------- str: The modified URL with the appended path. """ # See https://docs.python.org/3/library/urllib.parse.html#urllib.parse.urlsplit diff --git a/src/posit/connect/users.py b/src/posit/connect/users.py index ec290451..a35ce502 100644 --- a/src/posit/connect/users.py +++ b/src/posit/connect/users.py @@ -66,7 +66,8 @@ def lock(self, *, force: bool = False): Args: force (bool, optional): If set to True, overrides lock protection allowing a user to lock their own account. Defaults to False. - Returns: + Returns + ------- None """ _me = me.get(self.config, self.session) @@ -83,7 +84,8 @@ def unlock(self): """ Unlocks the user account. - Returns: + Returns + ------- None """ url = urls.append_path(self.config.url, f"v1/users/{self.guid}/lock") @@ -110,7 +112,8 @@ def update( last_name (str): The last name for the user. user_role (str): The role for the user. - Returns: + Returns + ------- None """ ... @@ -124,7 +127,8 @@ def update(self, *args, **kwargs) -> None: *args **kwargs - Returns: + Returns + ------- None """ ... @@ -137,7 +141,8 @@ def update(self, *args, **kwargs) -> None: *args **kwargs - Returns: + Returns + ------- None """ body = dict(*args, **kwargs) diff --git a/tests/posit/connect/api.py b/tests/posit/connect/api.py index ad89713b..0c2239a4 100644 --- a/tests/posit/connect/api.py +++ b/tests/posit/connect/api.py @@ -25,7 +25,11 @@ def load_mock(path: str) -> dict: Examples -------- - >>> data = load_mock("v1/example.json") - >>> data = load_mock("v1/example.jsonc") + >>> data = load_mock( + ... "v1/example.json" + ... ) + >>> data = load_mock( + ... "v1/example.jsonc" + ... ) """ return json.loads((Path(__file__).parent / "__api__" / path).read_text())