class InvalidChoiceError(MarathonError):

    def __init__(self, param, value, options):
        super().__init__(
            'Invalid choice "{value}" for param "{param}". Must be one of {options}'.format(
                param=param, value=value, options=options
            )
        ) class MarathonResource(MarathonObject):

    """Base Marathon resource."""

    def __repr__(self):
        if 'id' in list(vars(self).keys()):
            return f"{self.__class__.__name__}::{self.id}"
        else:
            return "{clazz}::{obj}".format(clazz=self.__class__.__name__, obj=self.to_json())

    def __eq__(self, other):
        try:
            return self.__dict__ == other.__dict__
        except Exception:
            return False

    def __hash__(self):
        # Technically this class shouldn't be hashable because it often
        # contains mutable fields, but in practice this class is used more
        # like a record or namedtuple.
        return hash(self.to_json())

    def __str__(self):
        return f"{self.__class__.__name__}::" + str(self.__dict__) class MarathonObject:
    """Base Marathon object."""

    def __repr__(self):
        return "{clazz}::{obj}".format(clazz=self.__class__.__name__, obj=self.to_json(minimal=False))

    def __eq__(self, other):
        try:
            return self.__dict__ == other.__dict__
        except Exception:
            return False

    def __hash__(self):
        # Technically this class shouldn't be hashable because it often
        # contains mutable fields, but in practice this class is used more
        # like a record or namedtuple.
        return hash(self.to_json())

    def json_repr(self, minimal=False):
        """Construct a JSON-friendly representation of the object.

        :param bool minimal: Construct a minimal representation of the object (ignore nulls and empty collections)

        :rtype: dict
        """
        if minimal:
            return {to_camel_case(k): v for k, v in vars(self).items() if (v or v is False or v == 0)}
        else:
            return {to_camel_case(k): v for k, v in vars(self).items()}

    @classmethod
    def from_json(cls, attributes):
        """Construct an object from a parsed response.

        :param dict attributes: object attributes from parsed response
        """
        return cls(**{to_snake_case(k): v for k, v in attributes.items()})

    def to_json(self, minimal=True):
        """Encode an object as a JSON string.

        :param bool minimal: Construct a minimal representation of the object (ignore nulls and empty collections)

        :rtype: str
        """
        if minimal:
            return json.dumps(self.json_repr(minimal=True), cls=MarathonMinimalJsonEncoder, sort_keys=True)
        else:
            return json.dumps(self.json_repr(), cls=MarathonJsonEncoder, sort_keys=True) def assert_valid_path(path):
    """Checks if a path is a correct format that Marathon expects. Raises ValueError if not valid.

    :param str path: The app id.

    :rtype: str
    """
    if path is None:
        return
    # As seen in:
    # https://github.com/mesosphere/marathon/blob/0c11661ca2f259f8a903d114ef79023649a6f04b/src/main/scala/mesosphere/marathon/state/PathId.scala#L71
    for id in filter(None, path.strip('/').split('/')):
        if not ID_PATTERN.match(id):
            raise ValueError(
                'invalid path (allowed: lowercase letters, digits, hyphen, "/", ".", ".."): %r' % path)
    return path class MarathonConstraint(MarathonObject):

    """Marathon placement constraint.

    See https://mesosphere.github.io/marathon/docs/constraints.html

    :param str field: constraint operator target
    :param str operator: must be one of [UNIQUE, CLUSTER, GROUP_BY, LIKE, UNLIKE]
    :param value: [optional] if `operator` is CLUSTER, constrain tasks to servers where `field` == `value`.
    If `operator` is GROUP_BY, place at most `value` tasks per group. If `operator`
    is `LIKE` or `UNLIKE`, filter servers using regexp.
    :type value: str, int, or None
    """

    """Valid operators"""

    def __init__(self, field, operator, value=None):
        self.field = field
        self.operator = operator
        self.value = value

    def __repr__(self):
        if self.value:
            template = "MarathonConstraint::{field}:{operator}:{value}"
        else:
            template = "MarathonConstraint::{field}:{operator}"
        return template.format(**self.__dict__)

    def json_repr(self, minimal=False):
        """Construct a JSON-friendly representation of the object.

        :param bool minimal: [ignored]

        :rtype: list
        """
        if self.value:
            return [self.field, self.operator, self.value]
        else:
            return [self.field, self.operator]

    @classmethod
    def from_json(cls, obj):
        """Construct a MarathonConstraint from a parsed response.

        :param dict attributes: object attributes from parsed response

        :rtype: :class:`MarathonConstraint`
        """
        if len(obj) == 2:
            (field, operator) = obj
            return cls(field, operator)
        if len(obj) > 2:
            (field, operator, value) = obj
            return cls(field, operator, value)

    @classmethod
    def from_string(cls, constraint):
        """
        :param str constraint: The string representation of a constraint

        :rtype: :class:`MarathonConstraint`
        """
        obj = constraint.split(':')
        marathon_constraint = cls.from_json(obj)

        if marathon_constraint:
            return marathon_constraint

        raise ValueError("Invalid string format. "
                         "Expected `field:operator:value`") class MarathonContainer(MarathonObject):

    """Marathon health check.

    See https://mesosphere.github.io/marathon/docs/native-docker.html

    :param docker: docker field (e.g., {"image": "mygroup/myimage"})'
    :type docker: :class:`marathon.models.container.MarathonDockerContainer` or dict
    :param str type:
    :param port_mappings: New in Marathon v1.5. container.docker.port_mappings moved here.
    :type port_mappings: list[:class:`marathon.models.container.MarathonContainerPortMapping`] or list[dict]
    :param volumes:
    :type volumes: list[:class:`marathon.models.container.MarathonContainerVolume`] or list[dict]
    """

    TYPES = ['DOCKER', 'MESOS']
    """Valid container types"""

    def __init__(self, docker=None, type='DOCKER', port_mappings=None, volumes=None):
        if type not in self.TYPES:
            raise InvalidChoiceError('type', type, self.TYPES)
        self.type = type

        # Marathon v1.5 moved portMappings from within container.docker object directly
        # under the container object
        if port_mappings:
            self.port_mappings = [
                pm if isinstance(
                    pm, MarathonContainerPortMapping) else MarathonContainerPortMapping().from_json(pm)
                for pm in (port_mappings or [])
            ]

        if docker:
            self.docker = docker if isinstance(docker, MarathonDockerContainer) \
                else MarathonDockerContainer().from_json(docker)

        self.volumes = [
            v if isinstance(
                v, MarathonContainerVolume) else MarathonContainerVolume().from_json(v)
            for v in (volumes or [])
        ] class MarathonDeployment(MarathonResource):

    """Marathon Application resource.

    See: https://mesosphere.github.io/marathon/docs/rest-api.html#deployments
         https://mesosphere.github.io/marathon/docs/generated/api.html#v2_deployments_get

    :param list[str] affected_apps: list of affected app ids
    :param current_actions: current actions
    :type current_actions: list[:class:`marathon.models.deployment.MarathonDeploymentAction`] or list[dict]
    :param int current_step: current step
    :param str id: deployment id
    :param steps: deployment steps
    :type steps: list[:class:`marathon.models.deployment.MarathonDeploymentAction`] or list[dict]
    :param int total_steps: total number of steps
    :param str version: version id
    :param str affected_pods: list of strings
    """

    def __init__(self, affected_apps=None, current_actions=None, current_step=None, id=None, steps=None,
                 total_steps=None, version=None, affected_pods=None):
        self.affected_apps = affected_apps
        self.current_actions = [
            a if isinstance(
                a, MarathonDeploymentAction) else MarathonDeploymentAction.from_json(a)
            for a in (current_actions or [])
        ]
        self.current_step = current_step
        self.id = id
        self.steps = [self.parse_deployment_step(step) for step in (steps or [])]
        self.total_steps = total_steps
        self.version = version
        self.affected_pods = affected_pods

    def parse_deployment_step(self, step):
        if step.__class__ == dict:
            # This is what Marathon 1.0.0 returns: steps
            return MarathonDeploymentStep().from_json(step)
        elif step.__class__ == list:
            # This is Marathon < 1.0.0 style, a list of actions
            return [s if isinstance(s, MarathonDeploymentAction) else MarathonDeploymentAction.from_json(s) for s in step]
        else:
            return step class MarathonTask(MarathonResource):

    """Marathon Task resource.

    :param str app_id: application id
    :param health_check_results: health check results
    :type health_check_results: list[:class:`marathon.models.MarathonHealthCheckResult`] or list[dict]
    :param str host: mesos slave running the task
    :param str id: task id
    :param list[int] ports: allocated ports
    :param list[int] service_ports: ports exposed for load balancing
    :param str state: State of the task e.g. TASK_RUNNING
    :param str slave_id: Mesos slave id
    :param staged_at: when this task was staged
    :type staged_at: datetime or str
    :param started_at: when this task was started
    :type started_at: datetime or str
    :param str version: app version with which this task was started
    :type region: str
    :param region: fault domain region support in DCOS EE
    :type zone: str
    :param zone: fault domain zone support in DCOS EE
    :type role: str
    :param role: mesos role
    """

    DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'

    def __init__(self, app_id=None, health_check_results=None, host=None, id=None, ports=None, service_ports=None,
                 slave_id=None, staged_at=None, started_at=None, version=None, ip_addresses=[], state=None, local_volumes=None,
                 region=None, zone=None, role=None):
        self.app_id = app_id
        self.health_check_results = health_check_results or []
        self.health_check_results = [
            hcr if isinstance(
                hcr, MarathonHealthCheckResult) else MarathonHealthCheckResult().from_json(hcr)
            for hcr in (health_check_results or []) if any(health_check_results)
        ]
        self.host = host
        self.id = id
        self.ports = ports or []
        self.service_ports = service_ports or []
        self.slave_id = slave_id
        self.staged_at = to_datetime(staged_at)
        self.started_at = to_datetime(started_at)
        self.state = state
        self.version = version
        self.ip_addresses = [
            ipaddr if isinstance(
                ip_addresses, MarathonIpAddress) else MarathonIpAddress().from_json(ipaddr)
            for ipaddr in (ip_addresses or [])]
        self.local_volumes = local_volumes or []
        self.region = region
        self.zone = zone
        self.role = role def get_log():
    return logging.getLogger(__name__.split('.')[0]) def to_datetime(timestamp):
    if (timestamp is None or isinstance(timestamp, datetime.datetime)):
        return timestamp
    else:
        for fmt in DATETIME_FORMATS:
            try:
                return datetime.datetime.strptime(timestamp, fmt).replace(tzinfo=datetime.timezone.utc)
            except ValueError:
                pass
        raise ValueError(f'Unrecognized datetime format: {timestamp}')
from ..exceptions import InvalidChoiceError
from .base import MarathonResource, MarathonObject, assert_valid_path
from .constraint import MarathonConstraint
from .container import MarathonContainer
from .deployment import MarathonDeployment
from .task import MarathonTask
from ..util import get_log
from ..util import to_datetime
        'env', 'executor', 'gpus', 'health_checks', 'instances', 'kill_selection', 'labels', 'max_launch_delay_seconds',
        'mem', 'ports', 'require_ports', 'store_urls', 'task_rate_limit', 'upgrade_strategy', 'unreachable_strategy',
        'uris', 'user', 'version', 'role'
    ]
    """List of attributes which may be updated/changed after app creation"""

    CREATE_ONLY_ATTRIBUTES = ['id', 'accepted_resource_roles']
    """List of attributes that should only be passed on creation"""

    READ_ONLY_ATTRIBUTES = [
        'deployments', 'tasks', 'tasks_running', 'tasks_staged', 'tasks_healthy', 'tasks_unhealthy']
    """List of read-only attributes"""

    KILL_SELECTIONS = ["YOUNGEST_FIRST", "OLDEST_FIRST"]

    def __init__(self, accepted_resource_roles=None, args=None, backoff_factor=None, backoff_seconds=None, cmd=None,
                 constraints=None, container=None, cpus=None, dependencies=None, deployments=None, disk=None, env=None,
                 executor=None, health_checks=None, id=None, role=None, instances=None, kill_selection=None, labels=None,
                 last_task_failure=None, max_launch_delay_seconds=None, mem=None, ports=None, require_ports=None,
                 store_urls=None, task_rate_limit=None, tasks=None, tasks_running=None, tasks_staged=None,
                 tasks_healthy=None, task_kill_grace_period_seconds=None, tasks_unhealthy=None, upgrade_strategy=None,
                 unreachable_strategy=None, uris=None, user=None, version=None, version_info=None,
                 ip_address=None, fetch=None, task_stats=None, readiness_checks=None,
                 readiness_check_results=None, secrets=None, port_definitions=None, residency=None, gpus=None, networks=None):

        # self.args = args or []
        self.accepted_resource_roles = accepted_resource_roles
        self.args = args
        # Marathon 0.7.0-RC1 throws a validation error if this is [] and cmd is passed:
        # "error": "AppDefinition must either contain a 'cmd' or a 'container'."

        self.backoff_factor = backoff_factor
        self.backoff_seconds = backoff_seconds
        self.cmd = cmd
        self.constraints = [
            c if isinstance(c, MarathonConstraint) else MarathonConstraint(*c)
            for c in (constraints or [])
        ]
        self.container = container if (isinstance(container, MarathonContainer) or container is None) \
            else MarathonContainer.from_json(container)
        self.cpus = cpus
        self.dependencies = dependencies or []
        self.deployments = [
            d if isinstance(
                d, MarathonDeployment) else MarathonDeployment().from_json(d)
            for d in (deployments or [])
        ]
        self.disk = disk
        self.env = env or dict()
        self.executor = executor
        self.gpus = gpus
        self.health_checks = health_checks or []
        self.health_checks = [
            hc if isinstance(
                hc, MarathonHealthCheck) else MarathonHealthCheck().from_json(hc)
            for hc in (health_checks or [])
        ]
        self.id = assert_valid_path(id)
        self.role = role
        self.instances = instances
        if kill_selection and kill_selection not in self.KILL_SELECTIONS:
            raise InvalidChoiceError(
                'kill_selection', kill_selection, self.KILL_SELECTIONS)
        self.kill_selection = kill_selection
        self.labels = labels or {}
        self.last_task_failure = last_task_failure if (isinstance(last_task_failure, MarathonTaskFailure) or last_task_failure is None) \
            else MarathonTaskFailure.from_json(last_task_failure)
        self.max_launch_delay_seconds = max_launch_delay_seconds
        self.mem = mem
        self.ports = ports or []
        self.port_definitions = [
            pd if isinstance(
                pd, PortDefinition) else PortDefinition.from_json(pd)
            for pd in (port_definitions or [])
        ]
        self.readiness_checks = [
            rc if isinstance(
                rc, ReadinessCheck) else ReadinessCheck().from_json(rc)
            for rc in (readiness_checks or [])
        ]
        self.readiness_check_results = readiness_check_results or []
        self.residency = residency
        self.require_ports = require_ports

        self.secrets = secrets or {}
        for k, s in self.secrets.items():
            if not isinstance(s, Secret):
                self.secrets[k] = Secret().from_json(s)

        self.store_urls = store_urls or []
        self.task_rate_limit = task_rate_limit
        self.tasks = [
            t if isinstance(t, MarathonTask) else MarathonTask().from_json(t)
            for t in (tasks or [])
        ]
        self.tasks_running = tasks_running
        self.tasks_staged = tasks_staged
        self.tasks_healthy = tasks_healthy
        self.task_kill_grace_period_seconds = task_kill_grace_period_seconds
        self.tasks_unhealthy = tasks_unhealthy
        self.upgrade_strategy = upgrade_strategy if (isinstance(upgrade_strategy, MarathonUpgradeStrategy) or upgrade_strategy is None) \
            else MarathonUpgradeStrategy.from_json(upgrade_strategy)
        self.unreachable_strategy = unreachable_strategy \
            if (isinstance(unreachable_strategy, MarathonUnreachableStrategy)
                or unreachable_strategy is None) \
            else MarathonUnreachableStrategy.from_json(unreachable_strategy)
        self.uris = uris or []
        self.fetch = fetch or []
        self.user = user
        self.version = version
        self.version_info = version_info if (isinstance(version_info, MarathonAppVersionInfo) or version_info is None) \
            else MarathonAppVersionInfo.from_json(version_info)
        self.task_stats = task_stats if (isinstance(task_stats, MarathonTaskStats) or task_stats is None) \
            else MarathonTaskStats.from_json(task_stats)
        self.networks = networks

    def add_env(self, key, value):
        self.env[key] = value



class MarathonHealthCheck(MarathonObject):