Source code for ops.hookcmds._types

# Copyright 2025 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

import dataclasses
import datetime
import pathlib
from collections.abc import Sequence
from typing import (
    TYPE_CHECKING,
    Any,
    Literal,
    TypeAlias,
    TypedDict,
)

from ._utils import datetime_from_iso

SecretRotate = Literal['never', 'hourly', 'daily', 'weekly', 'monthly', 'quarterly', 'yearly']
SettableStatusName = Literal['active', 'blocked', 'maintenance', 'waiting']
ReadOnlyStatusName = Literal['error', 'unknown']
StatusName: TypeAlias = SettableStatusName | ReadOnlyStatusName


if TYPE_CHECKING:
    from typing_extensions import NotRequired

    class AddressDict(TypedDict, total=False):
        hostname: str
        address: str  # Juju < 2.9
        value: str  # Juju >= 2.9
        cidr: str

    BindAddressDict = TypedDict(
        'BindAddressDict',
        {
            'mac-address': NotRequired[str],
            'interface-name': str,
            'addresses': list[AddressDict] | None,
        },
    )


[docs] @dataclasses.dataclass(frozen=True, kw_only=True) class Address: """A Juju space address, found in :class:`BindAddress` objects.""" hostname: str # These may be IP addresses or hostnames, so we keep things simple and use # str, and leave it to users to convert them to ipaddress types if needed. # See #818 for more information. value: str cidr: str @classmethod def _from_dict(cls, d: AddressDict) -> Address: return cls( hostname=d.get('hostname', ''), value=d.get('value', d.get('address', '')), cidr=d.get('cidr', ''), )
[docs] @dataclasses.dataclass(frozen=True, kw_only=True) class BindAddress: """A Juju space bind address, found in :class:`Network` objects.""" mac_address: str interface_name: str addresses: list[Address] = dataclasses.field(default_factory=list[Address]) @classmethod def _from_dict(cls, d: BindAddressDict) -> BindAddress: addresses = [Address._from_dict(addr) for addr in d.get('addresses') or []] return cls( mac_address=d.get('mac-address', ''), interface_name=d.get('interface-name', ''), addresses=addresses, )
[docs] @dataclasses.dataclass(frozen=True, kw_only=True) class CloudCredential: """Credentials for cloud. Used as the type of attribute `credential` in :class:`CloudSpec`. """ auth_type: str """Authentication type.""" attributes: dict[str, str] = dataclasses.field(default_factory=dict[str, str]) """A dictionary containing cloud credentials. For example, for AWS, it contains `access-key` and `secret-key`; for Azure, `application-id`, `application-password` and `subscription-id` can be found here. """ redacted: list[str] = dataclasses.field(default_factory=list[str]) """A list of redacted secrets.""" @classmethod def _from_dict(cls, d: dict[str, Any]) -> CloudCredential: """Create a new CloudCredential object from a dictionary.""" return cls( auth_type=d['auth-type'], attributes=d.get('attrs') or {}, redacted=d.get('redacted') or [], )
[docs] @dataclasses.dataclass(frozen=True, kw_only=True) class CloudSpec: """Cloud specification information (metadata) including credentials.""" type: str """Type of the cloud.""" name: str """Juju cloud name.""" region: str | None = None """Region of the cloud.""" endpoint: str | None = None """Endpoint of the cloud.""" identity_endpoint: str | None = None """Identity endpoint of the cloud.""" storage_endpoint: str | None = None """Storage endpoint of the cloud.""" credential: CloudCredential | None = None """Cloud credentials with key-value attributes.""" ca_certificates: list[str] = dataclasses.field(default_factory=list[str]) """A list of CA certificates.""" skip_tls_verify: bool = False """Whether to skip TLS verification.""" is_controller_cloud: bool = False """If this is the cloud used by the controller, defaults to False.""" @classmethod def _from_dict(cls, d: dict[str, Any]) -> CloudSpec: """Create a new CloudSpec object from a dict parsed from JSON.""" return cls( type=d['type'], name=d['name'], region=d.get('region') or None, endpoint=d.get('endpoint') or None, identity_endpoint=d.get('identity-endpoint') or None, storage_endpoint=d.get('storage-endpoint') or None, credential=CloudCredential._from_dict(d['credential']) if d.get('credential') else None, ca_certificates=d.get('cacertificates') or [], skip_tls_verify=d.get('skip-tls-verify') or False, is_controller_cloud=d.get('is-controller-cloud') or False, )
class GoalDict(TypedDict): status: str since: str class GoalStateDict(TypedDict): units: dict[str, GoalDict] relations: dict[str, dict[str, GoalDict]]
[docs] @dataclasses.dataclass(frozen=True, kw_only=True) class Goal: """A goal status and when it was last updated, found in :class:`GoalState` objects.""" status: str since: datetime.datetime @classmethod def _from_dict(cls, d: GoalDict) -> Goal: return cls( status=d['status'], since=datetime_from_iso(d['since']), )
[docs] @dataclasses.dataclass(frozen=True, kw_only=True) class GoalState: """The units and relations that the model should have, and the status of achieving that.""" units: dict[str, Goal] # The top key is the endpoint/relation name, the second key is the app/unit name. relations: dict[str, dict[str, Goal]] @classmethod def _from_dict(cls, d: GoalStateDict) -> GoalState: units: dict[str, Goal] = { name: Goal._from_dict(unit) for name, unit in d.get('units', {}).items() } relations: dict[str, dict[str, Goal]] = {} for name, relation in d.get('relations', {}).items(): goals: dict[str, Goal] = { app_or_unit: Goal._from_dict(data) for app_or_unit, data in relation.items() } relations[name] = goals return cls(units=units, relations=relations)
[docs] @dataclasses.dataclass(frozen=True, kw_only=True) class Network: """A Juju space.""" bind_addresses: Sequence[BindAddress] # These may be IP addresses or hostnames, so we keep things simple and use # str, and leave it to users to convert them to ipaddress types if needed. # See #818 for more information. egress_subnets: Sequence[str] ingress_addresses: Sequence[str] @classmethod def _from_dict(cls, d: dict[str, Any]) -> Network: bind_dicts: list[BindAddressDict] = d.get('bind-addresses', []) bind = [BindAddress._from_dict(bind_dict) for bind_dict in bind_dicts] egress = d.get('egress-subnets', []) ingress = d.get('ingress-addresses', []) return cls(bind_addresses=bind, egress_subnets=egress, ingress_addresses=ingress)
# Note that we intend to merge this with model.py's `Port` in the future, and # that does not have `kw_only=True`. That means that we should not use it here, # either, so that merging can be backwards compatible.
[docs] @dataclasses.dataclass(frozen=True) class Port: """A port that Juju has opened for the charm.""" protocol: Literal['tcp', 'udp', 'icmp'] | None = 'tcp' """The IP protocol.""" port: int | None = None """The port number. Will be ``None`` if protocol is ``'icmp'``.""" to_port: int | None = None """The final port number if this is a range of ports.""" endpoints: list[str] | None = None """The endpoints this port applies to, ``['*']`` if all endpoints, or ``None`` if unknown."""
class RelationModelDict(TypedDict): uuid: str
[docs] @dataclasses.dataclass(frozen=True, kw_only=True) class RelationModel: """Details of the model on the remote side of the relation.""" uuid: str @classmethod def _from_dict(cls, d: RelationModelDict) -> RelationModel: return cls( uuid=d['uuid'], )
[docs] @dataclasses.dataclass(frozen=True, kw_only=True) class SecretInfo: """Metadata for Juju secrets.""" revision: int id: str = '' label: str = '' description: str = '' expiry: datetime.datetime | None = None rotation: SecretRotate | None = None rotates: datetime.datetime | None = None @classmethod def _from_dict(cls, d: dict[str, Any]) -> SecretInfo: id, data = next(iter(d.items())) # Juju returns dict of {secret_id: {info}} return cls( id=id, label=data.get('label'), description=data.get('description'), expiry=datetime_from_iso(data['expiry']) if data.get('expiry') else None, rotation=data.get('rotation'), rotates=datetime_from_iso(data['rotates']) if data.get('rotates') else None, revision=data['revision'], )
[docs] @dataclasses.dataclass(frozen=True, kw_only=True) class Storage: """Metadata for Juju storage.""" kind: str location: pathlib.Path @classmethod def _from_dict(cls, d: dict[str, Any]) -> Storage: return cls( kind=d['kind'], location=pathlib.Path(d['location']), )
StatusDict = TypedDict( 'StatusDict', {'message': str, 'status': str, 'status-data': dict[str, Any]} ) AppStatusDict = TypedDict( 'AppStatusDict', { 'application-status': StatusDict, 'units': dict[str, StatusDict], }, )
[docs] @dataclasses.dataclass(frozen=True, kw_only=True) class UnitStatus: """The status of a Juju unit.""" status: str = '' message: str = '' status_data: dict[str, Any] @classmethod def _from_dict(cls, d: StatusDict) -> UnitStatus: return cls( status=d['status'], message=d['message'], status_data=d['status-data'], )
[docs] @dataclasses.dataclass(frozen=True, kw_only=True) class AppStatus: """The status of a Juju application.""" status: str = '' message: str = '' status_data: dict[str, Any] units: dict[str, UnitStatus] @classmethod def _from_dict(cls, d: AppStatusDict) -> AppStatus: units = {name: UnitStatus._from_dict(u) for name, u in d.get('units', {}).items()} app = d['application-status'] return cls( status=app['status'], message=app['message'], status_data=app['status-data'], units=units, )