# -*- coding: utf-8 -*-
"""
See :class:`DetailItem`.
"""
import typing as T
import dataclasses
import contextlib
import zelfred.api as zf
try:
import pyperclip
except ImportError: # pragma: no cover
pass
from ..terminal import ShortcutEnum, format_key_value
from ..compat import TypedDict
from .base_item import BaseArsItem, T_ARS_ITEM
from .exception_item import ExceptionItem
if T.TYPE_CHECKING: # pragma: no cover
from ..documents.resource_document import ResourceDocument
from ..ars_def import ARS
[docs]class T_DETAIL_ITEM_VARIABLES(TypedDict):
"""
.. note::
Copy is more common than url in detail item.
"""
copy: T.Optional[str]
url: T.Optional[str]
[docs]@dataclasses.dataclass
class DetailItem(BaseArsItem):
"""
Represent a detail information of an AWS resource in the dropdown menu.
**Why this class**
1. We need to write logic to create ``Item`` in the
``aws_resource_search.res.${aws_service}.py`` source code.
This class provides utility methods to make the code clean and descriptive.
2. An ``Item`` usually comes with user action. We need to store the
user action argument in ``variables`` and then implement the
``xyz_handler`` function to handle the user action. In this project,
we have a very clear mind of user action patterns. This class provides
utility methods to enable pre-defined user action smartly based
on the input, so we no longer need to implement ``variables`` logics
and ``xyz_handler`` functions in most cases.
"""
variables: T_DETAIL_ITEM_VARIABLES = dataclasses.field(default_factory=dict)
[docs] @classmethod
def new(
cls,
title: str,
subtitle: T.Optional[str] = None,
copy: T.Optional[str] = None,
url: T.Optional[str] = None,
uid: T.Optional[str] = None,
arg: T.Optional[str] = None,
autocomplete: T.Optional[str] = None,
):
"""
The factory method to create a new ``ArsBaseItem`` instance.
:param title: first line of the item. It has a checkbox in front of it to
indicate whether it is selected.
:param subtitle: second line of the item.
:param copy: the text to copy to clipboard.
:param url: the url to open in browser.
:param uid: item unique id. The UI use this to distinguish different items.
:param arg: argument that will be passed to the action.
:param autocomplete: the text that will be filled in the input box when
user hits ``TAB`` key.
"""
kwargs = dict(
title=title,
arg=arg,
autocomplete=autocomplete,
variables={"url": url, "copy": copy},
)
if uid:
kwargs["uid"] = uid
# fmt: off
if subtitle is None:
if url is not None and copy is not None:
kwargs[
"subtitle"] = f"🌐 {ShortcutEnum.ENTER} to open url, 📋 {ShortcutEnum.CTRL_A} to copy value, 🔗 {ShortcutEnum.CTRL_U} to copy url."
elif url is not None:
kwargs[
"subtitle"] = f"🌐 {ShortcutEnum.ENTER} to open url, 📋 {ShortcutEnum.CTRL_A} or 🔗 {ShortcutEnum.CTRL_U} to copy url."
elif copy is not None:
kwargs["subtitle"] = f"📋 {ShortcutEnum.CTRL_A} or 🔗 {ShortcutEnum.CTRL_U} to copy value"
elif autocomplete:
kwargs["subtitle"] = f"{ShortcutEnum.TAB} to auto complete."
else:
kwargs["subtitle"] = "No subtitle"
# fmt: on
else:
kwargs["subtitle"] = subtitle
return cls(**kwargs)
[docs] def enter_handler(self, ui: "zf.UI"): # pragma: no cover
"""
Behavior:
- If we have a URL, it will be opened in the browser or print the url
to the terminal if the terminal device can not do so.
- If not, the Enter key will function similarly to the Tab key,
it will autocomplete.
"""
if self.variables.get("url"):
self.open_url_or_print(ui, self.variables["url"])
elif self.autocomplete:
ui.line_editor.clear_line()
ui.line_editor.enter_text(self.autocomplete)
else:
pass
[docs] def ctrl_a_handler(self, ui: "zf.UI"): # pragma: no cover
"""
Behavior:
- If we have a copy text, it will copy the text to clipboard or print the text
to the terminal if the terminal device can not do so.
- If we don't have copy text but have url, then treat url as copy text.
"""
if self.variables.get("copy"):
self.copy_or_print(ui, self.variables["copy"])
elif self.variables.get("url"):
self.copy_or_print(ui, self.variables["url"])
else:
pass
[docs] def ctrl_u_handler(self, ui: "zf.UI"): # pragma: no cover
"""
Behavior:
- If we have an url, it will copy the url to clipboard or print the url
to the terminal if the terminal device can not do so.
- If we don't have URL but have copy text, then treat copy text as url.
"""
if self.variables.get("url"):
self.copy_or_print(ui, self.variables["url"])
elif self.variables.get("copy"):
self.copy_or_print(ui, self.variables["copy"])
else:
pass
[docs] @classmethod
def from_detail(
cls,
key: str,
value: T.Any,
key_text: T.Optional[str] = None,
value_text: T.Optional[str] = None,
url: T.Optional[str] = None,
copy: T.Optional[str] = None,
uid: T.Optional[str] = None,
):
"""
A utility method to create :class`DetailItem` from structured detail information.
This method will be used in
:class:`aws_resource_search.searcher.base_document.BaseDocument.get_details`
method.
A 'detail' is just a simple key value pair. For example, for S3 bucket.
the 'bucket_name' is the key and the 'your-bucket-name' is the value.
:param key: key of the detail, it's the original data
:param value: value of the detail, it's the original data. This is also
the text that will be copied to clipboard when user tap `Ctrl + A``.
:param key_text: the ``${key} = ${value}`` text to display in the UI.
if not provided, use ``key`` as the text.
:param value_text: the ``${key} = ${value}`` text to display in the UI.
if not provided, use ``value`` as the text.
:param url: if specified, user can hit Enter key to open the url in browser,
and Ctrl + U to copy it.
:param uid: this is for uid
"""
if key_text is None:
key_text = key
if value_text is None:
value_text = value
if copy is None:
copy = value
return cls.new(
title=format_key_value(key_text, value_text),
uid=uid,
copy=str(copy),
url=url,
)
[docs] @classmethod
def from_env_vars(
cls,
env_vars: T.Dict[str, str],
url: T.Optional[str] = None,
) -> T.List["DetailItem"]:
"""
A utility method to create many :class`DetailItem` from
environment variable key value pairs.
"""
if env_vars:
return [
cls.new(
title=f"🎯 env var: {format_key_value(k, v)}",
uid=f"env_var-{k}",
copy=v,
url=url,
)
for k, v in env_vars.items()
]
else:
return [
cls.new(
title=f"🏷 env var: 🔴 No environment variable found.",
uid=f"no-environment-variable-found",
url=url,
)
]
[docs] @classmethod
def get_initial_detail_items(
cls,
doc: "ResourceDocument",
ars: "ARS",
arn_key: str = "arn",
) -> T.List["DetailItem"]: # pragma: no cover
"""
Most AWS resource detail should have one ARN item that user can tap
"Ctrl A" to copy and tap "Enter" to open url. Only a few AWS resource
doesn't support ARN (for example, glue job run).
.. note::
This method is to simplify the authoring of the
:meth:`aws_resource_search.documents.resource_document.ResourceDocument.get_details` method.
Usage example:
>>> class S3BucketDocument(ResourceDocument):
... def get_details(self, ars: ARS):
... detail_items = DetailItem.get_initial_detail_items(self, ars)
... ...
"""
try:
return [
DetailItem.from_detail(
key=arn_key,
value=doc.arn,
url=doc.get_console_url(ars.aws_console),
),
]
# the ResourceDocument.arn and ResourceDocument.get_console_url
# may raise NotImplementedError
except NotImplementedError:
return []
[docs] @staticmethod
@contextlib.contextmanager
def error_handling(detail_items: T.List["T_ARS_ITEM"]):
"""
A context manager to add additional detail items to the list.
It automatically captures exception and creates
:class:`~aws_resource_search.items.exception_item.ExceptionItem`
to explain what went wrong.
Usage example:
>>> class S3BucketDocument(ResourceDocument):
... def get_details(self, ars: ARS):
... detail_items = DetailItem.get_initial_detail_items(self, ars)
... with DetailItem.error_handling(detail_items):
... res = ars.bsm.s3_client.get_bucket_policy(...)
... detail_items.append(DetailItem.from_detail(...))
.. note::
This method is to simplify the authoring of the
:meth:`aws_resource_search.documents.resource_document.ResourceDocument.get_details` method.
"""
try:
yield None
except Exception as e:
detail_items.append(ExceptionItem.from_error(error=e))