Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions interfaces/temporal-host-info/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# 1.0.0 - 25 November 2025

Initial release.
11 changes: 11 additions & 0 deletions interfaces/temporal-host-info/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# charmlibs.interfaces.temporal_host_info

The `temporal-host-info` interface library.

To install, add `charmlibs-interfaces-temporal-host-info` to your Python dependencies. Then in your Python code, import as:

```py
from charmlibs.interfaces import temporal_host_info
```

See the [reference documentation](https://documentation.ubuntu.com/charmlibs/reference/charmlibs/interfaces/temporal_host_info) for more.
73 changes: 73 additions & 0 deletions interfaces/temporal-host-info/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
[project]
name = "charmlibs-interfaces-temporal-host-info"
description = "The charmlibs.interfaces.temporal_host_info package."
readme = "README.md"
requires-python = ">=3.10"
authors = [
{name="The Charm Engineering team at Canonical"},
]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: Apache Software License",
"Intended Audience :: Developers",
"Operating System :: POSIX :: Linux",
"Development Status :: 5 - Production/Stable",
]
dynamic = ["version"]
dependencies = [
"jubilant>=1.5.0",
# "ops",
"ops==2.21.1",
]

[dependency-groups]
lint = [ # installed for `just lint interfaces/temporal-host-info` (unit, functional, and integration are also installed)
# "typing_extensions",
]
unit = [ # installed for `just unit interfaces/temporal-host-info`
"ops[testing]",
]
functional = [ # installed for `just functional interfaces/temporal-host-info`
]
integration = [ # installed for `just integration interfaces/temporal-host-info`
"jubilant",
]

[project.urls]
"Repository" = "https://github.com/canonical/charmlibs"
"Issues" = "https://github.com/canonical/charmlibs/issues"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/charmlibs"]

[tool.hatch.version]
path = "src/charmlibs/interfaces/temporal_host_info/_version.py"

[tool.ruff]
extend = "../../pyproject.toml"
src = ["src", "tests/unit", "tests/functional", "tests/integration"] # correctly sort local imports in tests

[tool.ruff.lint.extend-per-file-ignores]
# add additional per-file-ignores here to avoid overriding repo-level config
"tests/**/*" = [
# "E501", # line too long
]

[tool.pyright]
extends = "../../pyproject.toml"
include = ["src", "tests"]
pythonVersion = "3.10" # check no python > 3.10 features are used

[tool.charmlibs.functional]
ubuntu = [] # ubuntu versions to run functional tests with, e.g. "24.04" (defaults to just "latest")
pebble = [] # pebble versions to run functional tests with, e.g. "v1.0.0", "master" (defaults to no pebble versions)
sudo = false # whether to run functional tests with sudo (defaults to false)

[tool.charmlibs.integration]
# tags to run integration tests with (defaults to running once with no tag, i.e. tags = [''])
# Available in CI in tests/integration/pack.sh and integration tests as CHARMLIBS_TAG
tags = [] # Not used by the pack.sh and integration tests generated by the template
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# 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.

"""Relation management for temporal-host-info interface."""

from ._temporal_host_info import (
TemporalHostInfoProvider,
TemporalHostInfoRequirer,
)
from ._version import __version__ as __version__

__all__ = [
'TemporalHostInfoProvider',
'TemporalHostInfoRequirer',
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# 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.

"""Relation management for temporal-host-info interface."""

import logging

from ops import (
ConfigChangedEvent,
Handle,
LeaderElectedEvent,
RelationChangedEvent,
RelationJoinedEvent,
framework,
)
from ops.charm import CharmBase
from ops.framework import EventBase, EventSource, ObjectEvents
from ops.model import ActiveStatus, Relation, WaitingStatus

logger = logging.getLogger(__name__)

RELATION_NAME = 'temporal-host-info'


class TemporalHostInfoProvider(framework.Object):
"""A class for managing the temporal-host-info interface provider."""

def __init__(self, charm: CharmBase, port: int):
"""Create a new instance of the TemporalHostInfoProvider class.

:param: charm: The charm that is using this interface.
:type charm: CharmBase
:param: port: The port number to provide to requirers. This is typically
the 'frontend' service port.
:type port: int
"""
super().__init__(charm, 'host_info_provider')
self.charm = charm
self.port = port
charm.framework.observe(
charm.on[RELATION_NAME].relation_joined, self._on_host_info_relation_changed
)
charm.framework.observe(
charm.on[RELATION_NAME].relation_changed, self._on_host_info_relation_changed
)
charm.framework.observe(charm.on.leader_elected, self._on_config_changed)
charm.framework.observe(charm.on.config_changed, self._on_config_changed)

def _on_host_info_relation_changed(self, event: RelationChangedEvent | RelationJoinedEvent):
"""Update relation data.

:param: event: The relation event that triggered this handler.
:type event: RelationChangedEvent | RelationJoinedEvent
"""
logger.info('Handling temporal-host-info relation event')
if self.charm.unit.is_leader() and 'frontend' in str(self.charm.config['services']):
host = str(self.charm.config['external-hostname'])
if binding := self.charm.model.get_binding(event.relation):
host = host or str(binding.network.bind_address)
event.relation.data[self.charm.app]['host'] = host
event.relation.data[self.charm.app]['port'] = str(self.port)

def _on_config_changed(self, event: ConfigChangedEvent | LeaderElectedEvent):
"""Update relation data on config change."""
logger.info('Config changed, updating temporal-host-info relation data')
if self.charm.unit.is_leader() and 'frontend' in str(self.charm.config['services']):
host = str(self.charm.config['external-hostname'])
for relation in self.charm.model.relations.get('temporal-host-info', []):
if binding := self.charm.model.get_binding(relation):
host = host or str(binding.network.bind_address)
relation.data[self.charm.app]['host'] = host
relation.data[self.charm.app]['port'] = str(self.port)


class TemporalHostInfoRelationReadyEvent(EventBase):
"""Event emitted when temporal-host-info relation is ready."""

def __init__(
self,
handle: Handle,
host: str,
port: int,
):
super().__init__(handle)
self.host = host
self.port = port

def snapshot(self) -> dict[str, str | int]:
"""Return a snapshot of the event."""
data = super().snapshot()
data.update({'host': self.host, 'port': self.port})
return data

def restore(self, snapshot: dict[str, str | int]) -> None:
"""Restore the event from a snapshot."""
super().restore(snapshot)
self.host = snapshot['host']
self.port = snapshot['port']


class TemporalHostInfoRequirerCharmEvents(ObjectEvents):
"""List of events that the requirer charm can leverage."""

temporal_host_info_available = EventSource(TemporalHostInfoRelationReadyEvent)


class TemporalHostInfoRequirer(framework.Object):
"""A class for managing the temporal-host-info interface requirer.

Track this relation in your charm with:

.. code-block:: python

self.host_info = TemporalHostInfoRequirer(self)
# update container with new host info
framework.observe(self.host_info.on.temporal_host_info_available, self._update)

def _update(self, event):
host = self.host_info.host
port = self.host_info.port
"""

on = TemporalHostInfoRequirerCharmEvents() # type: ignore[reportAssignmentType]

def __init__(self, charm: CharmBase):
"""Create a new instance of the TemporalHostInfoProvider class.

:param: charm: The charm that is using this interface.
:type charm: CharmBase
"""
super().__init__(charm, 'host_info_requirer')
self.charm = charm
charm.framework.observe(
charm.on[RELATION_NAME].relation_joined, self._on_host_info_relation_changed
)
charm.framework.observe(
charm.on[RELATION_NAME].relation_changed, self._on_host_info_relation_changed
)

@property
def relations(self) -> list[Relation]:
"""Return the relations for this interface."""
return self.charm.model.relations.get(RELATION_NAME, [])

@property
def host(self) -> str | None:
"""Return the host from the relation data."""
for relation in self.relations:
if relation and relation.app:
return relation.data[relation.app].get('host', None)
return None

@property
def port(self) -> int | None:
"""Return the port from the relation data."""
for relation in self.relations:
if relation and relation.app:
port_str = relation.data[relation.app].get('port', None)
if port_str is not None:
return int(port_str)
return None

def _on_host_info_relation_changed(self, event: RelationChangedEvent):
"""Handle the relation joined/changed events.

:param: event: The relation event that triggered this handler.
:type event: RelationChangedEvent | RelationJoinedEvent
"""
try:
host = event.relation.data[event.relation.app]['host']
port = int(event.relation.data[event.relation.app]['port'])
except KeyError:
self.charm.unit.status = WaitingStatus('Waiting for temporal-host-info provider')
event.defer()
return
self.charm.unit.status = ActiveStatus()
self.on.temporal_host_info_available.emit(host=host, port=port)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# 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.

__version__ = '1.0.0'
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# common actions.yaml file symlinked by these charms
# consider adding an action for each thing you want to test

lib-version:
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# 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.

"""Common charm code for integration test charms.

This file is symlinked alongside src/charm.py by these charms.
"""

import logging

import ops

from charmlibs.interfaces import temporal_host_info

logger = logging.getLogger(__name__)


class Charm(ops.CharmBase):
"""Charm the application."""

def __init__(self, framework: ops.Framework):
super().__init__(framework)
framework.observe(self.on['lib-version'].action, self._on_lib_version)

def _on_lib_version(self, event: ops.ActionEvent):
logger.info('action [lib-version] called with params: %s', event.params)
results = {'version': temporal_host_info.__version__}
event.set_results(results)
logger.info('action [lib-version] set_results: %s', results)
Loading
Loading