Пример #1
0
class AnalysisSubmissionResponse(BaseResponse):
    """The API response domain model for a successful analysis job submision."""

    with open(resolve_schema(__file__, "analysis-submission.json")) as sf:
        schema = json.load(sf)

    def __init__(self, analysis: Analysis):
        self.analysis = analysis

    @classmethod
    def from_dict(cls, d):
        """Create the response domain model from a dict.

        This also validates the dict's schema and raises a :code:`ValidationError`
        if any required keys are missing or the data is malformed.

        :param d: The dict to deserialize from
        :return: The domain model with the data from :code:`d` filled in
        """
        cls.validate(d)
        return cls(analysis=Analysis.from_dict(d))

    def to_dict(self):
        """Serialize the reponse model to a Python dict.

        :return: A dict holding the request model data
        """
        d = self.analysis.to_dict()
        self.validate(d)
        return d

    def __getattr__(self, name):
        return getattr(self.analysis, name)
Пример #2
0
class VersionResponse(BaseResponse):
    """The API response domain model for an API version request."""

    with open(resolve_schema(__file__, "version.json")) as sf:
        schema = json.load(sf)

    def __init__(
        self,
        api_version: str,
        maru_version: str,
        mythril_version: str,
        harvey_version: str,
        hashed_version: str,
    ):
        self.api_version = api_version
        self.maru_version = maru_version
        self.mythril_version = mythril_version
        self.harvey_version = harvey_version
        self.hashed_version = hashed_version

    @classmethod
    def from_dict(cls, d: Dict):
        """Create the response domain model from a dict.

        This also validates the dict's schema and raises a :code:`ValidationError`
        if any required keys are missing or the data is malformed.

        :param d: The dict to deserialize from
        :return: The domain model with the data from :code:`d` filled in
        """
        cls.validate(d)
        return cls(
            api_version=d["api"],
            maru_version=d["maru"],
            mythril_version=d["mythril"],
            harvey_version=d["harvey"],
            hashed_version=d["hash"],
        )

    def to_dict(self):
        """Serialize the reponse model to a Python dict.

        :return: A dict holding the request model data
        """
        d = {
            "api": self.api_version,
            "maru": self.maru_version,
            "mythril": self.mythril_version,
            "harvey": self.harvey_version,
            "hash": self.hashed_version,
        }
        self.validate(d)
        return d
Пример #3
0
class OASResponse(BaseResponse):
    """The API response domain model for an OpenAPI request."""

    with open(resolve_schema(__file__, "openapi.json")) as sf:
        schema = json.load(sf)

    def __init__(self, data: str):
        if not type(data) == str:
            raise TypeError("Expected data type str but got {}".format(
                type(data)))
        self.data = data

    @classmethod
    def from_dict(cls, d: Dict) -> "OASResponse":
        """Create the response domain model from a dict.

        This also validates the dict's schema and raises a :code:`ValidationError`
        if any required keys are missing or the data is malformed.

        :param d: The dict to deserialize from
        :return: The domain model with the data from :code:`d` filled in
        """
        cls.validate(d)
        return cls(data=d["data"])

    @classmethod
    def from_json(cls, json_str: str) -> "OASResponse":
        """

        :param json_str: The string to decode from
        :return: An OASResponse instance
        """
        # overwrite from base response because the API doesn't actually deliver
        # JSON but raw YAML/HTML
        return cls.from_dict({"data": json_str})

    def to_dict(self) -> Dict:
        """Serialize the response model to a Python dict.

        :return: A dict holding the request model data
        """
        d = {"data": self.data}
        return d

    def __eq__(self, other: "OASResponse") -> bool:
        return self.data == other.data
Пример #4
0
class AuthRefreshResponse(BaseResponse):
    """The API response domain model for a successful authentication
    refresh."""

    with open(resolve_schema(__file__, "auth.json")) as sf:
        schema = json.load(sf)

    def __init__(self, access_token: str, refresh_token: str):
        self.access_token = access_token
        self.refresh_token = refresh_token

    @classmethod
    def from_dict(cls, d: Dict) -> "AuthRefreshResponse":
        """Create the response domain model from a dict.

        This also validates the dict's schema and raises a :code:`ValidationError`
        if any required keys are missing or the data is malformed.

        :param d: The dict to deserialize from
        :return: The domain model with the data from :code:`d` filled in
        """
        cls.validate(d)
        return cls(
            access_token=d["jwtTokens"]["access"],
            refresh_token=d["jwtTokens"]["refresh"],
        )

    def to_dict(self) -> Dict:
        """Serialize the response model to a Python dict.

        :return: A dict holding the request model data
        """
        d = {"jwtTokens": {"access": self.access_token, "refresh": self.refresh_token}}
        self.validate(d)
        return d

    def __eq__(self, other: "AuthRefreshResponse") -> bool:
        return all(
            (
                self.access_token == other.access_token,
                self.refresh_token == other.refresh_token,
            )
        )
Пример #5
0
class GroupStatusResponse(BaseResponse):
    """The API response domain model for a fetching the status of a group."""

    with open(resolve_schema(__file__, "group-status.json")) as sf:
        schema = json.load(sf)

    def __init__(self, group: Group):
        self.group = group

    @classmethod
    def from_dict(cls, d) -> "GroupStatusResponse":
        """Create the response domain model from a dict.

        This also validates the dict's schema and raises a :code:`ValidationError`
        if any required keys are missing or the data is malformed.

        :param d: The dict to deserialize from
        :return: The domain model with the data from :code:`d` filled in
        """
        cls.validate(d)
        return cls(group=Group.from_dict(d))

    def to_dict(self) -> Dict:
        """Serialize the response model to a Python dict.

        :return: A dict holding the request model data
        """
        d = self.group.to_dict()
        self.validate(d)
        return d

    def __getattr__(self, name: str) -> Any:
        return getattr(self.group, name)

    def __eq__(self, other: "GroupStatusResponse") -> bool:
        return self.group == other.group
Пример #6
0
class AuthLogoutRequest(BaseRequest):
    """Perform an API request that logs out the current user."""

    with open(resolve_schema(__file__, "auth-logout.json")) as sf:
        schema = json.load(sf)

    def __init__(self, global_: bool = False):
        self.global_ = global_

    @property
    def endpoint(self):
        """The API's logout endpoint.

        :return: A string denoting the logout endpoint without the host prefix
        """
        return "v1/auth/logout"

    @property
    def method(self):
        """The HTTP method to perform.

        :return: The uppercase HTTP method, e.g. "POST"
        """
        return "POST"

    @property
    def parameters(self):
        """Additional URL parameters

        :return: A dict (str -> str) instance mapping parameter name to parameter content
        """
        return {}

    @property
    def headers(self):
        """Additional request headers.

        :return: A dict (str -> str) instance mapping header name to header content
        """
        return {}

    @property
    def payload(self):
        """The request's payload data.

        :return: A Python dict to be serialized into JSON format and submitted to the endpoint.
        """
        return {}

    @classmethod
    def from_dict(cls, d: Dict):
        """Create the request domain model from a dict.

        This also validates the dict's schema and raises a :code:`ValidationError`
        if any required keys are missing or the data is malformed.

        :param d: The dict to deserialize from
        :return: The domain model with the data from :code:`d` filled in
        """
        cls.validate(d)
        return cls(global_=d["global"])

    def to_dict(self):
        """Serialize the request model to a Python dict.

        :return: A dict holding the request model data
        """
        d = {"global": self.global_}
        self.validate(d)
        return d
Пример #7
0
class DetectedIssuesRequest(AnalysisStatusRequest):
    """Perform an API request that lists the detected issues of a finished analysis job."""

    with open(resolve_schema(__file__, "detected-issues.json")) as sf:
        schema = json.load(sf)

    def __init__(self, uuid: str):
        super().__init__(uuid)
        self.uuid = uuid

    @property
    def endpoint(self):
        """The API's analysis issue report endpoint.

        :return: A string denoting the issue report endpoint without the host prefix
        """
        return "v1/analyses/{}/issues".format(self.uuid)

    @property
    def method(self):
        """The HTTP method to perform.

        :return: The uppercase HTTP method, e.g. "POST"
        """
        return "GET"

    @property
    def parameters(self):
        """Additional URL parameters

        :return: A dict (str -> str) instance mapping parameter name to parameter content
        """
        return {}

    @property
    def headers(self):
        """Additional request headers.

        :return: A dict (str -> str) instance mapping header name to header content
        """
        return {}

    @property
    def payload(self):
        """The request's payload data.

        :return: A Python dict to be serialized into JSON format and submitted to the endpoint.
        """
        return {}

    @classmethod
    def from_dict(cls, d: Dict):
        """Create the request domain model from a dict.

        This also validates the dict's schema and raises a :code:`ValidationError`
        if any required keys are missing or the data is malformed.

        :param d: The dict to deserialize from
        :return: The domain model with the data from :code:`d` filled in
        """
        cls.validate(d)
        return cls(uuid=d["uuid"])

    def to_dict(self):
        """Serialize the request model to a Python dict.

        :return: A dict holding the request model data
        """
        d = {"uuid": self.uuid}
        self.validate(d)
        return d
Пример #8
0
class AuthRefreshRequest(BaseRequest):
    """Perform an API request that refreshes the logged-in user's access token."""

    with open(resolve_schema(__file__, "auth-refresh.json")) as sf:
        schema = json.load(sf)

    def __init__(self, access_token: str, refresh_token: str):
        self.access_token = access_token
        self.refresh_token = refresh_token

    @property
    def endpoint(self):
        """The API's auth refresh endpoint.

        :return: A string denoting the refresh endpoint without the host prefix
        """
        return "v1/auth/refresh"

    @property
    def method(self):
        """The HTTP method to perform.

        :return: The uppercase HTTP method, e.g. "POST"
        """
        return "POST"

    @property
    def parameters(self):
        """Additional URL parameters

        :return: A dict (str -> str) instance mapping parameter name to parameter content
        """
        return {}

    @property
    def headers(self):
        """Additional request headers.

        :return: A dict (str -> str) instance mapping header name to header content
        """
        return {}

    @property
    def payload(self):
        """The request's payload data.

        :return: A Python dict to be serialized into JSON format and submitted to the endpoint.
        """
        return {
            "jwtTokens": {
                "access": self.access_token,
                "refresh": self.refresh_token
            }
        }

    @classmethod
    def from_dict(cls, d: Dict):
        """Create the request domain model from a dict.

        This also validates the dict's schema and raises a :code:`ValidationError`
        if any required keys are missing or the data is malformed.

        :param d: The dict to deserialize from
        :return: The domain model with the data from :code:`d` filled in
        """
        cls.validate(d)
        return cls(
            access_token=d["jwtTokens"]["access"],
            refresh_token=d["jwtTokens"]["refresh"],
        )

    def to_dict(self):
        """Serialize the request model to a Python dict.

        :return: A dict holding the request model data
        """
        d = {
            "jwtTokens": {
                "access": self.access_token,
                "refresh": self.refresh_token
            }
        }
        self.validate(d)
        return d
Пример #9
0
class GroupCreationRequest(BaseRequest):
    """Perform an API request creates a new analysis group."""

    with open(resolve_schema(__file__, "group-creation.json")) as sf:
        schema = json.load(sf)

    def __init__(self, group_name: str = ""):
        self.group_name = group_name

    @property
    def endpoint(self) -> str:
        """The API's logout endpoint.

        :return: A string denoting the group endpoint without the host prefix
        """
        return "v1/analysis-groups"

    @property
    def method(self) -> str:
        """The HTTP method to perform.

        :return: The uppercase HTTP method, e.g. "POST"
        """
        return "POST"

    @property
    def parameters(self) -> Dict:
        """Additional URL parameters.

        :return: A dict (str -> str) instance mapping parameter name to parameter content
        """
        return {}

    @property
    def headers(self) -> Dict:
        """Additional request headers.

        :return: A dict (str -> str) instance mapping header name to header content
        """
        return {}

    @property
    def payload(self) -> Dict:
        """The request's payload data.

        :return: A Python dict to be serialized into JSON format and submitted to the endpoint.
        """
        return {"groupName": self.group_name}

    @classmethod
    def from_dict(cls, d: Dict) -> "GroupCreationRequest":
        """Create the request domain model from a dict.

        This also validates the dict's schema and raises a :code:`ValidationError`
        if any required keys are missing or the data is malformed.

        :param d: The dict to deserialize from
        :return: The domain model with the data from :code:`d` filled in
        """
        cls.validate(d)
        return cls(group_name=d["groupName"])

    def to_dict(self) -> Dict:
        """Serialize the request model to a Python dict.

        :return: A dict holding the request model data
        """
        d = {"groupName": self.group_name}
        self.validate(d)
        return d

    def __eq__(self, other: "GroupCreationRequest") -> bool:
        return self.group_name == other.group_name
Пример #10
0
class DetectedIssuesResponse(BaseResponse):
    """The API response domain model for a report of the detected issues."""

    with open(resolve_schema(__file__, "detected-issues.json")) as sf:
        schema = json.load(sf)

    def __init__(self, issue_reports: List[IssueReport]) -> None:
        self.issue_reports = issue_reports

    @classmethod
    def from_dict(cls, d: Dict) -> "DetectedIssuesResponse":
        """Create the response domain model from a dict.

        This also validates the dict's schema and raises a :code:`ValidationError`
        if any required keys are missing or the data is malformed.

        :param d: The List to deserialize from
        :return: The domain model with the data from :code:`d` filled in
        """

        if type(d) == list:
            cls.validate(d)
            d = {"issueReports": d}
        elif type(d) == dict:
            if d.get("issueReports") is None:
                raise ValidationError(
                    "Cannot create DetectedIssuesResponse object from invalid dictionary d: {}"
                    .format(d))

            cls.validate(d["issueReports"])
        else:
            raise ValidationError(
                "Expected list or dict but got {} of type {}".format(
                    d, type(d)))

        return cls(issue_reports=[
            IssueReport.from_dict(i) for i in d["issueReports"]
        ])

    def to_dict(self, as_list=False) -> Dict:
        """Serialize the response model to a Python dict.

        :return: A dict holding the request model data
        """
        d = {
            "issueReports":
            [report.to_dict() for report in self.issue_reports]
        }
        self.validate(d["issueReports"])
        return d["issueReports"] if as_list else d

    def to_json(self) -> str:
        """Serialize the model to JSON format.

        Internally, this method is using the :code:`to_dict` method.

        :return: A JSON string holding the model's data
        """
        return json.dumps([report.to_dict() for report in self.issue_reports])

    def __contains__(self, key: str) -> bool:
        """Check whether a specified SWC ID is contained in any of the
        reports."""
        if not type(key) == str:
            raise ValueError(
                "Expected SWC ID of type str but got {} of type {}".format(
                    key, type(key)))
        for report in self.issue_reports:
            if key in report:
                return True
        return False

    def __iter__(self) -> Iterator[Issue]:
        for report in self.issue_reports:
            for issue in report:
                yield issue

    def __len__(self) -> int:
        """Return the number of issues across all reports."""
        total_detected_issues = 0
        for report in self.issue_reports:
            total_detected_issues += len(report)
        return total_detected_issues

    def __getitem__(self, key: int) -> IssueReport:
        """Get an issue report at a specific index."""
        return self.issue_reports[key]

    def __setitem__(self, key: int, value: IssueReport) -> None:
        """Set an issue report at a specified index."""
        self.issue_reports[key] = value

    def __delitem__(self, key: int) -> None:
        """Delete an issue report at a specified index."""
        del self.issue_reports[key]

    def __eq__(self, other: "DetectedIssuesResponse") -> bool:
        return self.issue_reports == other.issue_reports
Пример #11
0
class AuthLoginRequest(BaseRequest):
    """Perform an API request that performs a login action with Ethereum address and password."""

    with open(resolve_schema(__file__, "auth-login.json")) as sf:
        schema = json.load(sf)

    def __init__(self, eth_address: str, password: str):
        self.eth_address = eth_address
        self.password = password

    @property
    def endpoint(self):
        """The API's login endpoint.

        :return: A string denoting the login endpoint without the host prefix
        """
        return "v1/auth/login"

    @property
    def method(self):
        """The HTTP method to perform.

        :return: The uppercase HTTP method, e.g. "POST"
        """
        return "POST"

    @property
    def parameters(self):
        """Additional URL parameters

        :return: A dict (str -> str) instance mapping parameter name to parameter content
        """
        return {}

    @property
    def headers(self):
        """Additional request headers.

        :return: A dict (str -> str) instance mapping header name to header content
        """
        return {}

    @property
    def payload(self):
        """The request's payload data.

        :return: A Python dict to be serialized into JSON format and submitted to the endpoint.
        """
        return self.to_dict()

    @classmethod
    def from_dict(cls, d: Dict[str, str]):
        """Create the request domain model from a dict.

        This also validates the dict's schema and raises a :code:`ValidationError`
        if any required keys are missing or the data is malformed.

        :param d: The dict to deserialize from
        :return: The domain model with the data from :code:`d` filled in
        """
        cls.validate(d)
        return cls(eth_address=d["ethAddress"], password=d["password"])

    def to_dict(self):
        """Serialize the request model to a Python dict.

        :return: A dict holding the request model data
        """
        d = {"ethAddress": self.eth_address, "password": self.password}
        self.validate(d)
        return d
class AnalysisSubmissionRequest(BaseRequest):
    """Perform an API analysis job submission as a logged in user."""

    with open(resolve_schema(__file__, "analysis-submission.json")) as sf:
        schema = json.load(sf)

    def __init__(
        self,
        contract_name: str = None,
        bytecode: str = None,
        source_map: str = None,
        deployed_bytecode: str = None,
        deployed_source_map: str = None,
        main_source: str = None,
        sources: Dict[str, Dict[str, str]] = None,
        source_list: List[str] = None,
        solc_version: str = None,
        analysis_mode: str = "quick",
    ):
        self.contract_name = contract_name
        self.bytecode = bytecode
        self.source_map = source_map
        self.deployed_bytecode = deployed_bytecode
        self.deployed_source_map = deployed_source_map
        self.main_source = main_source
        self.sources = sources
        self.source_list = source_list
        self.solc_version = solc_version
        # set alias for full mode for backwards compatibility - new modes are quick, standard, deep
        self.analysis_mode = self._get_analysis_mode(analysis_mode)

    @staticmethod
    def _get_analysis_mode(mode: str) -> str:
        return "standard" if mode == "full" else mode

    @property
    def endpoint(self) -> str:
        """The API's analysis submission endpoint.

        :return: A string denoting the submission endpoint without the host prefix
        """
        return "v1/analyses"

    @property
    def method(self) -> str:
        """The HTTP method to perform.

        :return: The uppercase HTTP method, e.g. "POST"
        """
        return "POST"

    @property
    def parameters(self) -> Dict:
        """Additional URL parameters.

        :return: A dict (str -> str) instance mapping parameter name to parameter content
        """
        return {}

    @property
    def headers(self) -> Dict:
        """Additional request headers.

        :return: A dict (str -> str) instance mapping header name to header content
        """
        return {}

    @property
    def payload(self) -> Dict:
        """The request's payload data.

        :return: A Python dict to be serialized into JSON format and submitted to the endpoint.
        """
        return {"data": self.to_dict()}

    @classmethod
    def from_dict(cls, d: Dict) -> "AnalysisSubmissionRequest":
        """Create the request domain model from a dict.

        This also validates the dict's schema and raises a :code:`ValidationError`
        if any required keys are missing or the data is malformed.

        :param d: The dict to deserialize from
        :return: The domain model with the data from :code:`d` filled in
        """
        if d.get("analysisMode"):
            d["analysisMode"] = cls._get_analysis_mode(d["analysisMode"])

        cls.validate(d)
        return cls(
            contract_name=d.get("contractName"),
            bytecode=d.get("bytecode"),
            source_map=d.get("sourceMap"),
            deployed_bytecode=d.get("deployedBytecode"),
            deployed_source_map=d.get("deployedSourceMap"),
            main_source=d.get("mainSource"),
            sources=d.get("sources"),
            source_list=d.get("sourceList"),
            solc_version=d.get("version"),
            analysis_mode=d.get("analysisMode"),
        )

    def to_dict(self) -> Dict:
        """Serialize the request model to a Python dict.

        :return: A dict holding the request model data
        """
        base_dict = dict_delete_none_fields({
            "contractName":
            self.contract_name,
            "bytecode":
            self.bytecode,
            "sourceMap":
            self.source_map,
            "deployedBytecode":
            self.deployed_bytecode,
            "deployedSourceMap":
            self.deployed_source_map,
            "mainSource":
            self.main_source,
            "sources":
            self.sources if self.sources else None,
            "sourceList":
            self.source_list if self.source_list else None,
            "version":
            self.solc_version,
            "analysisMode":
            self.analysis_mode,
        })
        self.validate(base_dict)
        return base_dict

    def __eq__(self, candidate) -> bool:
        """Perform an equality check on two Request domain models."""

        return all([
            self.contract_name == candidate.contract_name,
            self.bytecode == candidate.bytecode,
            self.source_map == candidate.source_map,
            self.deployed_bytecode == candidate.deployed_bytecode,
            self.deployed_source_map == candidate.deployed_source_map,
            self.main_source == candidate.main_source,
            self.sources == candidate.sources,
            self.source_list == candidate.source_list,
            self.solc_version == candidate.solc_version,
            self.analysis_mode == candidate.analysis_mode,
        ])
Пример #13
0
class AnalysisListResponse(BaseResponse):
    """The API response domain model for a list of analyses."""

    with open(resolve_schema(__file__, "analysis-list.json")) as sf:
        schema = json.load(sf)

    def __init__(self, analyses: List[Analysis], total: int) -> None:
        self.analyses = analyses
        self.total = total

    @classmethod
    def validate(cls, candidate):
        """Validate the response data structure and add an explicit type check.

        :param candidate: The Python dict to validate
        """
        super().validate(candidate)
        if not type(candidate) == dict:
            raise ValidationError(
                "Expected type dict but got {}".format(type(candidate))
            )

    @classmethod
    def from_dict(cls, d: dict):
        """Create the response domain model from a dict.

        This also validates the dict's schema and raises a :code:`ValidationError`
        if any required keys are missing or the data is malformed.

        :param d: The dict to deserialize from
        :return: The domain model with the data from :code:`d` filled in
        """
        cls.validate(d)
        analyses = [Analysis.from_dict(a) for a in d["analyses"]]
        return cls(analyses=analyses, total=d["total"])

    def to_dict(self):
        """Serialize the reponse model to a Python dict.

        :return: A dict holding the request model data
        """
        d = {
            "analyses": [a.to_dict() for a in self.analyses],
            "total": len(self.analyses),
        }
        self.validate(d)
        return d

    def __iter__(self):
        for analysis in self.analyses:
            yield analysis

    def __getitem__(self, idx):
        try:
            return self.analyses[idx]
        except IndexError:
            raise IndexError(INDEX_ERROR_MSG.format(idx))

    def __setitem__(self, idx, value):
        try:
            self.analyses[idx] = value
        except IndexError:
            raise IndexError(INDEX_ERROR_MSG.format(idx))

    def __delitem__(self, idx):
        try:
            del self.analyses[idx]
            self.total -= 1
        except IndexError:
            raise IndexError(INDEX_ERROR_MSG.format(idx))

    def __len__(self):
        return self.total

    def __reversed__(self):
        return reversed(self.analyses)

    def __contains__(self, item):
        if not type(item) in (Analysis, str):
            raise ValueError(
                "Expected type Analysis or str but got {}".format(type(item))
            )
        uuid = item.uuid if type(item) == Analysis else item
        return uuid in map(lambda x: x.uuid, self.analyses)

    def __eq__(self, candidate):
        return self.total == candidate.total and self.analyses == candidate.analyses
Пример #14
0
class GroupListResponse(BaseResponse):
    """The API response domain model for a list of analyses."""

    with open(resolve_schema(__file__, "group-list.json")) as sf:
        schema = json.load(sf)

    def __init__(self, groups: List[Group], total: int):
        self.groups = groups
        self.total = total

    @classmethod
    def from_dict(cls, d: Dict) -> "GroupListResponse":
        """Create the response domain model from a dict.

        This also validates the dict's schema and raises a :code:`ValidationError`
        if any required keys are missing or the data is malformed.

        :param d: The dict to deserialize from
        :return: The domain model with the data from :code:`d` filled in
        """
        cls.validate(d)
        groups = [Group.from_dict(a) for a in d["groups"]]
        return cls(groups=groups, total=d["total"])

    def to_dict(self) -> Dict:
        """Serialize the response model to a Python dict.

        :return: A dict holding the request model data
        """
        d = {
            "groups": [a.to_dict() for a in self.groups],
            "total": len(self.groups)
        }
        self.validate(d)
        return d

    def __iter__(self) -> Group:
        """Iterate over all the groups in the list."""
        for group in self.groups:
            yield group

    def __getitem__(self, idx: int) -> Group:
        """Get a group at a specific list index."""
        try:
            return self.groups[idx]
        except IndexError:
            raise IndexError(INDEX_ERROR_MSG.format(idx))

    def __setitem__(self, idx: int, value: Group) -> None:
        """Set a group at a specific list index."""
        try:
            self.groups[idx] = value
        except IndexError:
            raise IndexError(INDEX_ERROR_MSG.format(idx))

    def __delitem__(self, idx: int) -> None:
        """Delete a group at a specified list index."""
        try:
            del self.groups[idx]
            self.total -= 1
        except IndexError:
            raise IndexError(INDEX_ERROR_MSG.format(idx))

    def __len__(self) -> int:
        """Get the number of total group items in the list."""
        return self.total

    def __reversed__(self) -> Iterator[Group]:
        """Return the reversed group list."""
        return reversed(self.groups)

    def __contains__(self, item: Group) -> bool:
        """Check whether a given group is part of the list."""
        if not type(item) in (Group, str):
            raise ValueError("Expected type Group or str but got {}".format(
                type(item)))
        identifier = item.identifier if type(item) == Group else item
        return identifier in map(lambda x: x.identifier, self.groups)

    def __eq__(self, candidate: "GroupListResponse") -> bool:
        return all(
            (self.total == candidate.total, self.groups == candidate.groups))