"""Models representing objects in GitHub Checks API."""
from functools import partial
from typing import List, Optional
import attr
__all__ = ('NewCheckRequest', 'UpdateCheckRequest', 'to_gh_query')
str_attrib = partial( # pylint: disable=invalid-name
attr.ib,
converter=lambda s: str(s) if s is not None else '',
)
int_attrib = partial(attr.ib, converter=int) # pylint: disable=invalid-name
optional_attrib = partial( # pylint: disable=invalid-name
attr.ib,
default=None,
)
optional_int_attrib = partial( # pylint: disable=invalid-name
optional_attrib,
validator=attr.validators.optional(lambda *_: int(_[-1])),
)
optional_str_attrib = partial( # pylint: disable=invalid-name
optional_attrib,
validator=attr.validators.optional(lambda *_: str(_[-1])),
)
optional_list_attrib = partial( # pylint: disable=invalid-name
attr.ib,
default=[],
validator=attr.validators.optional(lambda *_: list(_[-1])),
)
def optional_converter(kwargs_dict, convert_to_cls):
"""Instantiate a class instances from dict."""
if kwargs_dict is not None and not isinstance(kwargs_dict, convert_to_cls):
return convert_to_cls(**kwargs_dict)
return kwargs_dict
def optional_list_converter(args_list, convert_to_cls):
"""Convert list items to class instances."""
if args_list is not None and isinstance(args_list, list):
return [
optional_converter(kwargs_dict, convert_to_cls)
for kwargs_dict in args_list
]
return args_list
@attr.dataclass
class CheckAnnotation: # pylint: disable=too-few-public-methods
"""Checks API annotation struct."""
path: str = str_attrib()
start_line: int = int_attrib()
end_line: int = int_attrib()
annotation_level: str = str_attrib(
validator=attr.validators.in_(
(
'notice',
'warning',
'failure',
),
),
)
message: str = str_attrib()
start_column: Optional[int] = optional_int_attrib()
end_column: Optional[int] = optional_int_attrib()
title: Optional[str] = optional_str_attrib()
raw_details: Optional[str] = optional_str_attrib()
@attr.dataclass
class CheckImage: # pylint: disable=too-few-public-methods
"""Checks API image struct."""
alt: str = str_attrib()
image_url: str = str_attrib()
caption: Optional[str] = optional_str_attrib()
@attr.dataclass
class CheckActions:
"""Checks API actions struct."""
label: str = str_attrib()
description: str = str_attrib()
identifier: str = str_attrib()
@label.validator
def label_up_to_20(self, attribute, value):
"""Ensure that label is under 20."""
if len(value) > 20:
raise ValueError(
f'`{attribute.name}` must not exceed 20 characters.',
)
@description.validator
def description_up_to_40(self, attribute, value):
"""Ensure that description is under 40."""
if len(value) > 40:
raise ValueError(
f'`{attribute.name}` must not exceed 40 characters.',
)
@identifier.validator
def identifier_up_to_20(self, attribute, value):
"""Ensure that identifier is under 20."""
if len(value) > 20:
raise ValueError(
f'`{attribute.name}` must not exceed 20 characters.',
)
@attr.dataclass
class CheckOutput: # pylint: disable=too-few-public-methods
"""Checks API output struct."""
title: str = str_attrib()
summary: str = str_attrib()
text: str = str_attrib(default='')
annotations: List[CheckAnnotation] = optional_list_attrib(
converter=partial(
optional_list_converter,
convert_to_cls=CheckAnnotation,
),
)
images: List[CheckImage] = optional_list_attrib(
converter=partial(optional_list_converter, convert_to_cls=CheckImage),
)
@attr.dataclass
class BaseCheckRequestMixin:
"""Checks API base check request mixin."""
name: str = str_attrib()
details_url: Optional[str] = optional_str_attrib()
external_id: Optional[str] = optional_str_attrib()
status: Optional[str] = attr.ib(
default='queued',
validator=attr.validators.optional( # type: ignore[arg-type]
attr.validators.in_(
(
'queued',
'in_progress',
'completed',
),
),
),
)
# '2018-05-27T14:30:33Z', datetime.isoformat():
started_at: Optional[str] = optional_str_attrib()
conclusion: Optional[str] = attr.ib(
# [required] if 'status' is set to 'completed',
# should be missing if it's unset
default=None,
validator=attr.validators.optional(
attr.validators.in_(
(
'success',
'failure',
'neutral',
'cancelled',
'timed_out',
'action_required',
),
),
),
)
# [required] if 'conclusion' is set # '2018-05-27T14:30:33Z':
completed_at: Optional[str] = optional_str_attrib()
output: Optional[CheckOutput] = optional_attrib(
converter=partial(optional_converter, convert_to_cls=CheckOutput),
)
actions: List[CheckActions] = optional_list_attrib(
converter=partial(
optional_list_converter,
convert_to_cls=CheckActions,
),
)
@conclusion.validator
def depends_on_status(self, attribute, value):
"""Ensure that conclusion is present if there's status."""
if self.status == 'completed' and not value:
raise ValueError(
f'`{attribute.name}` must be provided if status is completed',
)
@completed_at.validator
def depends_on_conclusion(self, attribute, value):
"""Ensure that completed is present if there's conclusion."""
if self.conclusion is not None and not value:
raise ValueError(
f'`{attribute.name}` must be provided '
'if conclusion is present',
)
@actions.validator
def actions_up_to_3(self, attribute, value):
"""Ensure that the number of actions is below 3."""
if value is not None and len(value) > 3:
raise ValueError(f'`{attribute.name}` must not exceed 3 items.')
@attr.dataclass
class NewCheckRequestMixin: # pylint: disable=too-few-public-methods
"""Checks API new check request mixin."""
head_branch: str = str_attrib()
head_sha: str = str_attrib()
[docs]@attr.dataclass
class NewCheckRequest(NewCheckRequestMixin, BaseCheckRequestMixin):
"""Checks API new check request."""
[docs]@attr.dataclass
class UpdateCheckRequest(BaseCheckRequestMixin):
"""Checks API update check request."""
def conditional_to_gh_query(req):
"""Traverse Checks API request structure."""
if hasattr(req, '__attrs_attrs__'):
return to_gh_query(req)
if isinstance(req, list):
return list(map(conditional_to_gh_query, req))
if isinstance(req, dict):
return {
k: conditional_to_gh_query(v) for k, v in req.items()
if v is not None or (isinstance(v, (list, dict)) and not v)
}
return req
[docs]def to_gh_query(req):
"""Convert Checks API request object into a dict."""
return {
k: conditional_to_gh_query(v) # recursive if dataclass or list
for k, v in attr.asdict(req).items()
if v is not None or (isinstance(v, (list, dict)) and not v)
}