Exemplo n.º 1
0
class SHAManExperiment:
    """This class represents an optimization experiment.

    It allows to plan, launch and store the information corresponding to
    an experiment.
    """

    def __init__(
        self,
        component_name: str,
        nbr_iteration: int,
        sbatch_file: str,
        experiment_name: str,
        configuration_file: str,
        sbatch_dir: str = None,
        slurm_dir: str = None,
        result_file: str = None,
    ) -> None:
        """Builds an object of class SHAManExperiment. This experiment is
        defined by giving:

            - The name of the component to tune.
            - The number of allowed iterations for the optimization process.
            - The name of the sbatch file to run.
            - The name of the experiment.
            - A configuration file to setup the experiment.
            - Where the slurm outputs should be stored
                (optional, if not specified, the outputs are deleted)
            - The path to the result file (optional, if not specified,
                no file is created).

        Args:
            component_name (str): The name of the component to use.
            nbr_iteration (int): The number of iterations.
            sbatch_file (str): The path to the sbatch file.
            experiment_name (str): The name of the experiment.
            sbatch_dir (str or Path): The working directory,
                where the shaman sbatch will be stored.
                If not specified, the directory where the command is called.
            slurm_dir (str or Path): The directory where the slurm outputs
                will be stored.
                If set to None, all the slurm outputs are removed after the
                end of the experiment.
            result_file (str): The path to the result file.
                If set to None, no such file is created and the results are
                debuged to the screen.
            configuration_file (str): The path to the configuration file.
                Defaults to the configuration file present in the package
                and called config.cfg.
        """
        # The name of the component that will be tuned through the experiment
        self.component_name = component_name
        # The maximum number of iterations
        self.nbr_iteration = nbr_iteration
        # The name of the sbatch to use, after ensuring its existence
        if Path(sbatch_file).exists():
            self.sbatch_file = sbatch_file
        else:
            raise FileNotFoundError(f"Could not find sbatch {sbatch_file}.")
        # Store information about the experiment
        self.experiment_name = experiment_name
        # Store the sbatch directory and convert to Path if not already
        if sbatch_dir:
            if isinstance(sbatch_dir, Path):
                self.sbatch_dir = sbatch_dir
            else:
                self.sbatch_dir = Path(sbatch_dir)
        else:
            self.sbatch_dir = Path.cwd()
        # Store the slurm directory and convert to Path if not already
        if slurm_dir:
            if isinstance(slurm_dir, Path):
                self.slurm_dir = slurm_dir
            else:
                self.slurm_dir = Path(slurm_dir)
        else:
            self.slurm_dir = None
        self.result_file = result_file
        self.experiment_id = ""

        # Parse the configuration
        self.configuration = SHAManConfig.from_yaml(
            configuration_file, self.component_name
        )
        # Create API client using the configuration information
        self.api_client = Client(base_url=self.api_url, proxies={})
        # Create the black box object using the informations
        self.bb_wrapper = BBWrapper(
            self.component_name,
            self.configuration.component_parameter_names,
            self.sbatch_file,
            sbatch_dir=self.sbatch_dir,
            component_configuration=self.api_url
            + "/"
            + api_settings.component_endpoint,
        )
        logger.debug(self.api_url + "/" + api_settings.component_endpoint)

        # Setup black box optimizer using configuration information
        self.bb_optimizer = self.setup_bb_optimizer()
        # Compute the start of the experiment
        self.experiment_start = \
            datetime.datetime.utcnow().strftime("%y/%m/%d %H:%M:%S")

    def setup_bb_optimizer(self) -> BBOptimizer:
        """Setups the black-box from the configuration."""
        # Create the BBOptimizer object using the different options in the
        # configuration
        # If pruning is enabled, parse corresponding fields
        if self.configuration.pruning:
            pruning = True
            max_step_cost = (
                self.bb_wrapper.default_target_value
                if self.configuration.pruning.max_step_duration == "default"
                else self.configuration.pruning.max_step_duration
            )
        else:
            max_step_cost = None
            pruning = False

        self.bb_optimizer = BBOptimizer(
            black_box=self.bb_wrapper,
            parameter_space=self.configuration.component_parameter_space,
            max_iteration=self.nbr_iteration,
            async_optim=pruning,
            max_step_cost=max_step_cost,
            **self.configuration.bbo_parameters,
        )
        return self.bb_optimizer

    def launch(self) -> None:
        """Launches the tuning experiment."""
        logger.debug("Creating experiment.")
        # Create the experiment through API request
        self.create_experiment()
        if self.configuration.experiment.default_first:
            # Launch a run using default parameterization of the component
            logger.info("Running application with default parametrization.")
            self.bb_wrapper.run_default()
        # Setup the optimizer
        logger.debug("Initializing black box optimizer.")
        self.setup_bb_optimizer()
        # Launch the optimization
        logger.debug("Launching optimization.")
        self.bb_optimizer.optimize(callbacks=[self.update_history])
        # Summarize the optimization
        # If there is a result file, write the output in it
        if self.result_file:
            orig_stdout = sys.stdout
            file_ = open(self.result_file, "w")
            sys.stdout = file_
            self.summarize()
            sys.stdout = orig_stdout
            file_.close()
        logger.debug(f"Summary of experiment: {self.summarize()}")

    def clean(self) -> None:
        """Cleans the experiment by removing the sbatch generated by Shaman.

        If there is no value specified for the slurm outputs folder,
        removes them.
        """
        for file_ in Path(__CURRENT_DIR__).glob("slurm*.out"):
            if self.slurm_dir:
                # Create if it doesn't already exist the folder
                self.slurm_dir.mkdir(exist_ok=True)
                # Move the output
                file_.rename(self.slurm_dir / file_.name)
            else:
                # Remove the output
                file_.unlink()
        # Clean up the sbatch file in the current dir where the experiment runs
        # Else keep it
        for file_ in Path(__CURRENT_DIR__).glob("*_shaman*.sbatch"):
            (Path(__CURRENT_DIR__) / file_).unlink()

    @property
    def api_url(self) -> str:
        """Returns as a string the URL to the API using the data in the
        configuration file."""
        return f"http://{api_settings.api_host}:{api_settings.api_port}"

    @property
    def start_experiment_dict(self) -> Dict:
        """Creates a dictionnary describing the experiment from its start."""
        return InitExperiment(
            **{
                "experiment_name": self.experiment_name,
                "experiment_start": self.experiment_start,
                "experiment_budget": self.nbr_iteration,
                "component": self.component_name,
                "experiment_parameters": dict(self.configuration.bbo),
                "noise_reduction_strategy":
                dict(self.configuration.noise_reduction)
                if self.configuration.noise_reduction
                else dict(),
                "pruning_strategy": {
                    "pruning_strategy": str(
                        self.configuration.pruning.max_step_duration
                    )
                    if self.configuration.pruning
                    else self.configuration.pruning
                },
                "sbatch": open(self.sbatch_file, "r").read(),
            }
        ).dict()

    def create_experiment(self) -> None:
        """Create the experiment upon initialization."""
        request = self.api_client.post(
            "experiments", json=self.start_experiment_dict)
        if not 200 <= request.status_code < 400:
            raise Exception(
                "Could not create experiment with status code"
                f"{request.status_code}"
            )
        self.experiment_id = request.json()["id"]

    @property
    def best_performance(self) -> float:
        """Returns the current best performance.

        Returns:
            float: The best time.
        """
        return self.bb_optimizer._get_best_performance()

    @property
    def improvement_default(self) -> float:
        """Computes the improvement compared to the default parametrization.

        Returns:
            float: The improvement compared to the default parametrization.
        """
        _, best_time = self.best_performance
        return float(
            round(
                (self.bb_wrapper.default_target_value - best_time)
                / self.bb_wrapper.default_target_value,
                2,
            )
            * 100
        )

    @property
    def average_noise(self) -> float:
        """Computes the average noise within the tested parametrization.

        Returns:
            float: The average measured noise.
        """
        return float(np.mean(self.bb_optimizer.measured_noise))

    def _updated_dict(self, history: Dict) -> Dict:
        """Builds the updated dictionary that will be sent at each iteration to
        the API.

        Args:
            history (Dict): The BBO history from the optimizer

        Returns:
            Dict: The updated dict to add to the POST request.
        """
        logger.debug(f"Current optimization history: {history}")
        best_parameters, best_fitness = self.best_performance
        logger.debug(f"Best parameters so far: {best_parameters}")
        logger.debug(f"Best performance so far: {best_fitness}")
        return IntermediateResult(
            **{
                "jobids": self.bb_wrapper.component.submitted_jobids[-1],
                "fitness": list(history["fitness"])[-1],
                "parameters": self.build_parameter_dict(
                    self.configuration.component_parameter_names,
                    history["parameters"].tolist(),
                )[-1],
                "truncated": bool(list(history["truncated"])[-1]),
                "resampled": bool(list(history["resampled"])[-1]),
                "initialization": bool(list(history["initialization"])[-1]),
                "improvement_default": self.improvement_default,
                "average_noise": self.average_noise,
                "explored_space":
                float(self.bb_optimizer.size_explored_space[0]),
                "best_parameters": self.build_parameter_dict(
                    self.configuration.component_parameter_names,
                    best_parameters.tolist(),
                ),
                "best_fitness": best_fitness,
            }
        ).dict()

    def update_history(self, history: Dict) -> None:
        """Update the optimization history at each BBO step and force
        conversion from numpy types.

        Args:
            history (dict): The BBO history
        """
        logger.debug(
            f"Writing update dictionary {self._updated_dict(history)}")
        request = self.api_client.put(
            f"experiments/{self.experiment_id}/update",
            json=self._updated_dict(history)
        )
        if not 200 <= request.status_code < 400:
            self.fail()
            raise Exception(
                "Could not finish experiment with status code"
                f"{request.status_code}"
            )

    @property
    def end_dict(self) -> Dict:
        """Comptues the dictionary sent to the API at the end of the
        experiment.

        Returns:
            Dict: The dictionary to send to the API.
        """
        best_parameters, best_fitness = self.best_performance
        return FinalResult(
            **{
                "averaged_fitness": self.bb_optimizer.averaged_fitness,
                "min_fitness": self.bb_optimizer.min_fitness,
                "max_fitness": self.bb_optimizer.max_fitness,
                "std_fitness": self.bb_optimizer.measured_noise,
                "resampled_nbr": self.bb_optimizer.resampled_nbr,
                "improvement_default": self.improvement_default,
                "elapsed_time": self.bb_optimizer.elapsed_time,
                "default_run": {
                    "fitness": self.bb_wrapper.default_target_value,
                    "job_id": self.bb_wrapper.default_jobid,
                    "parameters": self.bb_wrapper.default_parameters,
                },
                "average_noise": self.average_noise,
                "explored_space":
                float(self.bb_optimizer.size_explored_space[0]),
                "best_fitness": best_fitness,
                "best_parameters": self.build_parameter_dict(
                    self.configuration.component_parameter_names,
                    best_parameters.tolist(),
                ),
            }
        ).dict()

    def end(self):
        """End the experiment once it is over."""
        request = self.api_client.put(
            f"experiments/{self.experiment_id}/finish", json=self.end_dict
        )
        if not 200 <= request.status_code < 400:
            self.fail()
            raise Exception(
                "Could not finish experiment with status code"
                f"{request.status_code}"
            )

    def fail(self):
        """Fail the experiment.

        If there is an experiment id, sends a request at the endpoint
        /fail. Else, does not do anything (could be a good idea to
        perform some logging).
        """
        if self.experiment_id:
            request = self.api_client.put(
                f"experiments/{self.experiment_id}/fail")
            if not 200 <= request.status_code < 400:
                raise Exception(
                    "Could not fail experiment with status code"
                    f"{request.status_code}"
                )

    def stop(self):
        """Stop the experiment."""
        request = self.api_client.put(f"experiments/{self.experiment_id}/stop")
        if not 200 <= request.status_code < 400:
            raise Exception(
                "Could not fail experiment with status code"
                f"{request.status_code}"
            )

    def summarize(self):
        """Summarize the experiment by printing out the best parametrization,
        the associated fitness, and the BBO summary."""
        # Compute the optimal parametrization from the bb optimizer object
        optimal_param = self.build_parameter_dict(
            self.configuration.component_parameter_names,
            [self.bb_optimizer.best_parameters_in_grid],
        )
        print(f"Optimal time: {self.bb_optimizer.best_fitness}")
        print(
            f"Improvement compared to default:" f"{self.improvement_default}%")
        print(f"Optimal parametrization: {optimal_param}")
        print(
            "Number of early stops:"
            f"{sum(self.bb_optimizer.history['truncated'])}")
        print(
            "Average noise within each parametrization:"
            f"{self.bb_optimizer.measured_noise}"
        )
        self.bb_optimizer.summarize()

    @staticmethod
    def build_parameter_dict(
        parameter_names: List[str], parameter_values: List[List[float]]
    ) -> List[Dict]:
        """Static method to build the dictionary associated with the name of
        the parameters and their associated values.

        Args:
            parameter_names (list of str): The name of the parameters to use.
            parameter_values (list of lists of float):
                The values of the parameters

        Returns:
            list of dict: the dicts of the parameters with
                corresponding values.
        """
        # The list that will contain the dictionaries
        list_dict = []
        # Check if there is at least two nested elements in the dictionary
        if not isinstance(parameter_values[0], list):
            parameter_values = [parameter_values]
        for value in parameter_values:
            list_dict.append(dict(zip(parameter_names, value)))
        return list_dict
Exemplo n.º 2
0
class ZebrunnerAPI(metaclass=Singleton):
    authenticated = False

    def __init__(self, service_url: str = None, access_token: str = None):
        if service_url and access_token:
            self.service_url = service_url.rstrip("/")
            self.access_token = access_token
            self._client = Client()
            self._auth_token = None
            self.authenticated = False

    def _sign_request(self, request: Request) -> Request:
        request.headers["Authorization"] = f"Bearer {self._auth_token}"
        return request

    def auth(self) -> None:
        if not self.access_token or not self.service_url:
            return

        url = self.service_url + "/api/iam/v1/auth/refresh"
        try:
            response = self._client.post(
                url, json={"refreshToken": self.access_token})
        except httpx.RequestError as e:
            logger.warning("Error while sending request to zebrunner.",
                           exc_info=e)
            return

        if response.status_code != 200:
            log_response(response, logging.ERROR)
            return

        self._auth_token = response.json()["authToken"]
        self._client.auth = self._sign_request
        self.authenticated = True

    def start_test_run(self, project_key: str,
                       body: StartTestRunModel) -> Optional[int]:
        url = self.service_url + "/api/reporting/v1/test-runs"

        try:
            response = self._client.post(url,
                                         params={"projectKey": project_key},
                                         json=body.dict(exclude_none=True,
                                                        by_alias=True))
        except httpx.RequestError as e:
            logger.warning("Error while sending request to zebrunner.",
                           exc_info=e)
            return None

        if response.status_code != 200:
            log_response(response, logging.ERROR)
            return None

        return response.json()["id"]

    def start_test(self, test_run_id: int,
                   body: StartTestModel) -> Optional[int]:
        url = self.service_url + f"/api/reporting/v1/test-runs/{test_run_id}/tests"

        try:
            response = self._client.post(url,
                                         json=body.dict(exclude_none=True,
                                                        by_alias=True))
        except httpx.RequestError as e:
            logger.warning("Error while sending request to zebrunner.",
                           exc_info=e)
            return None

        if response.status_code != 200:
            log_response(response, logging.ERROR)
            return None

        return response.json()["id"]

    def finish_test(self, test_run_id: int, test_id: int,
                    body: FinishTestModel) -> None:
        url = self.service_url + f"/api/reporting/v1/test-runs/{test_run_id}/tests/{test_id}"

        try:
            response = self._client.put(url,
                                        json=body.dict(exclude_none=True,
                                                       by_alias=True))
        except httpx.RequestError as e:
            logger.warning("Error while sending request to zebrunner.",
                           exc_info=e)
            return

        if response.status_code != 200:
            log_response(response, logging.ERROR)

    def finish_test_run(self, test_run_id: int) -> None:
        url = self.service_url + f"/api/reporting/v1/test-runs/{test_run_id}"
        try:
            response = self._client.put(
                url,
                json={
                    "endedAt":
                    (datetime.utcnow().replace(tzinfo=timezone.utc) -
                     timedelta(seconds=1)).isoformat()
                },
            )
        except httpx.RequestError as e:
            logger.warning("Error while sending request to zebrunner.",
                           exc_info=e)
            return

        if response.status_code != 200:
            log_response(response, logging.ERROR)
            return

    def send_logs(self, test_run_id: int, logs: List[LogRecordModel]) -> None:
        url = self.service_url + f"/api/reporting/v1/test-runs/{test_run_id}/logs"

        body = [x.dict(exclude_none=True, by_alias=True) for x in logs]
        self._client.post(url, json=body)

    def send_screenshot(self, test_run_id: int, test_id: int,
                        image_path: Union[str, Path]) -> None:
        url = self.service_url + f"/api/reporting/v1/test-runs/{test_run_id}/tests/{test_id}/screenshots"
        with open(image_path, "rb") as image:
            self._client.post(
                url,
                content=image.read(),
                headers={
                    "Content-Type": "image/png",
                    "x-zbr-screenshot-captured-at": round(time.time() * 1000)
                },
            )

    def send_artifact(self,
                      filename: Union[str, Path],
                      test_run_id: int,
                      test_id: Optional[int] = None) -> None:
        if test_id:
            url = f"{self.service_url}/api/reporting/v1/test-runs/{test_run_id}/tests/{test_id}/artifacts"
        else:
            url = f"{self.service_url}/api/reporting/v1/test-runs/{test_run_id}/artifacts"
        with open(filename, "rb") as file:
            self._client.post(url, files={"file": file})

    def send_artifact_references(self,
                                 references: List[ArtifactReferenceModel],
                                 test_run_id: int,
                                 test_id: Optional[int] = None) -> None:
        if test_id:
            url = f"{self.service_url}/api/reporting/v1/test-runs/{test_run_id}/tests/{test_id}/artifact-references"
        else:
            url = f"{self.service_url}/api/reporting/v1/test-runs/{test_run_id}/artifact-references/"
        json_items = [
            item.dict(exclude_none=True, by_alias=True) for item in references
        ]
        self._client.put(url, json={"items": json_items})

    def send_labels(self,
                    labels: List[LabelModel],
                    test_run_id: int,
                    test_id: Optional[int] = None) -> None:
        if test_id:
            url = f"{self.service_url}/api/reporting/v1/test-runs/{test_run_id}/tests/{test_id}/labels"
        else:
            url = f"{self.service_url}/api/reporting/v1/test-runs/{test_run_id}/labels"
        labels_json = [
            label.dict(exclude_none=True, by_alias=True) for label in labels
        ]
        self._client.put(url, json={"items": labels_json})

    def start_test_session(self, test_run_id: int,
                           body: StartTestSessionModel) -> Optional[str]:
        url = self.service_url + f"/api/reporting/v1/test-runs/{test_run_id}/test-sessions"
        response = self._client.post(url,
                                     json=body.dict(exclude_none=True,
                                                    by_alias=True))
        if not response.status_code == 200:
            log_response(response, logging.ERROR)
            return None

        return response.json().get("id")

    def finish_test_session(self, test_run_id: int, zebrunner_id: str,
                            body: FinishTestSessionModel) -> None:
        url = self.service_url + f"/api/reporting/v1/test-runs/{test_run_id}/test-sessions/{zebrunner_id}"
        self._client.put(url, json=body.dict(exclude_none=True, by_alias=True))

    def get_rerun_tests(self, run_context: str) -> RerunDataModel:
        url = self.service_url + "/api/reporting/v1/run-context-exchanges"
        run_context_dict = json.loads(run_context)
        response = self._client.post(url, json=run_context_dict)
        response_data = response.json()
        for test in response_data["tests"]:
            correlation_data = test["correlationData"]
            if correlation_data is not None:
                test["correlationData"] = json.loads(correlation_data)
        return RerunDataModel(**response_data)

    def close(self) -> None:
        self._client.close()
Exemplo n.º 3
0
Arquivo: api.py Projeto: Xowap/DEV-CLI
class DevApi:
    """
    A class to access the dev.to API
    """

    BASE_URL = "https://dev.to/api"

    def __init__(self, api_key: Text):
        """
        Creates the HTTP client
        """

        self.api_key = api_key
        self.client = Client(headers=[("Api-Key", self.api_key)])

    def url(self, path: Text) -> Text:
        """
        Generates an URL, be careful not to put any slash at the start or the
        end.
        """

        return f"{self.BASE_URL}/{path}"

    def get_my_articles(self, publication: Text = "all") -> Iterator[Dict]:
        """
        Returns an iterator over all the articles corresponding to the
        publication filter.

        :param publication: Publication status. Allowed values are "published",
                            "unpublished" and "all"
        :return: An iterator of all selected articles
        """

        assert publication in {"published", "unpublished", "all"}

        url = self.url(f"articles/me/{publication}")

        class NoMorePages(Exception):
            """
            A way to communicate that there is no more page coming up from the
            API and that the polling of pages should stop now.
            """

        def get_page(page: int):
            """
            Returns a given page. Pages are 1-indexed.
            """

            r = self.client.get(url, params={"page": page})
            stop = True

            for article in r.json():
                stop = False
                yield article

            if stop:
                raise NoMorePages

        for i in range(1, 1000):
            try:
                yield from get_page(i)
            except NoMorePages:
                return

    def find_article(self, key: DevKey) -> Optional[Dict]:
        """
        Finds the first article matching they key. Let's take a moment to note
        that this method is really approximate but since we can't retrofit the
        API ID into the Markdown file it's the only decent way to go.
        """

        for article in self.get_my_articles():
            if article[key.name] == key.value:
                return article

    def create_article(self, parser: DevParser) -> None:
        """
        Creates an article based on the parsed file.
        """

        url = self.url("articles")

        if "title" not in parser.front_matter:
            raise DevApiError(
                "Cannot create an article with no `title` in the front matter")

        r = self.client.post(
            url,
            json={
                "title": parser.front_matter["title"],
                "body_markdown": parser.file_content,
            },
        )
        r.raise_for_status()

    def update_article(self, parser: DevParser, article_id: int) -> None:
        """
        Updates an article based on the parsed file and an existing ID.
        """

        url = self.url(f"articles/{article_id}")

        r = self.client.put(url, json={"body_markdown": parser.file_content})
        r.raise_for_status()
Exemplo n.º 4
0
class Request:
    """Start a requests session and provide helper methods for HTTP verbs.
    """
    def __init__(self):
        self.session = Client(timeout=10)
        self.headers = self.client.AuthHeader

    def InjectAttrs(self, **kwargs):
        """Add custom headers if requested, default authorisation header included in __init__.

        Args:
            headers (dict): Key value pairs for headers
        """
        if kwargs.get("headers"):
            self.headers = {**self.headers, **kwargs.get("headers")}
        if kwargs.get("start"):
            self.body.update({"start": kwargs.get("start")})

    @retry(
        retry_on_exceptions=(RetryException),
        max_calls_total=3,
        retry_window_after_first_call_in_seconds=10,
    )
    @basicauth
    def GET(self, url, params={}, headers: dict = {}) -> Response:
        """HTTP GET Request

        Args:
            url (str): URL to connect to.
            headers (dict, optional): Key Value pairs for additional headers. (default: None)

        Returns:
            Response -- HTTPX Response object
        """
        self.InjectAttrs(headers=headers)
        try:
            response = self.session.get(url,
                                        params=params,
                                        headers=self.headers)
            return response
        except _exceptions.TimeoutException:
            raise RetryException

    @retry(
        retry_on_exceptions=(RetryException),
        max_calls_total=3,
        retry_window_after_first_call_in_seconds=10,
    )
    @contenttype
    @basicauth
    def PATCH(
        self,
        url: str,
        body=None,
        headers: dict = {},
        idempotent: bool = False,
    ) -> Response:
        """HTTP PATCH Request

        Args:
            url (str): URL to connect to.
            body (dict, optional): JSON payload for the request. (default: dict())
            headers (dict, optional): Key Value pairs for additional headers. (default: None)
            idempotent (bool, optional): Is this request idempotent? (default: False)

        Returns:
            Response -- HTTPX Response object
        """
        self.InjectAttrs(headers=headers)
        try:
            response = self.session.patch(url, data=body, headers=self.headers)
            return response
        except _exceptions.TimeoutException:
            if idempotent is False:
                raise IdempotentTimeout(method="PATCH",
                                        url=url,
                                        data=body,
                                        headers=self.headers)
            else:
                print("retry")
                raise RetryException

    @retry(
        retry_on_exceptions=(RetryException),
        max_calls_total=3,
        retry_window_after_first_call_in_seconds=10,
    )
    @contenttype
    @basicauth
    def PUT(self, url: str, body: dict = {}, headers: dict = {}) -> Response:
        """HTTP PUT Request

        Args:
            url (str): URL to connect to.
            body (dict, optional): JSON payload for the request. (default: dict())
            headers (dict, optional): Key Value pairs for additional headers. (default: None)

        Returns:
            Response -- HTTPX Response object
        """
        self.InjectAttrs(headers=headers)
        try:
            response = self.session.put(url, data=body, headers=self.headers)
            return response
        except _exceptions.TimeoutException:
            raise RetryException

    @retry(
        retry_on_exceptions=(RetryException),
        max_calls_total=3,
        retry_window_after_first_call_in_seconds=10,
    )
    @contenttype
    @basicauth
    def POST(
        self,
        url: str,
        body: str = None,
        params: dict = None,
        headers: dict = {},
        idempotent: bool = False,
    ) -> Response:
        """HTTP POST Request

        Args:
            url (str): URL to connect to.
            body (dict, optional): JSON payload for the request. (default: dict())
            params (dict, optional): Dictionary for path parameters. (default: None)
            headers (dict, optional): Key Value pairs for additional headers. (default: None)
            idempotent (bool, optional): Is this request idempotent? (default: False)

        Returns:
            Response -- HTTPX Response object
        """
        self.InjectAttrs(headers=headers)
        try:
            response = self.session.post(url,
                                         data=body,
                                         params=params,
                                         headers=self.headers)
            return response
        except _exceptions.TimeoutException:
            if idempotent is False:
                raise IdempotentTimeout(method="POST",
                                        url=url,
                                        body=body,
                                        headers=self.headers)
            else:
                print("retry")
                raise RetryException

    @retry(
        retry_on_exceptions=(RetryException),
        max_calls_total=3,
        retry_window_after_first_call_in_seconds=10,
    )
    @contenttype
    @basicauth
    def DELETE(self,
               url: str,
               body: dict = {},
               headers: dict = {}) -> Response:
        """HTTP DELETE Request

        Args:
            url (str): URL to connect to.
            body (dict, optional): JSON payload for the request. (default: dict())
            headers (dict, optional): Key Value pairs for additional headers. (default: None)

        Returns:
            Response -- HTTPX Response object
        """
        self.InjectAttrs(headers=headers)
        try:
            response = self.session.delete(url, headers=self.headers)
            return response
        except _exceptions.TimeoutException:
            raise RetryException
Exemplo n.º 5
0
class RegisterCommands(object):
    """This object contains methods to help registering a command to Discord.
    While you shouldn't need to import this directly, it's still accessible if you
    prefer not to initalize a Dispike object.

    Important to remember all methods are not async.

    """

    def __init__(self, application_id: str, bot_token: str):
        """Initalize object provided with application_id and a bot token

        Args:
            application_id (str): Client ID
            bot_token (str): Bot user Token
        """
        self.__bot_token = bot_token
        self._application_id = application_id
        self._client = Client(
            base_url=f"https://discord.com/api/v8/applications/{self._application_id}/",
            event_hooks={
                "response": [dispike_httpx_event_hook_incoming_request],
                "request": [dispike_httpx_event_hook_outgoing_request],
            },
        )

    @logger.catch(reraise=True, message="Issue with bulk overrwriting commands")
    def bulk_overwrite_commands(
        self,
        commands: typing.List[DiscordCommand],
        guild_only: bool = False,
        guild_to_target: int = None,
    ):
        """Bulk OVERWRITE commands to specific guilds or globally.

        Args:
            commands (typing.List[DiscordCommand]): List of new commands (these commands will be overwritten)
            guild_only (bool, optional): Default to set global mode (True). Set to False to let the function know to expect a guild_id
            guild_to_target (int, optional): A guild Id if guild_only is set to True.
        """
        if guild_only == True:
            if guild_to_target is None:
                raise TypeError(
                    "if guild_only is set to true, a guild id must be provided."
                )

            logger.info(f"Targeting a specific guild -> {guild_to_target}")
            _request_url = f"guilds/{guild_to_target}/commands"
        else:
            _request_url = f"commands"

        _commands_to_json = [command.dict(exclude_none=True) for command in commands]
        _send_request = self._client.put(
            url=_request_url, json=_commands_to_json, headers=self.request_headers
        )
        if _send_request.status_code == 200:
            logger.info(
                f"Overwritten {len(_send_request.json())} commands.. Recieved ({len(_commands_to_json)}"
            )
            return True
        else:
            logger.debug(
                f"BULK Overwrite failed ({guild_only} = {guild_to_target}): Body: {_commands_to_json}.. Status code: {_send_request.status_code}"
            )
            raise DiscordAPIError(_send_request.status_code, _send_request.text)

    def register(
        self, command: DiscordCommand, guild_only=False, guild_to_target: int = None
    ):
        """Register a completed `DiscordCommand` model to Discord API.

        Args:
            command (DiscordCommand): A properly configured DiscordCommand
            guild_only (bool, optional): Default to set global mode (True). Set to False to let the function know to expect a guild_id
            guild_to_target (int, optional): A guild Id if guild_only is set to True.
        """
        if guild_only == True:
            if guild_to_target is None:
                raise TypeError(
                    "if guild_only is set to true, a guild id must be provided."
                )

            logger.info(f"Targeting a specific guild -> {guild_to_target}")
            _request_url = f"guilds/{guild_to_target}/commands"
        else:
            _request_url = f"commands"

        try:
            _command_to_json = command.dict(exclude_none=True)
            _send_request = self._client.post(
                _request_url, headers=self.request_headers, json=_command_to_json
            )
            if _send_request.status_code in [200, 201]:
                return True

            raise DiscordAPIError(_send_request.status_code, _send_request.text)
        except Exception:
            raise

    @property
    def request_headers(self):
        """Return a valid header for authorization

        Returns:
            dict: a valid header for authorization
        """
        return {"Authorization": f"Bot {self.__bot_token}"}

    @property
    def bot_token(self):
        """You cannot view the bot_token directly, but you can still 'update' it.

        Raises:
            PermissionError: If you attempt to view the bot token without a new value
        """
        raise PermissionError("You cannot view the bot token directly.")

    @bot_token.setter
    def bot_token(self, new_bot_token):
        if new_bot_token == self.__bot_token:
            raise TypeError("Bot token already set to that.")
        self.__bot_token = new_bot_token