diff --git a/.vscode/settings.json b/.vscode/settings.json index 4169db45..6fbbf1ba 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,7 +4,4 @@ ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, - "cSpell.words": [ - "mypy" - ] } diff --git a/docs/_quarto.yml b/docs/_quarto.yml index a0555602..4cacdfce 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -104,6 +104,7 @@ quartodoc: - connect.permissions - connect.tasks - connect.users + - connect.vanities - title: Posit Connect Metrics package: posit contents: diff --git a/src/posit/connect/vanities.py b/src/posit/connect/vanities.py index 55474186..a21afcec 100644 --- a/src/posit/connect/vanities.py +++ b/src/posit/connect/vanities.py @@ -8,34 +8,103 @@ class Vanity(Resource): - """Represents a Vanity resource with the ability to destroy itself.""" + """A vanity resource. + + Vanities maintain custom URL paths assigned to content. + + Warnings + -------- + Vanity paths may only contain alphanumeric characters, hyphens, underscores, and slashes. + + Vanities cannot have children. For example, if the vanity path "/finance/" exists, the vanity path "/finance/budget/" cannot. But, if "/finance" does not exist, both "/finance/budget/" and "/finance/report" are allowed. + + The following vanities are reserved by Connect: + - `/__` + - `/favicon.ico` + - `/connect` + - `/apps` + - `/users` + - `/groups` + - `/setpassword` + - `/user-completion` + - `/confirm` + - `/recent` + - `/reports` + - `/plots` + - `/unpublished` + - `/settings` + - `/metrics` + - `/tokens` + - `/help` + - `/login` + - `/welcome` + - `/register` + - `/resetpassword` + - `/content` + """ + + _fuid: str = "content_guid" + """str : the foreign unique identifier field that points to the owner of this vanity, default is 'content_guid'""" def __init__( self, /, params: ResourceParameters, *, - after_destroy: AfterDestroyCallback = lambda: None, + after_destroy: Optional[AfterDestroyCallback] = None, **kwargs, ): + """Initialize a Vanity. + + Parameters + ---------- + params : ResourceParameters + after_destroy : AfterDestroyCallback, optional + Called after the Vanity is successfully destroyed, by default None + """ super().__init__(params, **kwargs) self._after_destroy = after_destroy def destroy(self) -> None: - """Destroy the vanity resource.""" + """Destroy the vanity. + + Raises + ------ + ValueError + If the foreign unique identifier is missing or its value is `None`. + + Warnings + -------- + This operation is irreversible. + + Note + ---- + This action requires administrator privileges. + """ fuid = self.get("content_guid") if fuid is None: raise ValueError("Missing value for required field: 'content_guid'.") endpoint = self.params.url + f"v1/content/{fuid}/vanity" self.params.session.delete(endpoint) - self._after_destroy() + + if self._after_destroy: + self._after_destroy() class Vanities(Resources): - """Manages a collection of Vanity resources.""" + """Manages a collection of vanities.""" def all(self) -> List[Vanity]: - """Retrieve all vanity resources.""" + """Retrieve all vanities. + + Returns + ------- + List[Vanity] + + Notes + ----- + This action requires administrator privileges. + """ endpoint = self.params.url + "v1/vanities" response = self.params.session.get(endpoint) results = response.json() @@ -43,7 +112,10 @@ def all(self) -> List[Vanity]: class VanityMixin(Resource): - """Mixin class to add vanity management capabilities to a resource.""" + """Mixin class to add a vanity attribute to a resource.""" + + _uid: str = "guid" + """str : the unique identifier field for this resource""" def __init__(self, /, params: ResourceParameters, **kwargs): super().__init__(params, **kwargs) @@ -62,7 +134,7 @@ def vanity(self) -> Optional[Vanity]: endpoint = self.params.url + f"v1/content/{uid}/vanity" response = self.params.session.get(endpoint) result = response.json() - self._vanity = Vanity(self.params, after_destroy=self.reset, **result) + self._vanity = Vanity(self.params, after_destroy=self.reset_vanity, **result) return self._vanity except ClientError as e: if e.http_status == 404: @@ -71,37 +143,99 @@ def vanity(self) -> Optional[Vanity]: @vanity.setter def vanity(self, value: Union[str, dict]) -> None: - """Set the vanity using a path or dictionary of attributes.""" + """Set the vanity. + + Parameters + ---------- + value : str or dict + The value can be a string or a dictionary. If provided as a string, it represents the vanity path. If provided as a dictionary, it contains key-value pairs with detailed information about the object. + """ if isinstance(value, str): self.set_vanity(path=value) elif isinstance(value, dict): self.set_vanity(**value) - self.reset() + self.reset_vanity() @vanity.deleter def vanity(self) -> None: - """Delete the vanity resource.""" + """Destroy the vanity. + + Warnings + -------- + This operation is irreversible. + + Note + ---- + This action requires administrator privileges. + + See Also + -------- + reset_vanity + """ if self._vanity: self._vanity.destroy() - self.reset() + self.reset_vanity() - def reset(self) -> None: - """Reset the cached vanity resource.""" + def reset_vanity(self) -> None: + """Unload the cached vanity. + + Forces the next access, if any, to query the vanity from the Connect server. + """ self._vanity = None @overload - def set_vanity(self, *, path: str) -> None: ... + def set_vanity(self, *, path: str) -> None: + """Set the vanity. + + Parameters + ---------- + path : str + The vanity path. + + Raises + ------ + ValueError + If the unique identifier field is missing or the value is None. + """ + ... @overload - def set_vanity(self, *, path: str, force: bool) -> None: ... + def set_vanity(self, *, path: str, force: bool) -> None: + """Set the vanity. + + Parameters + ---------- + path : str + The vanity path. + force : bool + If `True`, overwrite the ownership of this vanity to this resource, default `False` + + Raises + ------ + ValueError + If the unique identifier field is missing or the value is None. + """ + ... @overload - def set_vanity(self, **attributes) -> None: ... + def set_vanity(self, **attributes) -> None: + """Set the vanity. + + Parameters + ---------- + **attributes : dict, optional + Arbitrary vanity attributes. All attributes are passed as the request body to POST 'v1/content/:guid/vanity' + + Possible keys may include: + - `path` : str + - `force` : bool + """ + ... def set_vanity(self, **attributes) -> None: - """Set or update the vanity resource with given attributes.""" - uid = self.get("guid") - if uid is None: - raise ValueError("Missing value for required field: 'guid'.") - endpoint = self.params.url + f"v1/content/{uid}/vanity" + """Set the vanity.""" + v = self.get(self._uid) + if v is None: + raise ValueError(f"Missing value for required field: '{self._uid}'.") + endpoint = self.params.url + f"v1/content/{v}/vanity" self.params.session.put(endpoint, json=attributes)