dotanuki-labs/bitrise-reports

View on GitHub
bitrise_reports/bitrise.py

Summary

Maintainability
A
0 mins
Test Coverage
# bitrise.py

from .errors import ErrorCause, BitriseReportsError
from .models import BitriseBuild, BitriseProject, BuildStack, ExecutionStatus
from .models import BuildMachine, BuildMinutes, BitriseWorkflow, MachineSize

from datetime import datetime
import requests
import time

BITRISE_API_URL = "https://api.bitrise.io/v0.1"
FIRST_PAGE = "first-page"
NO_MORE_PAGES = "no-more-pages"


class Bitrise(object):
    def __init__(self, api_token):
        self.api = BitriseApiFetcher(api_token)
        self.converter = RawDataConverter()

    def available_projects(self):
        endpoint = f"{BITRISE_API_URL}/apps"
        raw_data = self.api.get(endpoint)
        return self.converter.projects_from(raw_data)

    def builds_for_project(self, project, starting=None, ending=None):
        endpoint = f"{BITRISE_API_URL}/apps/{project.slug}/builds"
        raw_data = self.api.get(endpoint, starting, ending)
        return self.converter.builds_from(raw_data, project)


class BitriseApiFetcher(object):
    def __init__(self, api_token):
        self.auth = {"Authorization": api_token}

    def get(self, endpoint, starting=None, ending=None):
        results = []
        next = FIRST_PAGE

        params = {}

        if starting:
            params["after"] = self.__unixtime(starting)

        if ending:
            params["before"] = self.__unixtime(ending)

        while next != NO_MORE_PAGES:

            if next != FIRST_PAGE:
                params["next"] = next

            fetched, next_page = self.__get_page(endpoint, params)
            results.extend(fetched)
            next = next_page

        return results

    def __get_page(self, endpoint, args=None):
        response = requests.get(endpoint, headers=self.auth, params=args)

        if response.status_code == requests.codes.ok:
            data = response.json()["data"]
            paging = response.json()["paging"]
            next = paging["next"] if "next" in paging.keys() else NO_MORE_PAGES
            return data, next
        else:
            cause = ErrorCause.NetworkingInfrastructure
            message = f"""
            Error when retriving data from : {endpoint}
            Status = {response.status_code}
            """
            raise BitriseReportsError(cause, message)

    def __unixtime(self, datetime):
        return int(time.mktime(datetime.timetuple()))


class RawDataConverter(object):
    def projects_from(self, json):
        def conversion(json, project):
            return [BitriseProject(item["title"], item["slug"]) for item in json]

        return self.__safely_convert(conversion, json)

    def builds_from(self, json, project):
        def conversion(json, project):
            finished_builds = list(filter(lambda raw: raw["finished_at"] is not None, json))
            return [self.build_from(item, project) for item in finished_builds]

        return self.__safely_convert(conversion, json, project)

    def __safely_convert(self, callable, json, project=None):
        try:
            return callable(json, project)
        except:
            cause = ErrorCause.DataConversion
            message = "Could not parse/convert information from builds"
            raise BitriseReportsError(cause, message)

    def build_from(self, json, project):
        machine = self.machine_from(json["machine_type_id"], json["stack_identifier"])
        workflow = self.workflow_from(json["triggered_workflow"])
        minutes = self.minutes_from(
            json["triggered_at"], json["started_on_worker_at"], json["finished_at"]
        )

        status = self.status_from(json["status"])
        branch = self.branch_from(json["original_build_params"])
        return BitriseBuild(project, machine, workflow, minutes, status, branch)

    def machine_from(self, machine_type_id, stack_identifier):
        size = MachineSize(machine_type_id)
        stack = BuildStack("macos" if "osx" in stack_identifier else "linux")
        return BuildMachine(f"{stack.value}.{size.value}", size, stack)

    def workflow_from(self, triggered_workflow):
        return BitriseWorkflow(triggered_workflow)

    def minutes_from(self, triggered_at, started_at, finished_at):
        triggered = self.__dt(triggered_at)
        started = self.__dt(started_at) if started_at is not None else triggered
        finished = self.__dt(finished_at)
        queued = self.__aproximate_minutes(started - triggered)
        building = self.__aproximate_minutes(finished - started)
        total = queued + building
        return BuildMinutes(queued, building, total)

    def status_from(self, status):
        return ExecutionStatus(status) if status in range(1, 3) else ExecutionStatus.aborted

    def branch_from(self, build_parameters):
        return None if build_parameters is None else build_parameters["branch"]

    def __dt(self, timestamp):
        return datetime.fromisoformat(timestamp.replace("Z", ""))

    def __aproximate_minutes(self, diff):
        return round((diff.days * 24 * 60) + (diff.seconds / 60))