<commit_before>import logging
import threading
from abc import ABC
from abc import abstractmethod
from time import sleep
from typing import Optional
from typing import Union

from kubernetes.client import V1beta1PodDisruptionBudget
from kubernetes.client import V1DeleteOptions
from kubernetes.client import V1Deployment
from kubernetes.client import V1StatefulSet
from kubernetes.client.rest import ApiException

from paasta_tools.kubernetes_tools import create_deployment
from paasta_tools.kubernetes_tools import create_pod_disruption_budget
from paasta_tools.kubernetes_tools import create_stateful_set
from paasta_tools.kubernetes_tools import force_delete_pods
from paasta_tools.kubernetes_tools import KubeClient
from paasta_tools.kubernetes_tools import KubeDeployment
from paasta_tools.kubernetes_tools import KubernetesDeploymentConfig
from paasta_tools.kubernetes_tools import list_all_deployments
from paasta_tools.kubernetes_tools import load_kubernetes_service_config_no_cache
from paasta_tools.kubernetes_tools import pod_disruption_budget_for_service_instance
from paasta_tools.kubernetes_tools import update_deployment
from paasta_tools.kubernetes_tools import update_stateful_set

class Application(ABC):
    def __init__(
        self,
        item: Union[V1Deployment, V1StatefulSet],
        logging=logging.getLogger(__name__),
    ) -> None:
        """
        This Application wrapper is an interface for creating/deleting k8s deployments and statefulsets
        soa_config is KubernetesDeploymentConfig. It is not loaded in init because it is not always required.
        :param item: Kubernetes Object(V1Deployment/V1StatefulSet) that has already been filled up.
        :param logging: where logs go
        """
        if not item.metadata.namespace:
            item.metadata.namespace = "paasta"
        self.kube_deployment = KubeDeployment(
            service=item.metadata.labels["paasta.yelp.com/service"],
            instance=item.metadata.labels["paasta.yelp.com/instance"],
            git_sha=item.metadata.labels["paasta.yelp.com/git_sha"],
            config_sha=item.metadata.labels["paasta.yelp.com/config_sha"],
            replicas=item.spec.replicas,
        )
        self.item = item
        self.soa_config = None  # type: KubernetesDeploymentConfig
        self.logging = logging

    def load_local_config(
        self, soa_dir: str, cluster: str
    ) -> Optional[KubernetesDeploymentConfig]:
        if not self.soa_config:
            self.soa_config = load_kubernetes_service_config_no_cache(
                service=self.kube_deployment.service,
                instance=self.kube_deployment.instance,
                cluster=cluster,
                soa_dir=soa_dir,
            )
        return self.soa_config

    def __str__(self):
        service = self.kube_deployment.service
        instance = self.kube_deployment.instance
        git_sha = self.kube_deployment.git_sha
        config_sha = self.kube_deployment.config_sha
        return f"{service}-{instance}-{git_sha}-{config_sha}"

    @abstractmethod
    def deep_delete(self, kube_client: KubeClient) -> None:
        """
        Remove all controllers, pods, and pod disruption budgets related to this application
        :param kube_client:
        """
        pass

    def create(self, kube_client: KubeClient):
        """
        Create all controllers, HPA, and pod disruption budgets related to this application
        :param kube_client:
        """
        pass

    def update(self, kube_client: KubeClient):
        """
        Update all controllers, HPA, and pod disruption budgets related to this application
        :param kube_client:
        """
        pass

    def delete_pod_disruption_budget(self, kube_client: KubeClient) -> None:
        try:
            kube_client.policy.delete_namespaced_pod_disruption_budget(
                name=self.item.metadata.name,
                namespace=self.item.metadata.namespace,
                body=V1DeleteOptions(),
            )
        except ApiException as e:
            if e.status == 404:
                # Deployment does not exist, nothing to delete but
                # we can consider this a success.
                self.logging.debug(
                    "not deleting nonexistent pod disruption budget/{} from namespace/{}".format(
                        self.item.metadata.name, self.item.metadata.namespace
                    )
                )
            else:
                raise
        else:
            self.logging.info(
                "deleted pod disruption budget/{} from namespace/{}".format(
                    self.item.metadata.name, self.item.metadata.namespace
                )
            )

    def ensure_pod_disruption_budget(
        self, kube_client: KubeClient
    ) -> V1beta1PodDisruptionBudget:
        pdr = pod_disruption_budget_for_service_instance(
            service=self.kube_deployment.service,
            instance=self.kube_deployment.instance,
            max_unavailable="{}%".format(
                int((1 - self.soa_config.get_bounce_margin_factor()) * 100)
            ),
        )
        try:
            existing_pdr = kube_client.policy.read_namespaced_pod_disruption_budget(
                name=pdr.metadata.name, namespace=pdr.metadata.namespace
            )
        except ApiException as e:
            if e.status == 404:
                existing_pdr = None
            else:
                raise

        if existing_pdr:
            if existing_pdr.spec.min_available is not None:
                logging.debug(
                    "Not updating poddisruptionbudget: can't have both "
                    "min_available and max_unavailable"
                )
            elif existing_pdr.spec.max_unavailable != pdr.spec.max_unavailable:
                logging.debug(f"Updating poddisruptionbudget {pdr.metadata.name}")
                return kube_client.policy.patch_namespaced_pod_disruption_budget(
                    name=pdr.metadata.name, namespace=pdr.metadata.namespace, body=pdr
                )
            else:
                logging.debug(f"poddisruptionbudget {pdr.metadata.name} up to date")
        else:
            logging.debug(f"creating poddisruptionbudget {pdr.metadata.name}")
            return create_pod_disruption_budget(
                kube_client=kube_client, pod_disruption_budget=pdr
            )

class DeploymentWrapper(Application):
    def deep_delete(self, kube_client: KubeClient) -> None:
        """
        Remove all controllers, pods, and pod disruption budgets related to this application
        :param kube_client:
        """
        delete_options = V1DeleteOptions(propagation_policy="Foreground")
        try:
            kube_client.deployments.delete_namespaced_deployment(
                self.item.metadata.name,
                self.item.metadata.namespace,
                body=delete_options,
            )
        except ApiException as e:
            if e.status == 404:
                # Deployment does not exist, nothing to delete but
                # we can consider this a success.
                self.logging.debug(
                    "not deleting nonexistent deploy/{} from namespace/{}".format(
                        self.item.metadata.name, self.item.metadata.namespace
                    )
                )
            else:
                raise
        else:
            self.logging.info(
                "deleted deploy/{} from namespace/{}".format(
                    self.item.metadata.name, self.item.metadata.namespace
                )
            )
        self.delete_pod_disruption_budget(kube_client)
        self.delete_horizontal_pod_autoscaler(kube_client)

    def get_existing_app(self, kube_client: KubeClient):
        return kube_client.deployments.read_namespaced_deployment(
            name=self.item.metadata.name, namespace=self.item.metadata.namespace
        )

    def create(self, kube_client: KubeClient) -> None:
        create_deployment(kube_client=kube_client, formatted_deployment=self.item)
        self.ensure_pod_disruption_budget(kube_client)
        self.sync_horizontal_pod_autoscaler(kube_client)

    def deep_delete_and_create(self, kube_client: KubeClient) -> None:
        self.deep_delete(kube_client)
        timer = 0
        while (
            self.kube_deployment in set(list_all_deployments(kube_client))
            and timer < 60
        ):
            sleep(1)
            timer += 1

        if timer >= 60 and self.kube_deployment in set(
            list_all_deployments(kube_client)
        ):
            try:
                force_delete_pods(
                    self.item.metadata.name,
                    self.kube_deployment.service,
                    self.kube_deployment.instance,
                    self.item.metadata.namespace,
                    kube_client,
                )
            except ApiException as e:
                if e.status == 404:
                    # Deployment does not exist, nothing to delete but
                    # we can consider this a success.
                    self.logging.debug(
                        "not deleting nonexistent deploy/{} from namespace/{}".format(
                            self.kube_deployment.service, self.item.metadata.namespace
                        )
                    )
                else:
                    raise
        else:
            self.logging.info(
                "deleted deploy/{} from namespace/{}".format(
                    self.kube_deployment.service, self.item.metadata.namespace
                )
            )
        self.create(kube_client=kube_client)

    def update(self, kube_client: KubeClient) -> None:
        # If HPA is enabled, do not update replicas.
        # In all other cases, replica is set to max(instances, min_instances)
        if self.soa_config.config_dict.get("bounce_method", "") == "brutal":
            threading.Thread(
                target=self.deep_delete_and_create, args=[KubeClient()]
            ).start()
            return
        if self.should_have_hpa() is None:
            self.item.spec.replicas = self.get_existing_app(kube_client).spec.replicas
        update_deployment(kube_client=kube_client, formatted_deployment=self.item)
        self.ensure_pod_disruption_budget(kube_client)
        self.sync_horizontal_pod_autoscaler(kube_client)

    def should_have_hpa(self):
        return (
            self.soa_config.get_max_instances() is not None
            # with bespoke autoscaler, setup_kubernetes_job sets the number of instances directly; no HPA is required.
            and self.soa_config.get_autoscaling_params()["decision_policy"] != "bespoke"
        )

    def sync_horizontal_pod_autoscaler(self, kube_client: KubeClient) -> None:
        """
        In order for autoscaling to work, there needs to be at least two configurations
        min_instnace, max_instance, and there cannot be instance.
        """
        self.logging.info(
            f"Syncing HPA setting for {self.item.metadata.name}/name in {self.item.metadata.namespace}"
        )
        hpa_exists = self.exists_hpa(kube_client)
        # NO autoscaling
        if not self.should_have_hpa():
            # Remove HPA if autoscaling is disabled
            if hpa_exists:
                self.delete_horizontal_pod_autoscaler(kube_client)
            return

        body = self.soa_config.get_autoscaling_metric_spec(
            name=self.item.metadata.name,
            cluster=self.soa_config.cluster,
            namespace=self.item.metadata.namespace,
        )
        if not body:
            raise Exception(
                f"CRIT: autoscaling misconfigured for {self.kube_deployment.service}."
                + f"{self.kube_deployment.instance}.Please correct the configuration and update pre-commit hook."
            )
        self.logging.debug(body)
        if not hpa_exists:
            self.logging.info(
                f"Creating new HPA for {self.item.metadata.name}/name in {self.item.metadata.namespace}"
            )
            kube_client.autoscaling.create_namespaced_horizontal_pod_autoscaler(
                namespace=self.item.metadata.namespace, body=body, pretty=True
            )
        else:
            self.logging.info(
                f"Updating new HPA for {self.item.metadata.name}/name in {self.item.metadata.namespace}/namespace"
            )
            kube_client.autoscaling.patch_namespaced_horizontal_pod_autoscaler(
                name=self.item.metadata.name,
                namespace=self.item.metadata.namespace,
                body=body,
                pretty=True,
            )

    def exists_hpa(self, kube_client: KubeClient) -> bool:
        return (
            len(
                kube_client.autoscaling.list_namespaced_horizontal_pod_autoscaler(
                    field_selector=f"metadata.name={self.item.metadata.name}",
                    namespace=self.item.metadata.namespace,
                ).items
            )
            > 0
        )

    def delete_horizontal_pod_autoscaler(self, kube_client: KubeClient) -> None:
        try:
            kube_client.autoscaling.delete_namespaced_horizontal_pod_autoscaler(
                name=self.item.metadata.name,
                namespace=self.item.metadata.namespace,
                body=V1DeleteOptions(),
            )
        except ApiException as e:
            if e.status == 404:
                # Deployment does not exist, nothing to delete but
                # we can consider this a success.
                self.logging.debug(
                    f"not deleting nonexistent HPA/{self.item.metadata.name} from namespace/{self.item.metadata.namespace}"
                )
            else:
                raise
        else:
            self.logging.info(
                "deleted HPA/{} from namespace/{}".format(
                    self.item.metadata.name, self.item.metadata.namespace
                )
            )

class StatefulSetWrapper(Application):
    def deep_delete(self, kube_client: KubeClient) -> None:
        """
        Remove all controllers, pods, and pod disruption budgets related to this application
        :param kube_client:
        """
        delete_options = V1DeleteOptions(propagation_policy="Foreground")
        try:
            kube_client.deployments.delete_namespaced_stateful_set(
                self.item.metadata.name,
                self.item.metadata.namespace,
                body=delete_options,
            )
        except ApiException as e:
            if e.status == 404:
                # StatefulSet does not exist, nothing to delete but
                # we can consider this a success.
                self.logging.debug(
                    "not deleting nonexistent statefulset/{} from namespace/{}".format(
                        self.item.metadata.name, self.item.metadata.namespace
                    )
                )
            else:
                raise
        else:
            self.logging.info(
                "deleted statefulset/{} from namespace/{}".format(
                    self.item.metadata.name, self.item.metadata.namespace
                )
            )
        self.delete_pod_disruption_budget(kube_client)

    def create(self, kube_client: KubeClient):
        create_stateful_set(kube_client=kube_client, formatted_stateful_set=self.item)
        self.ensure_pod_disruption_budget(kube_client)

    def update(self, kube_client: KubeClient):
        update_stateful_set(kube_client=kube_client, formatted_stateful_set=self.item)
        self.ensure_pod_disruption_budget(kube_client)
<commit_msg>fix a small new autoscaler only bug (#2597)<commit_after>import logging
import threading
from abc import ABC
from abc import abstractmethod
from time import sleep
from typing import Optional
from typing import Union

from kubernetes.client import V1beta1PodDisruptionBudget
from kubernetes.client import V1DeleteOptions
from kubernetes.client import V1Deployment
from kubernetes.client import V1StatefulSet
from kubernetes.client.rest import ApiException

from paasta_tools.kubernetes_tools import create_deployment
from paasta_tools.kubernetes_tools import create_pod_disruption_budget
from paasta_tools.kubernetes_tools import create_stateful_set
from paasta_tools.kubernetes_tools import force_delete_pods
from paasta_tools.kubernetes_tools import KubeClient
from paasta_tools.kubernetes_tools import KubeDeployment
from paasta_tools.kubernetes_tools import KubernetesDeploymentConfig
from paasta_tools.kubernetes_tools import list_all_deployments
from paasta_tools.kubernetes_tools import load_kubernetes_service_config_no_cache
from paasta_tools.kubernetes_tools import pod_disruption_budget_for_service_instance
from paasta_tools.kubernetes_tools import update_deployment
from paasta_tools.kubernetes_tools import update_stateful_set

class Application(ABC):
    def __init__(
        self,
        item: Union[V1Deployment, V1StatefulSet],
        logging=logging.getLogger(__name__),
    ) -> None:
        """
        This Application wrapper is an interface for creating/deleting k8s deployments and statefulsets
        soa_config is KubernetesDeploymentConfig. It is not loaded in init because it is not always required.
        :param item: Kubernetes Object(V1Deployment/V1StatefulSet) that has already been filled up.
        :param logging: where logs go
        """
        if not item.metadata.namespace:
            item.metadata.namespace = "paasta"
        self.kube_deployment = KubeDeployment(
            service=item.metadata.labels["paasta.yelp.com/service"],
            instance=item.metadata.labels["paasta.yelp.com/instance"],
            git_sha=item.metadata.labels["paasta.yelp.com/git_sha"],
            config_sha=item.metadata.labels["paasta.yelp.com/config_sha"],
            replicas=item.spec.replicas,
        )
        self.item = item
        self.soa_config = None  # type: KubernetesDeploymentConfig
        self.logging = logging

    def load_local_config(
        self, soa_dir: str, cluster: str
    ) -> Optional[KubernetesDeploymentConfig]:
        if not self.soa_config:
            self.soa_config = load_kubernetes_service_config_no_cache(
                service=self.kube_deployment.service,
                instance=self.kube_deployment.instance,
                cluster=cluster,
                soa_dir=soa_dir,
            )
        return self.soa_config

    def __str__(self):
        service = self.kube_deployment.service
        instance = self.kube_deployment.instance
        git_sha = self.kube_deployment.git_sha
        config_sha = self.kube_deployment.config_sha
        return f"{service}-{instance}-{git_sha}-{config_sha}"

    @abstractmethod
    def deep_delete(self, kube_client: KubeClient) -> None:
        """
        Remove all controllers, pods, and pod disruption budgets related to this application
        :param kube_client:
        """
        pass

    def create(self, kube_client: KubeClient):
        """
        Create all controllers, HPA, and pod disruption budgets related to this application
        :param kube_client:
        """
        pass

    def update(self, kube_client: KubeClient):
        """
        Update all controllers, HPA, and pod disruption budgets related to this application
        :param kube_client:
        """
        pass

    def delete_pod_disruption_budget(self, kube_client: KubeClient) -> None:
        try:
            kube_client.policy.delete_namespaced_pod_disruption_budget(
                name=self.item.metadata.name,
                namespace=self.item.metadata.namespace,
                body=V1DeleteOptions(),
            )
        except ApiException as e:
            if e.status == 404:
                # Deployment does not exist, nothing to delete but
                # we can consider this a success.
                self.logging.debug(
                    "not deleting nonexistent pod disruption budget/{} from namespace/{}".format(
                        self.item.metadata.name, self.item.metadata.namespace
                    )
                )
            else:
                raise
        else:
            self.logging.info(
                "deleted pod disruption budget/{} from namespace/{}".format(
                    self.item.metadata.name, self.item.metadata.namespace
                )
            )

    def ensure_pod_disruption_budget(
        self, kube_client: KubeClient
    ) -> V1beta1PodDisruptionBudget:
        pdr = pod_disruption_budget_for_service_instance(
            service=self.kube_deployment.service,
            instance=self.kube_deployment.instance,
            max_unavailable="{}%".format(
                int((1 - self.soa_config.get_bounce_margin_factor()) * 100)
            ),
        )
        try:
            existing_pdr = kube_client.policy.read_namespaced_pod_disruption_budget(
                name=pdr.metadata.name, namespace=pdr.metadata.namespace
            )
        except ApiException as e:
            if e.status == 404:
                existing_pdr = None
            else:
                raise

        if existing_pdr:
            if existing_pdr.spec.min_available is not None:
                logging.debug(
                    "Not updating poddisruptionbudget: can't have both "
                    "min_available and max_unavailable"
                )
            elif existing_pdr.spec.max_unavailable != pdr.spec.max_unavailable:
                logging.debug(f"Updating poddisruptionbudget {pdr.metadata.name}")
                return kube_client.policy.patch_namespaced_pod_disruption_budget(
                    name=pdr.metadata.name, namespace=pdr.metadata.namespace, body=pdr
                )
            else:
                logging.debug(f"poddisruptionbudget {pdr.metadata.name} up to date")
        else:
            logging.debug(f"creating poddisruptionbudget {pdr.metadata.name}")
            return create_pod_disruption_budget(
                kube_client=kube_client, pod_disruption_budget=pdr
            )

class DeploymentWrapper(Application):
    def deep_delete(self, kube_client: KubeClient) -> None:
        """
        Remove all controllers, pods, and pod disruption budgets related to this application
        :param kube_client:
        """
        delete_options = V1DeleteOptions(propagation_policy="Foreground")
        try:
            kube_client.deployments.delete_namespaced_deployment(
                self.item.metadata.name,
                self.item.metadata.namespace,
                body=delete_options,
            )
        except ApiException as e:
            if e.status == 404:
                # Deployment does not exist, nothing to delete but
                # we can consider this a success.
                self.logging.debug(
                    "not deleting nonexistent deploy/{} from namespace/{}".format(
                        self.item.metadata.name, self.item.metadata.namespace
                    )
                )
            else:
                raise
        else:
            self.logging.info(
                "deleted deploy/{} from namespace/{}".format(
                    self.item.metadata.name, self.item.metadata.namespace
                )
            )
        self.delete_pod_disruption_budget(kube_client)
        self.delete_horizontal_pod_autoscaler(kube_client)

    def get_existing_app(self, kube_client: KubeClient):
        return kube_client.deployments.read_namespaced_deployment(
            name=self.item.metadata.name, namespace=self.item.metadata.namespace
        )

    def create(self, kube_client: KubeClient) -> None:
        create_deployment(kube_client=kube_client, formatted_deployment=self.item)
        self.ensure_pod_disruption_budget(kube_client)
        self.sync_horizontal_pod_autoscaler(kube_client)

    def deep_delete_and_create(self, kube_client: KubeClient) -> None:
        self.deep_delete(kube_client)
        timer = 0
        while (
            self.kube_deployment in set(list_all_deployments(kube_client))
            and timer < 60
        ):
            sleep(1)
            timer += 1

        if timer >= 60 and self.kube_deployment in set(
            list_all_deployments(kube_client)
        ):
            try:
                force_delete_pods(
                    self.item.metadata.name,
                    self.kube_deployment.service,
                    self.kube_deployment.instance,
                    self.item.metadata.namespace,
                    kube_client,
                )
            except ApiException as e:
                if e.status == 404:
                    # Deployment does not exist, nothing to delete but
                    # we can consider this a success.
                    self.logging.debug(
                        "not deleting nonexistent deploy/{} from namespace/{}".format(
                            self.kube_deployment.service, self.item.metadata.namespace
                        )
                    )
                else:
                    raise
        else:
            self.logging.info(
                "deleted deploy/{} from namespace/{}".format(
                    self.kube_deployment.service, self.item.metadata.namespace
                )
            )
        self.create(kube_client=kube_client)

    def update(self, kube_client: KubeClient) -> None:
        # If HPA is enabled, do not update replicas.
        # In all other cases, replica is set to max(instances, min_instances)
        if self.soa_config.config_dict.get("bounce_method", "") == "brutal":
            threading.Thread(
                target=self.deep_delete_and_create, args=[KubeClient()]
            ).start()
            return
        if self.should_have_hpa() is None:
            self.item.spec.replicas = self.get_existing_app(kube_client).spec.replicas
        update_deployment(kube_client=kube_client, formatted_deployment=self.item)
        self.ensure_pod_disruption_budget(kube_client)
        self.sync_horizontal_pod_autoscaler(kube_client)

    def should_have_hpa(self):
        return (
            (
                self.soa_config.get_max_instances() is not None
                or self.soa_config.config_dict.get("horizontal_autoscaling", False)
            )
            # with bespoke autoscaler, setup_kubernetes_job sets the number of instances directly; no HPA is required.
            and self.soa_config.get_autoscaling_params()["decision_policy"] != "bespoke"
        )

    def sync_horizontal_pod_autoscaler(self, kube_client: KubeClient) -> None:
        """
        In order for autoscaling to work, there needs to be at least two configurations
        min_instnace, max_instance, and there cannot be instance.
        """
        self.logging.info(
            f"Syncing HPA setting for {self.item.metadata.name}/name in {self.item.metadata.namespace}"
        )
        hpa_exists = self.exists_hpa(kube_client)
        # NO autoscaling
        if not self.should_have_hpa():
            # Remove HPA if autoscaling is disabled
            if hpa_exists:
                self.delete_horizontal_pod_autoscaler(kube_client)
            return

        body = self.soa_config.get_autoscaling_metric_spec(
            name=self.item.metadata.name,
            cluster=self.soa_config.cluster,
            namespace=self.item.metadata.namespace,
        )
        if not body:
            raise Exception(
                f"CRIT: autoscaling misconfigured for {self.kube_deployment.service}."
                + f"{self.kube_deployment.instance}.Please correct the configuration and update pre-commit hook."
            )
        self.logging.debug(body)
        if not hpa_exists:
            self.logging.info(
                f"Creating new HPA for {self.item.metadata.name}/name in {self.item.metadata.namespace}"
            )
            kube_client.autoscaling.create_namespaced_horizontal_pod_autoscaler(
                namespace=self.item.metadata.namespace, body=body, pretty=True
            )
        else:
            self.logging.info(
                f"Updating new HPA for {self.item.metadata.name}/name in {self.item.metadata.namespace}/namespace"
            )
            kube_client.autoscaling.patch_namespaced_horizontal_pod_autoscaler(
                name=self.item.metadata.name,
                namespace=self.item.metadata.namespace,
                body=body,
                pretty=True,
            )

    def exists_hpa(self, kube_client: KubeClient) -> bool:
        return (
            len(
                kube_client.autoscaling.list_namespaced_horizontal_pod_autoscaler(
                    field_selector=f"metadata.name={self.item.metadata.name}",
                    namespace=self.item.metadata.namespace,
                ).items
            )
            > 0
        )

    def delete_horizontal_pod_autoscaler(self, kube_client: KubeClient) -> None:
        try:
            kube_client.autoscaling.delete_namespaced_horizontal_pod_autoscaler(
                name=self.item.metadata.name,
                namespace=self.item.metadata.namespace,
                body=V1DeleteOptions(),
            )
        except ApiException as e:
            if e.status == 404:
                # Deployment does not exist, nothing to delete but
                # we can consider this a success.
                self.logging.debug(
                    f"not deleting nonexistent HPA/{self.item.metadata.name} from namespace/{self.item.metadata.namespace}"
                )
            else:
                raise
        else:
            self.logging.info(
                "deleted HPA/{} from namespace/{}".format(
                    self.item.metadata.name, self.item.metadata.namespace
                )
            )

class StatefulSetWrapper(Application):
    def deep_delete(self, kube_client: KubeClient) -> None:
        """
        Remove all controllers, pods, and pod disruption budgets related to this application
        :param kube_client:
        """
        delete_options = V1DeleteOptions(propagation_policy="Foreground")
        try:
            kube_client.deployments.delete_namespaced_stateful_set(
                self.item.metadata.name,
                self.item.metadata.namespace,
                body=delete_options,
            )
        except ApiException as e:
            if e.status == 404:
                # StatefulSet does not exist, nothing to delete but
                # we can consider this a success.
                self.logging.debug(
                    "not deleting nonexistent statefulset/{} from namespace/{}".format(
                        self.item.metadata.name, self.item.metadata.namespace
                    )
                )
            else:
                raise
        else:
            self.logging.info(
                "deleted statefulset/{} from namespace/{}".format(
                    self.item.metadata.name, self.item.metadata.namespace
                )
            )
        self.delete_pod_disruption_budget(kube_client)

    def create(self, kube_client: KubeClient):
        create_stateful_set(kube_client=kube_client, formatted_stateful_set=self.item)
        self.ensure_pod_disruption_budget(kube_client)

    def update(self, kube_client: KubeClient):
        update_stateful_set(kube_client=kube_client, formatted_stateful_set=self.item)
        self.ensure_pod_disruption_budget(kube_client)
<|endoftext|>


<commit_before>from __future__ import unicode_literals

import os.path

import requests
from django.db import models
from lxml.html import document_fromstring

class Year(models.Model):
    """
    Represents a calendar year.

    Every year gets its own set of urls, so it's helpful to have a model just
    for the year. Not every board or commission meets every year, so knowing
    which ones actually meet saves a lot of on scraping time.
    """
    year = models.PositiveIntegerField()

    def __unicode__(self):
        return self.year

class BandC(models.Model):
    name = models.CharField(max_length=255)
    identifier = models.CharField(
        max_length=50,
        help_text='The id, probaly an auto-inc integer.')
    slug = models.SlugField(max_length=255)
    homepage = models.URLField()
    description = models.TextField(null=True, blank=True)  # allow html

    scrapable = models.BooleanField(default=True)
    years_active = models.ManyToManyField(Year)

    class Meta:
        ordering = ('name',)
        verbose_name = 'Board or Commission'
        verbose_name_plural = 'Boards and Commissions'

    def __unicode__(self):
        return self.name

    def pull_details(self):
        """Get details about a bandc you have to get from the homepage."""
        response = requests.get(self.homepage)
        assert response.ok

        doc = document_fromstring(response.text)

        # TODO description
        agenda_links = doc.xpath('//a/@href[contains(.,"cityclerk/'
                                 'boards_commissions/meetings/")]')
        if not agenda_links:
            self.scrapable = False
            self.save()
            return

        identifier = os.path.splitext(os.path.basename(
            agenda_links[0]))[0].split('_')[-2]
        self.identifier = identifier
        self.save()

class Item(models.Model):
    """An agenda item."""
    scrape_status_choices = (
        ('scraped', 'Scraped'),
        ('toscrape', 'To Scrape'),
        ('error', 'Error Scraping'),
    )

    title = models.CharField(max_length=255)
    type = models.CharField(
        max_length=50,
        help_text='Document/video, etc. scraped from the css class')

    # the date of the meeting this item came up
    date = models.DateField()

    # when this data was scraped
    scraped_at = models.DateTimeField(auto_now_add=True)

    # Mark data as old so it can be deleted
    dirty = models.BooleanField(default=False)

    # Has the text been extracted? TODO base this on if `text` is null
    # True   scraped
    # False  not scraped
    # None   error scraping
    scrape_status = models.CharField(
        choices=scrape_status_choices, default='no', max_length=20,
    )

    thumbnail = models.URLField(null=True, blank=True)

    text = models.TextField(
        null=True, blank=True,
        help_text='The text scraped from the pdf')

    url = models.URLField()

    bandc = models.ForeignKey(BandC)

    def __unicode__(self):
        return '{} {}'.format(self.title, self.url)

    @property
    def edims_id(self):
        """Get the EDIMS id associate with the document or None."""
        if self.url.startswith('http://www.austintexas.gov/edims/document'):
            return self.url.rsplit('=', 2)[-1]
        return None
<commit_msg>prep for scraping documents<commit_after>from __future__ import unicode_literals

import os.path

import requests
from django.db import models
from lxml.html import document_fromstring

class Year(models.Model):
    """
    Represents a calendar year.

    Every year gets its own set of urls, so it's helpful to have a model just
    for the year. Not every board or commission meets every year, so knowing
    which ones actually meet saves a lot of on scraping time.
    """
    year = models.PositiveIntegerField()

    def __unicode__(self):
        return self.year

class BandC(models.Model):
    name = models.CharField(max_length=255)
    identifier = models.CharField(
        max_length=50,
        help_text='The id, probaly an auto-inc integer.')
    slug = models.SlugField(max_length=255)
    homepage = models.URLField()
    description = models.TextField(null=True, blank=True)  # allow html

    scrapable = models.BooleanField(default=True)
    scraped_at = models.DateTimeField(
        null=True, blank=True,
        help_text='The last time documents were scraped.')
    years_active = models.ManyToManyField(Year)

    class Meta:
        ordering = ('name',)
        verbose_name = 'Board or Commission'
        verbose_name_plural = 'Boards and Commissions'

    def __unicode__(self):
        return self.name

    def pull_details(self):
        """Get details about a bandc you have to get from the homepage."""
        response = requests.get(self.homepage)
        assert response.ok

        doc = document_fromstring(response.text)

        # TODO description
        agenda_links = doc.xpath('//a/@href[contains(.,"cityclerk/'
                                 'boards_commissions/meetings/")]')
        if not agenda_links:
            self.scrapable = False
            self.save()
            return

        identifier = os.path.splitext(os.path.basename(
            agenda_links[0]))[0].split('_')[-2]
        self.identifier = identifier
        self.save()

class Meeting(models.Model):
    title = models.CharField(max_length=255)
    date = models.DateField()
    bandc = models.ForeignKey(BandC)

    class Meta:
        ordering = ('date',)

    def __unicode__(self):
        return self.title

class Document(models.Model):
    """
    A meeting document.
    """
    scrape_status_choices = (
        ('scraped', 'Scraped'),
        ('toscrape', 'To Scrape'),
        ('error', 'Error Scraping'),
    )

    title = models.CharField(max_length=255)
    type = models.CharField(
        max_length=50,
        help_text='Document/video, etc. scraped from the css class')

    meeting = models.ForeignKey(Meeting)

    # when this data was scraped
    scraped_at = models.DateTimeField(auto_now_add=True)

    # Mark data as old so it can be deleted
    dirty = models.BooleanField(default=False)

    # Has the text been extracted? TODO base this on if `text` is null
    # True   scraped
    # False  not scraped
    # None   error scraping
    scrape_status = models.CharField(
        choices=scrape_status_choices, default='no', max_length=20,
    )

    thumbnail = models.URLField(null=True, blank=True)

    text = models.TextField(
        null=True, blank=True,
        help_text='The text scraped from the pdf')

    url = models.URLField()

    bandc = models.ForeignKey(BandC)

    def __unicode__(self):
        return '{} {}'.format(self.title, self.url)

    @property
    def edims_id(self):
        """Get the EDIMS id associate with the document or None."""
        if self.url.startswith('http://www.austintexas.gov/edims/document'):
            return self.url.rsplit('=', 2)[-1]
        return None
<|endoftext|>


<commit_before>from django_prometheus.testutils import PrometheusTestCaseMixin
from django.test import TestCase
from django.db import connections

class TestDbMetrics(PrometheusTestCaseMixin, TestCase):
    """Test django_prometheus.db metrics.

    Note regarding the values of metrics: many tests interact with the
    database, and the test runner itself does. As such, tests that
    require that a metric has a specific value are at best very
    fragile. Consider asserting that the value exceeds a certain
    threshold, or check by how much it increased during the test.
    """
    def setUp(self):
        pass

    def testConfigHasExpectedDatabases(self):
        """Not a real unit test: ensures that testapp.settings contains the
        databases this test expects."""
        self.assertTrue('default' in connections.databases.keys())
        self.assertTrue('test_db_1' in connections.databases.keys())
        self.assertTrue('test_db_2' in connections.databases.keys())

    def testCounters(self):
        cursor_db1 = connections['test_db_1'].cursor()
        cursor_db2 = connections['test_db_2'].cursor()
        cursor_db1.execute('SELECT 1')
        for _ in range(200):
            cursor_db2.execute('SELECT 2')
        cursor_db1.execute('SELECT 3')
        try:
            cursor_db1.execute('this is clearly not valid SQL')
        except:
            pass

        self.assertTrue(self.getMetric(
            'django_db_new_connections_total',
            alias='default', vendor='sqlite') > 0)
        self.assertTrue(self.getMetric(
            'django_db_new_connections_total',
            alias='test_db_1', vendor='sqlite') > 0)
        self.assertTrue(self.getMetric(
            'django_db_new_connections_total',
            alias='test_db_2', vendor='sqlite') > 0)

        self.assertMetricEquals(
            1, 'django_db_errors_total',
            alias='test_db_1', vendor='sqlite', type='OperationalError')

        self.assertTrue(self.getMetric(
            'django_db_execute_total',
            alias='test_db_1', vendor='sqlite') > 0)
        self.assertTrue(self.getMetric(
            'django_db_execute_total',
            alias='test_db_2', vendor='sqlite') > 200)

    def testExecuteMany(self):
        cursor_db1 = connections['test_db_1'].cursor()
        cursor_db1.executemany(
            'INSERT INTO testapp_lawn(location) VALUES (?)', [
                ('Paris',), ('New York',), ('Berlin',), ('San Francisco',)])
        self.assertTrue(self.getMetric(
            'django_db_execute_many_total',
            alias='test_db_1', vendor='sqlite') >= 4)
<commit_msg>Remove the test for the db_new_connections_total metric.<commit_after>from django_prometheus.testutils import PrometheusTestCaseMixin
from django.test import TestCase
from django.db import connections

class TestDbMetrics(PrometheusTestCaseMixin, TestCase):
    """Test django_prometheus.db metrics.

    Note regarding the values of metrics: many tests interact with the
    database, and the test runner itself does. As such, tests that
    require that a metric has a specific value are at best very
    fragile. Consider asserting that the value exceeds a certain
    threshold, or check by how much it increased during the test.
    """
    def setUp(self):
        pass

    def testConfigHasExpectedDatabases(self):
        """Not a real unit test: ensures that testapp.settings contains the
        databases this test expects."""
        self.assertTrue('default' in connections.databases.keys())
        self.assertTrue('test_db_1' in connections.databases.keys())
        self.assertTrue('test_db_2' in connections.databases.keys())

    def testCounters(self):
        cursor_db1 = connections['test_db_1'].cursor()
        cursor_db2 = connections['test_db_2'].cursor()
        cursor_db1.execute('SELECT 1')
        for _ in range(200):
            cursor_db2.execute('SELECT 2')
        cursor_db1.execute('SELECT 3')
        try:
            cursor_db1.execute('this is clearly not valid SQL')
        except:
            pass

        self.assertMetricEquals(
            1, 'django_db_errors_total',
            alias='test_db_1', vendor='sqlite', type='OperationalError')

        self.assertTrue(self.getMetric(
            'django_db_execute_total',
            alias='test_db_1', vendor='sqlite') > 0)
        self.assertTrue(self.getMetric(
            'django_db_execute_total',
            alias='test_db_2', vendor='sqlite') >= 200)

    def testExecuteMany(self):
        cursor_db1 = connections['test_db_1'].cursor()
        cursor_db1.executemany(
            'INSERT INTO testapp_lawn(location) VALUES (?)', [
                ('Paris',), ('New York',), ('Berlin',), ('San Francisco',)])
        self.assertTrue(self.getMetric(
            'django_db_execute_many_total',
            alias='test_db_1', vendor='sqlite') >= 4)
<|endoftext|>


<commit_before># Raspberry PI Machine Control / Sensor Demo for Murano platform
import sys
import time
import json
import datetime
import grovepi
from grove_rgb_lcd import *

sensor = 7

from murano import Murano
import requests

if len(sys.argv) < 3:
    print('Usage:')
    print('   python ./device_app <product_id> <device_id>');

PRODUCT_ID = sys.argv[1] # 'do5pjwoazfsf9a4i'
DEVICE_ID = sys.argv[2]  # '001'

def show_state():
    print('Product ID: {0} Device ID: {1}'.format(PRODUCT_ID, DEVICE_ID))

# create a murano object for the device
murano = Murano(PRODUCT_ID, DEVICE_ID)

# Get the device key (CIK)
try:
    cik = murano.load_cik()
    print('Loaded CIK from file {0}'.format(murano.filename))
except EnvironmentError:
    # no stored CIK, so activate
    cik = murano.activate()
    print('Activated device to obtain CIK')

# initialize the device state of of Pi
class State:
    battery_percent = 100
    cpu_usage = 0
    mem_used = 0
    run_time = 0

# initialize the sensor state
class Sensors:
    temperature = 0
    humidity = 0
    sound = 0
    light = 0

#stats.update_stats()

# write current state of the lock to Murano
#writes = {'battery-percent': State.battery_percent,
#          'lock-state': State.lock_state}
#murano.write(writes)

def print_state():
    print('---------------------------------')
    print('product id:      {0}'.format(PRODUCT_ID))
    print('device id:       {0}'.format(DEVICE_ID))
    print('run time:        {0}'.format(State.run_time))
    print('cpu usage:       {0}'.format(State.cpu_usage))
    print('mem used:        {0}'.format(State.mem_used))
    print('temperature:     {0}'.format(Sensors.temperature))
    print('humidity:        {0}'.format(Sensors.humidity))
    print('pressure:        {0}'.format(Sensors.pressure))
    print('---------------------------------')

print('Press Ctrl+C to quit')
timestamp = None

period_sensor_time = 2 #how many seconds to send Sensors
last_sensor_time = 0 # when was last time we sent sensor data

start_time = int(time.time())

while True:
    #stats.update_stats()
    #meminfo = stats.get_memory_info()
    #print "total\tused\tfree\tcached"
    #print "%i\t%i\t%i\t%i"%(meminfo['total'],meminfo['used'],meminfo['free'],meminfo['cached'])
    #print "Memory Usage:\t%i%%"%(meminfo['percent'])
    #print "\n"
    #cpu_info = stats.get_cpu_info()
    #print "CPU Usage:\t%i%%"%(cpu_info['percent'])

    #State.cpu_usage = int(cpu_info['percent'])
    #State.mem_used = int(meminfo['used'])

    State.run_time = int(time.time())-start_time

    # TIME TO SEND PERIODIC SENSOR DATA
    if time.time() - last_sensor_time > 10:

        now = datetime.datetime.utcnow()
        temp = 0.01
        hum = 0.01
        [temp,humidity] = grovepi.dht(sensor,1)
        light=int(grovepi.analogRead(0)/10.24)
        sound=int(grovepi.analogRead(1)/10.24)

        print(temp,humidity,light,sound)

        Sensors.temperature = round(temp, 1)
        Sensors.light = round(light, 1)
        Sensors.humidity = round(humidity, 1)
        Sensors.sound = round(sound, 1)

        setRGB(0,128,64)
        setRGB(0,255,0)
        setText("Temp:" + str(temp) + "C      " + "Humidity :" + str(humidity) + "%")

        rawdata = json.dumps(Sensors)
        #rawdata  = '{'
        #+ '"temperature":' + str(Sensors.temperature)
        #+ ', "sound":' + str(Sensors.sound)
        #+ ', "light":' + str(Sensors.light)
        #+ ', "humidity":'+ str(Sensors.humidity)
        #+', "runtime":'+ str(State.run_time)
        #+'}'

        writes = {}
        writes['raw_data'] = rawdata

        # send current states up to Murano
        try:
            #murano.write(writes)
            print(writes)
        except requests.exceptions.RequestException as e:
            print str(e)
            last_sensor_time = time.time()

    time.sleep(0.2);
<commit_msg>fix structure to json problem<commit_after># Raspberry PI Machine Control / Sensor Demo for Murano platform
import sys
import time
import json
import datetime
import grovepi
from grove_rgb_lcd import *

sensor = 7

from murano import Murano
import requests

if len(sys.argv) < 3:
    print('Usage:')
    print('   python ./device_app <product_id> <device_id>');

PRODUCT_ID = sys.argv[1] # 'do5pjwoazfsf9a4i'
DEVICE_ID = sys.argv[2]  # '001'

def show_state():
    print('Product ID: {0} Device ID: {1}'.format(PRODUCT_ID, DEVICE_ID))

# create a murano object for the device
murano = Murano(PRODUCT_ID, DEVICE_ID)

# Get the device key (CIK)
try:
    cik = murano.load_cik()
    print('Loaded CIK from file {0}'.format(murano.filename))
except EnvironmentError:
    # no stored CIK, so activate
    cik = murano.activate()
    print('Activated device to obtain CIK')

#stats.update_stats()

# write current state of the lock to Murano
#writes = {'battery-percent': State.battery_percent,
#          'lock-state': State.lock_state}
#murano.write(writes)

print('Press Ctrl+C to quit')
timestamp = None

period_sensor_time = 2 #how many seconds to send Sensors
last_sensor_time = 0 # when was last time we sent sensor data

start_time = int(time.time())
run_time = int(time.time())-start_time

while True:
    #stats.update_stats()
    #meminfo = stats.get_memory_info()
    #print "total\tused\tfree\tcached"
    #print "%i\t%i\t%i\t%i"%(meminfo['total'],meminfo['used'],meminfo['free'],meminfo['cached'])
    #print "Memory Usage:\t%i%%"%(meminfo['percent'])
    #print "\n"
    #cpu_info = stats.get_cpu_info()
    #print "CPU Usage:\t%i%%"%(cpu_info['percent'])

    #State.cpu_usage = int(cpu_info['percent'])
    #State.mem_used = int(meminfo['used'])

    run_time = int(time.time())-start_time

    # TIME TO SEND PERIODIC SENSOR DATA
    if time.time() - last_sensor_time > 10:

        now = datetime.datetime.utcnow()
        temp = 0.01
        hum = 0.01
        [temp,humidity] = grovepi.dht(sensor,1)
        light=int(grovepi.analogRead(0)/10.24)
        sound=int(grovepi.analogRead(1)/10.24)

        print(temp,humidity,light,sound)

        setRGB(0,128,64)
        setRGB(0,255,0)
        setText("Temp:" + str(temp) + "C      " + "Humidity :" + str(humidity) + "%")

        data = {}

        data['runtime'] = run_time

        rawdata = json.dumps(data)
        #rawdata  = '{'
        #+ '"temperature":' + str(Sensors.temperature)
        #+ ', "sound":' + str(Sensors.sound)
        #+ ', "light":' + str(Sensors.light)
        #+ ', "humidity":'+ str(Sensors.humidity)
        #+', "runtime":'+ str(State.run_time)
        #+'}'

        writes = {}
        writes['raw_data'] = rawdata

        # send current states up to Murano
        try:
            #murano.write(writes)
            print(writes)
        except requests.exceptions.RequestException as e:
            print str(e)
            last_sensor_time = time.time()

    time.sleep(0.2);
<|endoftext|>


<commit_before>import json
from tempodb.temporal.validate import convert_iso_stamp, check_time_param
from cursor import DataPointCursor, SeriesCursor

class JSONSerializable(object):
    """Base class for objects that are serializable to and from JSON.
    This class defines default methods for serializing each way that use
    the class's "properties" class variable to determine what should be
    serialized or deserialized.  For example::

        class MySerialized(JSONSerializable)
            properties = ['foo', 'bar']

    This would define a class that expects to have the 'foo' and 'bar'
    keys in JSON data and would likewise serialize a JSON object with
    those keys.

    The base constructor calls the :meth:`from_json` method, which
    enforces these constraints for object construction.  If you override
    this constructor (for example, to provide static initialization of
    some variables), it is highly recommended that the subclass constructor
    call this constructor at some point through super().

    :param string json_text: the JSON string to deserialize from"""

    properties = []

    def __init__(self, json_text, response):
        self.from_json(json_text)
        self.response = response

    def from_json(self, json_text):
        """Deserialize a JSON object into this object.  This method will
        check that the JSON object has the required keys and will set each
        of the keys in that JSON object as an instance attribute of this
        object.

        :param json_text: the JSON text or object to deserialize from
        :type json_text: dict or string
        :raises ValueError: if the JSON object lacks an expected key
        :rtype: None"""

        #due to the architecture of response parsing, particularly
        #where the API returns lists, the JSON might have already been
        #parsed by the time it gets here
        if type(json_text) in [str, unicode]:
            j = json.loads(json_text)
        else:
            j = json_text

        try:
            for p in self.properties:
                setattr(self, p, j[p])
        except KeyError, e:
            msg = 'Expected key %s in JSON object, found None' % str(e)
            raise ValueError(msg)

    def to_json(self):
        """Serialize an object to JSON based on its "properties" class
        attribute.

        :rtype: string"""

        j = {}
        for p in self.properties:
            j[p] = getattr(self, p)

        return json.dumps(j)

    def to_dictionary(self):
        """Serialize an object into dictionary form.  Useful if you have to
        serialize an array of objects into JSON.  Otherwise, if you call the
        :meth:`to_json` method on each object in the list and then try to
        dump the array, you end up with an array with one string."""

        j = {}
        for p in self.properties:
            j[p] = getattr(self, p)

        return j

#PLACEHOLDER FOR EMPTY RESPONSES
class Nothing(object):
    """Used to represent empty responses.  This class should not be
    used directly in user code."""

    def __init__(self, *args, **kwargs):
        pass

class SeriesSet(object):
    """Represents a set of Series objects as returned by the list series
    TempoDB API endpoint.  The SeriesSet exposes a cursor that can be iterated
    over to examine each series return by the API."""

    def __init__(self, json_text, response):
        self.data = json.loads(json_text)
        self.cursor = SeriesCursor(self.data, Series, response)

class Series(JSONSerializable):
    """Represents a Series object from the TempoDB API.  Series objects
    are serialized to and from JSON using the :meth:`to_json` and
    :meth:`from_json` methods."""

    properties = ['key', 'name', 'tags', 'attributes']

    def __init__(self, json_text, response):
        #the formatting of the series object returned from the series by key
        #endpoint is slightly different
        if type(json_text) == str:
            j = json.loads(json_text)
        else:
            j = json_text
        if 'series' in j:
            self.from_json(j['series'])
        else:
            self.from_json(json_text)
        self.response = response

class DataSet(JSONSerializable):
    """Represents a data set returned using the /data resource in the
    TempoDB API.  Depending on the original API call, some attributes of
    this object (such as rollup) could be None."""

    properties = ['data', 'rollup', 'tz']

    def __init__(self, json_text, response):
        #override to force the instantiation of a cursor
        super(DataSet, self).__init__(json_text, response)
        self.cursor = DataPointCursor(self.data, DataPoint, response)
        if self.rollup is not None:
            self.rollup = Rollup(self.rollup)

class SingleValue(JSONSerializable):
    """Represents a data set returned by calling the single value
    endpoint of the TempoDB API.  This domain object is not cursored, so
    it is implemented separately from the more generic DataSet object."""

    properties = ['series', 'data']

    def __init__(self, json_text, response):
        #force conversion of the subobjects in this datatype after we get
        #them
        super(SingleValue, self).__init__(json_text, response)
        self.series = Series(self.series, response)
        if self.data is not None:
            self.data = DataPoint(self.data, response)

class Summary(JSONSerializable):
    """Represents the summary received from the TempoDB API when a data read
    request is sent.  The properties are summary statistics for the dataset
    returned."""

    properties = ['mean', 'sum', 'min', 'max', 'stddev', 'ss', 'count']

class Rollup(JSONSerializable):
    """Represents the rollup information returned from the TempoDB API when
    the API calls demands it."""

    properties = ['interval', 'function', 'tz']

class DataPoint(JSONSerializable):
    """Represents a single data point in a series.  To construct these objects
    in user code, use the class method :meth:`from_data`."""

    properties = ['t', 'v', 'key', 'id']

    def __init__(self, json_text, response, tz=None):
        self.tz = tz
        super(DataPoint, self).__init__(json_text, response)

    @classmethod
    def from_data(self, time, value, series_id=None, key=None, tz=None):
        """Create a DataPoint object from data, rather than a JSON object or
        string.  This should be used by user code to construct DataPoints from
        Python-based data like Datetime objects and floats.

        The series_id and key arguments are only necessary if you are doing a
        multi write, in which case those arguments can be used to specify which
        series the DataPoint belongs to.

        If needed, the tz argument should be an Olsen database compliant string
        specifying the time zone for this DataPoint.  This argument is most
        often used internally when reading data from TempoDB.

        :param time: the point in time for this reading
        :type time: ISO8601 string or Datetime
        :param value: the value for this reading
        :type value: int or float
        :param string series_id: (optional) a series ID for this point
        :param string key: (optional) a key for this point
        :param string tz: (optional) a timezone for this point
        :rtype: :class:`DataPoint`"""

        t = check_time_param(time)
        if type(value) in [float, int]:
            v = value
        else:
            raise ValueError('Values must be int or float. Got "%s".' %
                             str(value))

        j = {
            't': t,
            'v': v,
            'id': series_id,
            'key': key
        }
        return DataPoint(j, None, tz=tz)

    def from_json(self, json_text):
        """Deserialize a JSON object into this object.  This method will
        check that the JSON object has the required keys and will set each
        of the keys in that JSON object as an instance attribute of this
        object.

        :param json_text: the JSON text or object to deserialize from
        :type json_text: dict or string
        :raises ValueError: if the JSON object lacks an expected key
        :rtype: None"""

        if type(json_text) in [str, unicode]:
            j = json.loads(json_text)
        else:
            j = json_text

        try:
            for p in self.properties:
                if p == 't':
                    val = convert_iso_stamp(j[p], self.tz)
                    setattr(self, p, val)
                else:
                    setattr(self, p, j[p])
        #overriding this exception allows us to handle optional values like
        #id and key which are only present during particular API calls like
        #multi writes
        except KeyError:
            pass

    def to_json(self):
        """Serialize an object to JSON based on its "properties" class
        attribute.

        :rtype: string"""

        j = {}
        for p in self.properties:
            #this logic change allows us to work with optional values for
            #this data type
            try:
                v = getattr(self, p)
            except AttributeError:
                continue
            if v is not None:
                if p == 't':
                    j[p] = getattr(self, p).isoformat()
                else:
                    j[p] = getattr(self, p)

        return json.dumps(j)

    def to_dictionary(self):
        """Serialize an object into dictionary form.  Useful if you have to
        serialize an array of objects into JSON.  Otherwise, if you call the
        :meth:`to_json` method on each object in the list and then try to
        dump the array, you end up with an array with one string."""

        j = {}
        for p in self.properties:
            try:
                v = getattr(self, p)
            except AttributeError:
                continue
            if v is not None:
                if p == 't':
                    j[p] = getattr(self, p).isoformat()
                else:
                    j[p] = getattr(self, p)

        return j

class DataPointFound(JSONSerializable):
    properties = ['interval', 't', 'v']

    def __init__(self, json_text, response, tz=None):
        self.tz = tz
        super(DataPointFound, self).__init__(json_text, response)

    def from_json(self, json_text):
        """Deserialize a JSON object into this object.  This method will
        check that the JSON object has the required keys and will set each
        of the keys in that JSON object as an instance attribute of this
        object.

        :param json_text: the JSON text or object to deserialize from
        :type json_text: dict or string
        :raises ValueError: if the JSON object lacks an expected key
        :rtype: None"""

        if type(json_text) in [str, unicode]:
            j = json.loads(json_text)
        else:
            j = json_text

        try:
            for p in self.properties:
                if p == 'interval':
                    self.start = convert_iso_stamp(j[p]['start'])
                    self.end = convert_iso_stamp(j[p]['end'])
                elif p == 't':
                    t = convert_iso_stamp(j[p], self.tz)
                    setattr(self, 't', t)
                elif p == 'v':
                    v = j[p]
                    setattr(self, 'v', v)
        #overriding this exception allows us to handle optional values like
        #id and key which are only present during particular API calls like
        #multi writes
        except KeyError:
            pass

class MultiPoint(JSONSerializable):
    properties = ['t', 'v']

    def __init__(self, json_text, response, tz=None):
        self.tz = tz
        super(MultiPoint, self).__init__(json_text, response)

    def from_json(self, json_text):
        """Deserialize a JSON object into this object.  This method will
        check that the JSON object has the required keys and will set each
        of the keys in that JSON object as an instance attribute of this
        object.

        :param json_text: the JSON text or object to deserialize from
        :type json_text: dict or string
        :raises ValueError: if the JSON object lacks an expected key
        :rtype: None"""

        if type(json_text) in [str, unicode]:
            j = json.loads(json_text)
        else:
            j = json_text

        try:
            for p in self.properties:
                if p == 't':
                    t = convert_iso_stamp(j[p], self.tz)
                    setattr(self, 't', t)
                else:
                    setattr(self, p, j[p])
        #overriding this exception allows us to handle optional values like
        #id and key which are only present during particular API calls like
        #multi writes
        except KeyError:
            pass

    def to_json(self):
        """Serialize an object to JSON based on its "properties" class
        attribute.

        :rtype: string"""

        j = {}
        for p in self.properties:
            try:
                v = getattr(self, p)
            except AttributeError:
                continue
            if v is not None:
                if p == 't':
                    j[p] = getattr(self, p).isoformat()
                else:
                    j[p] = getattr(self, p)

        return json.dumps(j)

    def get(self, k):
        """Convenience method for getting values for individual series out of
        the MultiPoint.  This is equivalent to calling::

            >>> point.v.get('foo')

        :param string k: the key to read
        :rtype: number"""

        return self.v.get(k)
<commit_msg>update find object to latest protocol<commit_after>import json
from tempodb.temporal.validate import convert_iso_stamp, check_time_param
from cursor import DataPointCursor, SeriesCursor

class JSONSerializable(object):
    """Base class for objects that are serializable to and from JSON.
    This class defines default methods for serializing each way that use
    the class's "properties" class variable to determine what should be
    serialized or deserialized.  For example::

        class MySerialized(JSONSerializable)
            properties = ['foo', 'bar']

    This would define a class that expects to have the 'foo' and 'bar'
    keys in JSON data and would likewise serialize a JSON object with
    those keys.

    The base constructor calls the :meth:`from_json` method, which
    enforces these constraints for object construction.  If you override
    this constructor (for example, to provide static initialization of
    some variables), it is highly recommended that the subclass constructor
    call this constructor at some point through super().

    :param string json_text: the JSON string to deserialize from"""

    properties = []

    def __init__(self, json_text, response):
        self.from_json(json_text)
        self.response = response

    def from_json(self, json_text):
        """Deserialize a JSON object into this object.  This method will
        check that the JSON object has the required keys and will set each
        of the keys in that JSON object as an instance attribute of this
        object.

        :param json_text: the JSON text or object to deserialize from
        :type json_text: dict or string
        :raises ValueError: if the JSON object lacks an expected key
        :rtype: None"""

        #due to the architecture of response parsing, particularly
        #where the API returns lists, the JSON might have already been
        #parsed by the time it gets here
        if type(json_text) in [str, unicode]:
            j = json.loads(json_text)
        else:
            j = json_text

        try:
            for p in self.properties:
                setattr(self, p, j[p])
        except KeyError, e:
            msg = 'Expected key %s in JSON object, found None' % str(e)
            raise ValueError(msg)

    def to_json(self):
        """Serialize an object to JSON based on its "properties" class
        attribute.

        :rtype: string"""

        j = {}
        for p in self.properties:
            j[p] = getattr(self, p)

        return json.dumps(j)

    def to_dictionary(self):
        """Serialize an object into dictionary form.  Useful if you have to
        serialize an array of objects into JSON.  Otherwise, if you call the
        :meth:`to_json` method on each object in the list and then try to
        dump the array, you end up with an array with one string."""

        j = {}
        for p in self.properties:
            j[p] = getattr(self, p)

        return j

#PLACEHOLDER FOR EMPTY RESPONSES
class Nothing(object):
    """Used to represent empty responses.  This class should not be
    used directly in user code."""

    def __init__(self, *args, **kwargs):
        pass

class SeriesSet(object):
    """Represents a set of Series objects as returned by the list series
    TempoDB API endpoint.  The SeriesSet exposes a cursor that can be iterated
    over to examine each series return by the API."""

    def __init__(self, json_text, response):
        self.data = json.loads(json_text)
        self.cursor = SeriesCursor(self.data, Series, response)

class Series(JSONSerializable):
    """Represents a Series object from the TempoDB API.  Series objects
    are serialized to and from JSON using the :meth:`to_json` and
    :meth:`from_json` methods."""

    properties = ['key', 'name', 'tags', 'attributes']

    def __init__(self, json_text, response):
        #the formatting of the series object returned from the series by key
        #endpoint is slightly different
        if type(json_text) == str:
            j = json.loads(json_text)
        else:
            j = json_text
        if 'series' in j:
            self.from_json(j['series'])
        else:
            self.from_json(json_text)
        self.response = response

class DataSet(JSONSerializable):
    """Represents a data set returned using the /data resource in the
    TempoDB API.  Depending on the original API call, some attributes of
    this object (such as rollup) could be None."""

    properties = ['data', 'rollup', 'tz']

    def __init__(self, json_text, response):
        #override to force the instantiation of a cursor
        super(DataSet, self).__init__(json_text, response)
        self.cursor = DataPointCursor(self.data, DataPoint, response)
        if self.rollup is not None:
            self.rollup = Rollup(self.rollup)

class SingleValue(JSONSerializable):
    """Represents a data set returned by calling the single value
    endpoint of the TempoDB API.  This domain object is not cursored, so
    it is implemented separately from the more generic DataSet object."""

    properties = ['series', 'data']

    def __init__(self, json_text, response):
        #force conversion of the subobjects in this datatype after we get
        #them
        super(SingleValue, self).__init__(json_text, response)
        self.series = Series(self.series, response)
        if self.data is not None:
            self.data = DataPoint(self.data, response)

class Summary(JSONSerializable):
    """Represents the summary received from the TempoDB API when a data read
    request is sent.  The properties are summary statistics for the dataset
    returned."""

    properties = ['mean', 'sum', 'min', 'max', 'stddev', 'ss', 'count']

class Rollup(JSONSerializable):
    """Represents the rollup information returned from the TempoDB API when
    the API calls demands it."""

    properties = ['interval', 'function', 'tz']

class DataPoint(JSONSerializable):
    """Represents a single data point in a series.  To construct these objects
    in user code, use the class method :meth:`from_data`."""

    properties = ['t', 'v', 'key', 'id']

    def __init__(self, json_text, response, tz=None):
        self.tz = tz
        super(DataPoint, self).__init__(json_text, response)

    @classmethod
    def from_data(self, time, value, series_id=None, key=None, tz=None):
        """Create a DataPoint object from data, rather than a JSON object or
        string.  This should be used by user code to construct DataPoints from
        Python-based data like Datetime objects and floats.

        The series_id and key arguments are only necessary if you are doing a
        multi write, in which case those arguments can be used to specify which
        series the DataPoint belongs to.

        If needed, the tz argument should be an Olsen database compliant string
        specifying the time zone for this DataPoint.  This argument is most
        often used internally when reading data from TempoDB.

        :param time: the point in time for this reading
        :type time: ISO8601 string or Datetime
        :param value: the value for this reading
        :type value: int or float
        :param string series_id: (optional) a series ID for this point
        :param string key: (optional) a key for this point
        :param string tz: (optional) a timezone for this point
        :rtype: :class:`DataPoint`"""

        t = check_time_param(time)
        if type(value) in [float, int]:
            v = value
        else:
            raise ValueError('Values must be int or float. Got "%s".' %
                             str(value))

        j = {
            't': t,
            'v': v,
            'id': series_id,
            'key': key
        }
        return DataPoint(j, None, tz=tz)

    def from_json(self, json_text):
        """Deserialize a JSON object into this object.  This method will
        check that the JSON object has the required keys and will set each
        of the keys in that JSON object as an instance attribute of this
        object.

        :param json_text: the JSON text or object to deserialize from
        :type json_text: dict or string
        :raises ValueError: if the JSON object lacks an expected key
        :rtype: None"""

        if type(json_text) in [str, unicode]:
            j = json.loads(json_text)
        else:
            j = json_text

        try:
            for p in self.properties:
                if p == 't':
                    val = convert_iso_stamp(j[p], self.tz)
                    setattr(self, p, val)
                else:
                    setattr(self, p, j[p])
        #overriding this exception allows us to handle optional values like
        #id and key which are only present during particular API calls like
        #multi writes
        except KeyError:
            pass

    def to_json(self):
        """Serialize an object to JSON based on its "properties" class
        attribute.

        :rtype: string"""

        j = {}
        for p in self.properties:
            #this logic change allows us to work with optional values for
            #this data type
            try:
                v = getattr(self, p)
            except AttributeError:
                continue
            if v is not None:
                if p == 't':
                    j[p] = getattr(self, p).isoformat()
                else:
                    j[p] = getattr(self, p)

        return json.dumps(j)

    def to_dictionary(self):
        """Serialize an object into dictionary form.  Useful if you have to
        serialize an array of objects into JSON.  Otherwise, if you call the
        :meth:`to_json` method on each object in the list and then try to
        dump the array, you end up with an array with one string."""

        j = {}
        for p in self.properties:
            try:
                v = getattr(self, p)
            except AttributeError:
                continue
            if v is not None:
                if p == 't':
                    j[p] = getattr(self, p).isoformat()
                else:
                    j[p] = getattr(self, p)

        return j

class DataPointFound(JSONSerializable):
    properties = ['interval', 'found']

    def __init__(self, json_text, response, tz=None):
        self.tz = tz
        super(DataPointFound, self).__init__(json_text, response)

    def from_json(self, json_text):
        """Deserialize a JSON object into this object.  This method will
        check that the JSON object has the required keys and will set each
        of the keys in that JSON object as an instance attribute of this
        object.

        :param json_text: the JSON text or object to deserialize from
        :type json_text: dict or string
        :raises ValueError: if the JSON object lacks an expected key
        :rtype: None"""

        if type(json_text) in [str, unicode]:
            j = json.loads(json_text)
        else:
            j = json_text

        try:
            for p in self.properties:
                if p == 'interval':
                    self.start = convert_iso_stamp(j[p]['start'])
                    self.end = convert_iso_stamp(j[p]['end'])
                elif p == 'found':
                    t = convert_iso_stamp(j[p]['t'], self.tz)
                    setattr(self, 't', t)
                    v = j[p]['v']
                    setattr(self, 'v', v)
        #overriding this exception allows us to handle optional values like
        #id and key which are only present during particular API calls like
        #multi writes
        except KeyError:
            pass

class MultiPoint(JSONSerializable):
    properties = ['t', 'v']

    def __init__(self, json_text, response, tz=None):
        self.tz = tz
        super(MultiPoint, self).__init__(json_text, response)

    def from_json(self, json_text):
        """Deserialize a JSON object into this object.  This method will
        check that the JSON object has the required keys and will set each
        of the keys in that JSON object as an instance attribute of this
        object.

        :param json_text: the JSON text or object to deserialize from
        :type json_text: dict or string
        :raises ValueError: if the JSON object lacks an expected key
        :rtype: None"""

        if type(json_text) in [str, unicode]:
            j = json.loads(json_text)
        else:
            j = json_text

        try:
            for p in self.properties:
                if p == 't':
                    t = convert_iso_stamp(j[p], self.tz)
                    setattr(self, 't', t)
                else:
                    setattr(self, p, j[p])
        #overriding this exception allows us to handle optional values like
        #id and key which are only present during particular API calls like
        #multi writes
        except KeyError:
            pass

    def to_json(self):
        """Serialize an object to JSON based on its "properties" class
        attribute.

        :rtype: string"""

        j = {}
        for p in self.properties:
            try:
                v = getattr(self, p)
            except AttributeError:
                continue
            if v is not None:
                if p == 't':
                    j[p] = getattr(self, p).isoformat()
                else:
                    j[p] = getattr(self, p)

        return json.dumps(j)

    def get(self, k):
        """Convenience method for getting values for individual series out of
        the MultiPoint.  This is equivalent to calling::

            >>> point.v.get('foo')

        :param string k: the key to read
        :rtype: number"""

        return self.v.get(k)
<|endoftext|>


<commit_before>import asyncio
import traceback

from appdaemon.appdaemon import AppDaemon

class ThreadAsync:

    """
    Module to translate from the thread world to the async world via queues
    """

    def __init__(self, ad: AppDaemon):

        self.AD = ad
        self.stopping = False
        self.logger = ad.logging.get_child("_thread_async")
        #
        # Initial Setup
        #

        self.appq = asyncio.Queue(maxsize=0)

    def stop(self):
        self.logger.debug("stop() called for thread_async")
        self.stopping = True
        # Queue a fake event to make the loop wake up and exit
        self.appq.put_nowait({"stop": True})

    async def loop(self):
        while not self.stopping:
            args = None
            try:
                args = await self.appq.get()
                if "stop" not in args:
                    self.logger.debug("thread_async loop, args=%s", args)
                    function = args["function"]
                    myargs = args["args"]
                    mykwargs = args["kwargs"]
                    asyncio.ensure_future(function(*myargs, **mykwargs))
                    # self.logger.debug("calling task_done()")
                    # self.appq.task_done()
            except:
                self.logger.warning("-" * 60)
                self.logger.warning("Unexpected error during thread_async() loop()")
                self.logger.warning("args: %s", args)
                self.logger.warning("-" * 60)
                self.logger.warning(traceback.format_exc())
                self.logger.warning("-" * 60)

    def call_async_no_wait(self, function, *args, **kwargs):
        self.appq.put_nowait({"function": function, "args": args, "kwargs": kwargs})
<commit_msg>fix flake8 issues for appdaemon/thread_async.py<commit_after>import asyncio
import traceback

from appdaemon.appdaemon import AppDaemon

class ThreadAsync:

    """
    Module to translate from the thread world to the async world via queues
    """

    def __init__(self, ad: AppDaemon):

        self.AD = ad
        self.stopping = False
        self.logger = ad.logging.get_child("_thread_async")
        #
        # Initial Setup
        #

        self.appq = asyncio.Queue(maxsize=0)

    def stop(self):
        self.logger.debug("stop() called for thread_async")
        self.stopping = True
        # Queue a fake event to make the loop wake up and exit
        self.appq.put_nowait({"stop": True})

    async def loop(self):
        while not self.stopping:
            args = None
            try:
                args = await self.appq.get()
                if "stop" not in args:
                    self.logger.debug("thread_async loop, args=%s", args)
                    function = args["function"]
                    myargs = args["args"]
                    mykwargs = args["kwargs"]
                    asyncio.ensure_future(function(*myargs, **mykwargs))
                    # self.logger.debug("calling task_done()")
                    # self.appq.task_done()
            except Exception:
                self.logger.warning("-" * 60)
                self.logger.warning("Unexpected error during thread_async() loop()")
                self.logger.warning("args: %s", args)
                self.logger.warning("-" * 60)
                self.logger.warning(traceback.format_exc())
                self.logger.warning("-" * 60)

    def call_async_no_wait(self, function, *args, **kwargs):
        self.appq.put_nowait({"function": function, "args": args, "kwargs": kwargs})
<|endoftext|>


<commit_before># -*- coding: utf-8 -*-
from __future__ import unicode_literals

import logging
import sys

from pokemongo_bot.event_manager import EventHandler

class LoggingHandler(EventHandler):
    EVENT_COLOR_MAP = {
        'api_error':                         'red',
        'badges':                            'blue',
        'bot_exit':                          'red',
        'bot_start':                         'green',
        'buddy_candy_earned':                'green',
        'buddy_candy_fail':                  'red',
        'buddy_keep_active':                 'red',
        'buddy_next_reward':                 'yellow',
        'buddy_not_available':               'red',
        'buddy_pokemon':                     'magenta',
        'buddy_update':                      'blue',
        'buddy_update_fail':                 'red',
        'buddy_reward':                      'green',
        'buddy_walked':                      'yellow',
        'catch_limit':                       'red',
        'catch_log':                         'magenta',
        'config_error':                      'red',
        'egg_already_incubating':            'yellow',
        'egg_hatched':                       'green',
        'egg_hatched_fail':                  'red',
        'eggs_hatched_log':                  'magenta',
        'evolve_log':                        'magenta',
        'future_pokemon_release':            'yellow',
        'incubate':                          'green',
        'incubator_already_used':            'yellow',
        'inventory_full':                    'yellow',
        'item_discard_fail':                 'red',
        'item_discarded':                    'green',
        'next_force_recycle':                'green',
        'force_recycle':                     'green',
        'keep_best_release':                 'green',
        'level_up':                          'green',
        'level_up_reward':                   'green',
        'location_cache_error':              'yellow',
        'location_cache_ignored':            'yellow',
        'login_failed':                      'red',
        'login_log':                         'magenta',
        'login_successful':                  'green',
        'log_stats':                         'magenta',
        'lucky_egg_error':                   'red',
        'move_to_map_pokemon_encounter':     'green',
        'move_to_map_pokemon_fail':          'red',
        'next_egg_incubates':                'yellow',
        'next_sleep':                        'green',
        'next_random_pause':                 'green',
        'next_random_alive_pause':           'green',
        'no_pokeballs':                      'red',
        'path_lap_end':                      'green',
        'pokemon_appeared':                  'yellow',
        'pokemon_capture_failed':            'red',
        'pokemon_caught':                    'blue',
        'pokemon_evolved':                   'green',
        'pokemon_evolve_check':              'green',
        'pokemon_fled':                      'red',
        'pokemon_inventory_full':            'red',
        'pokemon_nickname_invalid':          'red',
        'pokemon_not_in_range':              'yellow',
        'pokemon_keep':                      'green',
        'pokemon_release':                   'green',
        'pokemon_upgraded':                  'green',
        'pokemon_vanished':                  'red',
        'pokestop_empty':                    'yellow',
        'pokestop_log':                      'magenta',
        'pokestop_searching_too_often':      'yellow',
        'rename_pokemon':                    'green',
        'show_best_pokemon':                 'magenta',
        'show_inventory':                    'magenta',
        'skip_evolve':                       'yellow',
        'softban':                           'red',
        'softban_log':                       'magenta',
        'spin_limit':                        'red',
        'spun_pokestop':                     'cyan',
        'threw_berry_failed':                'red',
        'transfer_log':                      'magenta',
        'unknown_spin_result':               'red',
        'unset_pokemon_nickname':            'red',
        'vip_pokemon':                       'red',
        'use_incense':                       'blue',
        'vanish_limit_reached':              'red',
        'move_to_map_pokemon_teleport_to':   'yellow',
        'sniper_teleporting':                'yellow',
        'sniper_error':                      'red',

        'sniper_log':                        'none',
        'arrived_at_cluster':                'none',
        'arrived_at_fort':                   'none',
        'bot_sleep':                         'none',
        'bot_random_pause':                  'none',
        'bot_random_alive_pause':            'none',
        'catchable_pokemon':                 'none',
        'found_cluster':                     'none',
        'incubate_try':                      'none',
        'load_cached_location':              'none',
        'location_found':                    'none',
        'login_started':                     'none',
        'lured_pokemon_found':               'none',
        'move_to_map_pokemon_move_towards':  'none',
        'move_to_map_pokemon_teleport_back': 'none',
        'move_to_map_pokemon_updated_map':   'none',
        'moving_to_fort':                    'none',
        'moving_to_lured_fort':              'none',
        'pokemon_catch_rate':                'none',
        'pokemon_evolve_fail':               'none',
        'pokestop_on_cooldown':              'none',
        'pokestop_out_of_range':             'none',
        'polyline_request':                  'none',
        'position_update':                   'none',
        'path_lap_update':                   'none',
        'set_start_location':                'none',
        'softban_fix':                       'none',
        'softban_fix_done':                  'none',
        'spun_fort':                         'none',
        'threw_berry':                       'none',
        'threw_pokeball':                    'none',
        'used_lucky_egg':                    'none',
        'catch_limit_on':                    'yellow',
        'catch_limit_off':                   'green'
    }
    COLOR_CODE = {
        'gray':    '\033[90m',
        'red':     '\033[91m',
        'green':   '\033[92m',
        'yellow':  '\033[93m',
        'blue':    '\033[94m',
        'magenta': '\033[95m',
        'cyan':    '\033[96m',
        'white':   '\033[97m',
        'none':    '\033[0m'
    }

    def __init__(self, color=True, debug=False):
        self.color = color
        self.debug = debug

    def handle_event(self, event, sender, level, formatted_msg, data):
        if not formatted_msg:
            formatted_msg = str(data)

        if self.color and event in self.EVENT_COLOR_MAP:
            color = self.COLOR_CODE[self.EVENT_COLOR_MAP[event]]
            formatted_msg = '{}{}{}'.format(color, formatted_msg, self.COLOR_CODE['none'])

        if self.debug:
            formatted_msg = '[{}] {}'.format(event, formatted_msg)

        logger = logging.getLogger(type(sender).__name__)
        getattr(logger, level)(formatted_msg.encode(sys.stdout.encoding or sys.getdefaultencoding(), "replace").decode("utf-8", "replace"))
<commit_msg>define color in logging_handler<commit_after># -*- coding: utf-8 -*-
from __future__ import unicode_literals

import logging
import sys

from pokemongo_bot.event_manager import EventHandler

class LoggingHandler(EventHandler):
    EVENT_COLOR_MAP = {
        'api_error':                         'red',
        'badges':                            'blue',
        'bot_exit':                          'red',
        'bot_start':                         'green',
        'buddy_candy_earned':                'green',
        'buddy_candy_fail':                  'red',
        'buddy_keep_active':                 'red',
        'buddy_next_reward':                 'yellow',
        'buddy_not_available':               'red',
        'buddy_pokemon':                     'magenta',
        'buddy_update':                      'blue',
        'buddy_update_fail':                 'red',
        'buddy_reward':                      'green',
        'buddy_walked':                      'yellow',
        'catch_limit':                       'red',
        'catch_log':                         'magenta',
        'config_error':                      'red',
        'egg_already_incubating':            'yellow',
        'egg_hatched':                       'green',
        'egg_hatched_fail':                  'red',
        'eggs_hatched_log':                  'magenta',
        'evolve_log':                        'magenta',
        'future_pokemon_release':            'yellow',
        'incubate':                          'green',
        'incubator_already_used':            'yellow',
        'inventory_full':                    'yellow',
        'item_discard_fail':                 'red',
        'item_discarded':                    'green',
        'next_force_recycle':                'green',
        'force_recycle':                     'green',
        'keep_best_release':                 'green',
        'level_up':                          'green',
        'level_up_reward':                   'green',
        'location_cache_error':              'yellow',
        'location_cache_ignored':            'yellow',
        'login_failed':                      'red',
        'login_log':                         'magenta',
        'login_successful':                  'green',
        'log_stats':                         'magenta',
        'lucky_egg_error':                   'red',
        'move_to_map_pokemon_encounter':     'green',
        'move_to_map_pokemon_fail':          'red',
        'next_egg_incubates':                'yellow',
        'next_sleep':                        'green',
        'next_random_pause':                 'green',
        'next_random_alive_pause':           'green',
        'no_pokeballs':                      'red',
        'path_lap_end':                      'green',
        'pokemon_appeared':                  'yellow',
        'pokemon_capture_failed':            'red',
        'pokemon_caught':                    'blue',
        'pokemon_evolved':                   'green',
        'pokemon_evolve_check':              'green',
        'pokemon_fled':                      'red',
        'pokemon_inventory_full':            'red',
        'pokemon_nickname_invalid':          'red',
        'pokemon_not_in_range':              'yellow',
        'pokemon_keep':                      'green',
        'pokemon_release':                   'green',
        'pokemon_upgraded':                  'green',
        'pokemon_vanished':                  'red',
        'pokemon_vip_caught':                'blue',
        'pokestop_empty':                    'yellow',
        'pokestop_log':                      'magenta',
        'pokestop_searching_too_often':      'yellow',
        'rename_pokemon':                    'green',
        'show_best_pokemon':                 'magenta',
        'show_inventory':                    'magenta',
        'skip_evolve':                       'yellow',
        'softban':                           'red',
        'softban_log':                       'magenta',
        'spin_limit':                        'red',
        'spun_pokestop':                     'cyan',
        'threw_berry_failed':                'red',
        'transfer_log':                      'magenta',
        'unknown_spin_result':               'red',
        'unset_pokemon_nickname':            'red',
        'vip_pokemon':                       'red',
        'use_incense':                       'blue',
        'vanish_limit_reached':              'red',
        'move_to_map_pokemon_teleport_to':   'yellow',
        'sniper_teleporting':                'yellow',
        'sniper_error':                      'red',

        'sniper_log':                        'none',
        'arrived_at_cluster':                'none',
        'arrived_at_fort':                   'none',
        'bot_sleep':                         'none',
        'bot_random_pause':                  'none',
        'bot_random_alive_pause':            'none',
        'catchable_pokemon':                 'none',
        'found_cluster':                     'none',
        'incubate_try':                      'none',
        'load_cached_location':              'none',
        'location_found':                    'none',
        'login_started':                     'none',
        'lured_pokemon_found':               'none',
        'move_to_map_pokemon_move_towards':  'none',
        'move_to_map_pokemon_teleport_back': 'none',
        'move_to_map_pokemon_updated_map':   'none',
        'moving_to_fort':                    'none',
        'moving_to_lured_fort':              'none',
        'pokemon_catch_rate':                'none',
        'pokemon_evolve_fail':               'none',
        'pokestop_on_cooldown':              'none',
        'pokestop_out_of_range':             'none',
        'polyline_request':                  'none',
        'position_update':                   'none',
        'path_lap_update':                   'none',
        'set_start_location':                'none',
        'softban_fix':                       'none',
        'softban_fix_done':                  'none',
        'spun_fort':                         'none',
        'threw_berry':                       'none',
        'threw_pokeball':                    'none',
        'used_lucky_egg':                    'none',
        'catch_limit_on':                    'yellow',
        'catch_limit_off':                   'green'
    }
    COLOR_CODE = {
        'gray':    '\033[90m',
        'red':     '\033[91m',
        'green':   '\033[92m',
        'yellow':  '\033[93m',
        'blue':    '\033[94m',
        'magenta': '\033[95m',
        'cyan':    '\033[96m',
        'white':   '\033[97m',
        'none':    '\033[0m'
    }

    def __init__(self, color=True, debug=False):
        self.color = color
        self.debug = debug

    def handle_event(self, event, sender, level, formatted_msg, data):
        if not formatted_msg:
            formatted_msg = str(data)

        if self.color and event in self.EVENT_COLOR_MAP:
            color = self.COLOR_CODE[self.EVENT_COLOR_MAP[event]]
            formatted_msg = '{}{}{}'.format(color, formatted_msg, self.COLOR_CODE['none'])

        if self.debug:
            formatted_msg = '[{}] {}'.format(event, formatted_msg)

        logger = logging.getLogger(type(sender).__name__)
        getattr(logger, level)(formatted_msg.encode(sys.stdout.encoding or sys.getdefaultencoding(), "replace").decode("utf-8", "replace"))
<|endoftext|>