Skip to content

Commit

Permalink
feat: add cli and platform utils (#599)
Browse files Browse the repository at this point in the history
Signed-off-by: Dariusz Duda <[email protected]>
Co-authored-by: Alex Lowe <[email protected]>
  • Loading branch information
dariuszd21 and lengau authored Dec 20, 2024
1 parent 44b1a94 commit 8e9893d
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 2 deletions.
2 changes: 1 addition & 1 deletion craft_application/services/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class ProviderService(base.ProjectService):
:param install_snap: Whether to install this app's snap from the host (default True)
"""

managed_mode_env_var = "CRAFT_MANAGED_MODE"
managed_mode_env_var = platforms.ENVIRONMENT_CRAFT_MANAGED_MODE

def __init__(
self,
Expand Down
6 changes: 6 additions & 0 deletions craft_application/util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
convert_architecture_deb_to_platform,
get_host_base,
is_valid_architecture,
get_hostname,
is_managed_mode,
)
from craft_application.util.retry import retry
from craft_application.util.snap_config import (
Expand All @@ -34,6 +36,7 @@
from craft_application.util.string import humanize_list, strtobool
from craft_application.util.system import get_parallel_build_count
from craft_application.util.yaml import dump_yaml, safe_yaml_load
from craft_application.util.cli import format_timestamp

__all__ = [
"get_unique_callbacks",
Expand All @@ -54,4 +57,7 @@
"safe_yaml_load",
"retry",
"get_parallel_build_count",
"get_hostname",
"is_managed_mode",
"format_timestamp",
]
43 changes: 43 additions & 0 deletions craft_application/util/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# This file is part of craft-application.
#
# Copyright 2024 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License version 3, as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""CLI-related utilities."""

import datetime
import warnings


def format_timestamp(dt: datetime.datetime) -> str:
"""Convert a datetime object (with or without timezone) to a string.
The format is an ISO 8601-compliant UTC date and time stamp formatted as:
<DATE>T<TIME>Z
Timezone-aware datetime objects are converted to UTC. Timezone-naive ones
are assumed to be UTC.
"""
if dt.tzinfo is not None and dt.tzinfo.utcoffset(None) is not None:
# timezone aware
dtz = dt.astimezone(datetime.timezone.utc)
else:
# timezone naive, assume it's UTC
dtz = dt
warnings.warn(
"Timezone-naive datetime used. Replace with a timezone-aware one if possible.",
category=UserWarning,
stacklevel=2,
)
return dtz.strftime("%Y-%m-%dT%H:%M:%SZ")
22 changes: 22 additions & 0 deletions craft_application/util/platforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,17 @@
from __future__ import annotations

import functools
import os
import platform
from typing import Final

from craft_parts.utils import os_utils
from craft_providers import bases

from .string import strtobool

ENVIRONMENT_CRAFT_MANAGED_MODE: Final[str] = "CRAFT_MANAGED_MODE"


@functools.lru_cache(maxsize=1)
def get_host_architecture() -> str:
Expand Down Expand Up @@ -53,6 +59,22 @@ def get_host_base() -> bases.BaseName:
return bases.BaseName(os_id, version_id)


def get_hostname(hostname: str | None = None) -> str:
"""Return the computer's network name or UNNKOWN if it cannot be determined."""
if hostname is None:
hostname = platform.node()
hostname = hostname.strip()
if not hostname:
hostname = "UNKNOWN"
return hostname


def is_managed_mode() -> bool:
"""Check if craft is running in a managed environment."""
managed_flag = os.getenv(ENVIRONMENT_CRAFT_MANAGED_MODE, "n")
return strtobool(managed_flag)


# architecture translations from the platform syntax to the deb/snap syntax
# These two architecture mappings are almost inverses of each other, except one map is
# not reversible (same value for different keys)
Expand Down
11 changes: 11 additions & 0 deletions docs/reference/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@
Changelog
*********

4.8.0 (2025-MMM-DD)
-------------------

Utils
=====

- Add ``format_timestamp()`` helper that helps with formatting time
in command responses.
- Add ``is_managed_mode()`` helper to check if running in managed mode.
- Add ``get_hostname()`` helper to get a name of current host.

4.7.0 (2024-Dec-19)
-------------------

Expand Down
48 changes: 48 additions & 0 deletions tests/unit/util/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# This file is part of craft-application.
#
# Copyright 2024 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License version 3, as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from datetime import datetime, timedelta, timezone

import pytest

from craft_application.util import format_timestamp

pytestmark = [
pytest.mark.filterwarnings(
"ignore:Timezone-naive datetime used. Replace with a timezone-aware one if possible.",
)
]


@pytest.mark.parametrize(
("dt_obj", "expected"),
[
(
datetime(2024, 5, 23, 13, 24, 0),
"2024-05-23T13:24:00Z",
),
(
datetime(2024, 5, 23, 13, 24, 0, tzinfo=timezone.utc),
"2024-05-23T13:24:00Z",
),
(
datetime(2024, 5, 23, 13, 24, 0, tzinfo=timezone(timedelta(hours=-5))),
"2024-05-23T18:24:00Z",
),
],
)
def test_timezone_parsing(dt_obj: datetime, expected: str) -> None:
assert format_timestamp(dt=dt_obj) == expected
49 changes: 48 additions & 1 deletion tests/unit/util/test_platforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@
"""Tests for platform utilities."""

import pytest
import pytest_mock

from craft_application import util
from craft_application.util.platforms import _ARCH_TRANSLATIONS_DEB_TO_PLATFORM
from craft_application.util.platforms import (
_ARCH_TRANSLATIONS_DEB_TO_PLATFORM,
ENVIRONMENT_CRAFT_MANAGED_MODE,
)


@pytest.mark.parametrize("arch", _ARCH_TRANSLATIONS_DEB_TO_PLATFORM.keys())
Expand All @@ -29,3 +33,46 @@ def test_is_valid_architecture_true(arch):

def test_is_valid_architecture_false():
assert not util.is_valid_architecture("unknown")


def test_get_hostname_returns_node_name(mocker: pytest_mock.MockerFixture) -> None:
mocker.patch("platform.node", return_value="test-platform")
assert util.get_hostname() == "test-platform"


def test_get_hostname_returns_unknown(mocker: pytest_mock.MockerFixture) -> None:
mocker.patch("platform.node", return_value="")
assert util.get_hostname() == "UNKNOWN"


@pytest.mark.parametrize("empty_hostname", ["", " ", "\n\t"])
def test_get_hostname_does_not_allow_empty(empty_hostname: str) -> None:
assert util.get_hostname(empty_hostname) == "UNKNOWN"


@pytest.mark.parametrize("hostname", ["test-hostname", "another-hostname"])
def test_get_hostname_override(hostname: str) -> None:
assert util.get_hostname(hostname) == hostname


def test_is_managed_is_false_if_env_unset(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv(ENVIRONMENT_CRAFT_MANAGED_MODE, raising=False)
assert util.is_managed_mode() is False


@pytest.mark.parametrize(
("managed_mode_env", "expected"),
[
("0", False),
("no", False),
("n", False),
("1", True),
("yes", True),
("y", True),
],
)
def test_is_managed_mode(
monkeypatch: pytest.MonkeyPatch, managed_mode_env: str, *, expected: bool
) -> None:
monkeypatch.setenv(ENVIRONMENT_CRAFT_MANAGED_MODE, managed_mode_env)
assert util.is_managed_mode() is expected

0 comments on commit 8e9893d

Please sign in to comment.