mreg-cli 1.2.1__tar.gz → 1.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/.github/workflows/test.yml +4 -8
  2. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/CHANGELOG.md +7 -0
  3. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/CONTRIBUTING +4 -0
  4. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/PKG-INFO +1 -1
  5. mreg_cli-1.2.2/ci/Dockerfile +13 -0
  6. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/ci/diff.py +2 -2
  7. mreg_cli-1.2.2/ci/run_testsuite_and_record_V2.sh +88 -0
  8. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/ci/testsuite-result.json +3 -54
  9. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/_version.py +2 -2
  10. mreg_cli-1.2.2/mreg_cli/api/fields.py +172 -0
  11. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/api/models.py +139 -167
  12. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/cli.py +11 -2
  13. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/commands/dhcp.py +11 -17
  14. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/commands/group.py +3 -3
  15. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/commands/help.py +0 -1
  16. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/commands/host_submodules/a_aaaa.py +3 -2
  17. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/commands/host_submodules/bacnet.py +3 -3
  18. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/commands/host_submodules/cname.py +27 -20
  19. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/commands/host_submodules/core.py +29 -26
  20. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/commands/host_submodules/rr.py +32 -27
  21. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/commands/network.py +2 -3
  22. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/commands/policy.py +4 -4
  23. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/exceptions.py +48 -3
  24. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/types.py +16 -0
  25. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/utilities/api.py +8 -0
  26. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli.egg-info/PKG-INFO +1 -1
  27. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli.egg-info/SOURCES.txt +3 -0
  28. mreg_cli-1.2.2/tests/api/test_fields.py +269 -0
  29. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/tests/conftest.py +10 -0
  30. mreg_cli-1.2.2/tests/test_exceptions.py +175 -0
  31. mreg_cli-1.2.1/mreg_cli/api/fields.py +0 -137
  32. mreg_cli-1.2.1/tests/api/test_fields.py +0 -144
  33. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/.github/workflows/publish.yml +0 -0
  34. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/.gitignore +0 -0
  35. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/.markdownlint.json +0 -0
  36. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/AUTHORS +0 -0
  37. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/LICENSE +0 -0
  38. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/README.md +0 -0
  39. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/ci/docker-compose.yml +0 -0
  40. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/ci/run_testsuite_and_record.sh +0 -0
  41. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/ci/testsuite +0 -0
  42. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/data/mreg-cli.conf +0 -0
  43. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/__about__.py +0 -0
  44. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/__init__.py +0 -0
  45. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/__main__.py +0 -0
  46. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/api/__init__.py +0 -0
  47. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/api/abstracts.py +0 -0
  48. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/api/endpoints.py +0 -0
  49. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/api/history.py +0 -0
  50. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/commands/__init__.py +0 -0
  51. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/commands/base.py +0 -0
  52. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/commands/host.py +0 -0
  53. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/commands/host_submodules/__init__.py +0 -0
  54. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/commands/label.py +0 -0
  55. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/commands/logging.py +0 -0
  56. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/commands/permission.py +0 -0
  57. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/commands/recording.py +0 -0
  58. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/commands/registry.py +0 -0
  59. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/commands/root.py +0 -0
  60. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/commands/zone.py +0 -0
  61. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/config.py +0 -0
  62. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/errorbuilder.py +0 -0
  63. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/help_formatter.py +0 -0
  64. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/main.py +0 -0
  65. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/outputmanager.py +0 -0
  66. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/prompt.py +0 -0
  67. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/py.typed +0 -0
  68. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/tags.txt +0 -0
  69. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/tokenfile.py +0 -0
  70. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/utilities/__init__.py +0 -0
  71. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/utilities/shared.py +0 -0
  72. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli/utilities/validators.py +0 -0
  73. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli.egg-info/dependency_links.txt +0 -0
  74. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli.egg-info/entry_points.txt +0 -0
  75. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli.egg-info/requires.txt +0 -0
  76. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/mreg_cli.egg-info/top_level.txt +0 -0
  77. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/pyproject.toml +0 -0
  78. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/run.py +0 -0
  79. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/setup.cfg +0 -0
  80. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/tests/__init__.py +0 -0
  81. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/tests/api/__init__.py +0 -0
  82. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/tests/api/test_models.py +0 -0
  83. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/tests/test_errorbuilder.py +0 -0
  84. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/tests/test_prompt.py +0 -0
  85. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/tests/test_tokenfile.py +0 -0
  86. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/tests/utilities/__init__.py +0 -0
  87. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/tests/utilities/test_api.py +0 -0
  88. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/tox.ini +0 -0
  89. {mreg_cli-1.2.1 → mreg_cli-1.2.2}/uv.lock +0 -0
@@ -14,20 +14,15 @@ jobs:
14
14
  python-version:
15
15
  - '3.11'
16
16
  - '3.12'
17
+ - '3.13'
17
18
  env:
18
19
  # Disable colors and formatting in Rich console output
19
20
  TERM: dumb
20
21
  steps:
21
22
  - name: Checkout
22
23
  uses: actions/checkout@v4
23
- - name: Install uv
24
- uses: astral-sh/setup-uv@v2
25
- - name: Set up Python ${{ matrix.python-version }}
26
- run: uv python install ${{ matrix.python-version }}
27
- - name: Install mreg-cli
28
- run: uv sync
29
24
  - name: Test and compare api calls
30
- run: uv run ci/run_testsuite_and_record.sh
25
+ run: ci/run_testsuite_and_record_V2.sh ${{ matrix.python-version }}
31
26
 
32
27
  tox:
33
28
  name: tox
@@ -68,6 +63,7 @@ jobs:
68
63
  python-version:
69
64
  - "3.11"
70
65
  - "3.12"
66
+ - '3.13'
71
67
  steps:
72
68
  - uses: actions/checkout@v4
73
69
  - name: Install uv
@@ -77,4 +73,4 @@ jobs:
77
73
  - name: Install dependencies
78
74
  run: uv sync
79
75
  - name: Run unittest
80
- run: uv run pytest
76
+ run: uv run pytest
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  <!-- ## Unreleased -->
9
9
 
10
+ ## [1.2.2](https://github.com/unioslo/mreg-cli/releases/tag/1.2.2) - 2024-12-09
11
+
12
+ ### Fixed
13
+
14
+ - `dhcp assoc` and `host add` commands displaying unique constraint error instead of a proper error message when a MAC is already associated with an IP.
15
+ - Validation errors causing the application to crash instead of displaying a proper error message.
16
+
10
17
  ## [1.2.1](https://github.com/unioslo/mreg-cli/releases/tag/1.2.1) - 2024-12-02
11
18
 
12
19
  ### Fixed
@@ -9,6 +9,10 @@ If you would like to contribute to this project, please follow these steps:
9
9
  5. Push to the branch (`git push origin feature/branch-name`)
10
10
  6. Create a Pull Request
11
11
 
12
+ ## Testing
13
+
14
+ To run tests locally, just run the script `ci/run_testsuite_and_record_V2.sh` from anywhere. The only prerequisite is a working installation of docker or podman.
15
+
12
16
  ## Publishing
13
17
 
14
18
  Publishing new versions is automatically handled by GitHub actions for versions tagged with `x.y.z[.prerelease]` (`1.2.3`, `1.2.3.rc1`, etc.). If you are a maintainer, you can create a new release by following these steps:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mreg-cli
3
- Version: 1.2.1
3
+ Version: 1.2.2
4
4
  Summary: MREG interactive command line client
5
5
  Author: Others (see AUTHORS)
6
6
  Author-email: Øyvind Kolbu <oyvind.kolbu@usit.uio.no>, Safet A <amedov@usit.uio.no>, Magnus Hirth <mhmorgan42@gmail.com>, Nico <nicolaydmohebi@gmail.com>, Marius Bakke <marius.bakke@usit.uio.no>, Fredrik Larsen <fredrik.larsen@usit.uio.no>, Paal Braathen <paal.braathen@usit.uio.no>, Nils Hiorth <nils@nls.me>, Dmytro Karpenko <dmytrok@usit.uio.no>
@@ -0,0 +1,13 @@
1
+ # This container image is only used temporarily for testing during a CI process.
2
+ FROM ubuntu:latest
3
+ ARG python_version=3.12
4
+
5
+ RUN apt-get -qq update
6
+ RUN apt-get -y install curl git >/dev/null
7
+ RUN curl -LsSf https://astral.sh/uv/0.5.6/install.sh | sh
8
+ RUN /root/.local/bin/uv self update -q
9
+ RUN /root/.local/bin/uv python install ${python_version}
10
+ COPY . /build
11
+ RUN cd /build; /root/.local/bin/uv sync -q
12
+
13
+ CMD cd /build/ci; /root/.local/bin/uv run bash -c 'echo "test" | mreg-cli -u test -d example.org --url http://127.0.0.1:8000 --source testsuite --record new_testsuite_log.json --record-without-timestamps -v ERROR >/dev/null'
@@ -290,8 +290,8 @@ class CommandDiffer:
290
290
  new_testsuite_results.append(expected.original)
291
291
  self.diff_unresolved += 1
292
292
  else:
293
- # No diff, keep new line
294
- new_testsuite_results.append(result.original)
293
+ # No diff, keep old line
294
+ new_testsuite_results.append(expected.original)
295
295
 
296
296
  # Only write back changes if we are in review mode and there are changes
297
297
  if self.review and self.diff_resolved > 0:
@@ -0,0 +1,88 @@
1
+ #!/bin/bash
2
+
3
+ # exit immediately on error
4
+ set -e
5
+
6
+ # check if we have docker or podman
7
+ if command -v docker >/dev/null 2>&1 && ! docker 2>&1 | grep -q podman; then
8
+ echo "Using Docker"
9
+ GH_PARAMS="--ansi=never"
10
+ elif command -v podman >/dev/null 2>&1; then
11
+ echo "Using Podman"
12
+ GH_PARAMS="--no-ansi"
13
+ PODMAN=1
14
+ alias docker=podman
15
+ else
16
+ echo "Found neither docker nor podman."
17
+ exit 1
18
+ fi
19
+
20
+ # clean up on exit, even if something fails
21
+ function cleanup {
22
+ set +e
23
+ if [[ -n "$GITHUB_ACTIONS" ]]; then
24
+ docker compose $GH_PARAMS down
25
+ else
26
+ docker compose down
27
+ fi
28
+ docker ps -a | grep "mreg-" | awk '{print $1}' | xargs -r docker stop
29
+ docker ps -a | grep "mreg-" | awk '{print $1}' | xargs -r docker rm
30
+ docker images | grep "mreg-" | awk '{print $1}' | xargs -r docker rmi
31
+ rm -f new_testsuite_log.json
32
+ echo "cleanup done."
33
+ }
34
+ trap cleanup EXIT
35
+
36
+ # chdir to where this script is
37
+ cd `dirname $0`
38
+
39
+ # get Python version from argument
40
+ PYTHON_VERSION=3.12
41
+ if [ ! -z "$1" ]; then
42
+ PYTHON_VERSION=$1
43
+ fi
44
+ echo "Python version $PYTHON_VERSION"
45
+
46
+ # build a container image for mreg-cli
47
+ docker build -f Dockerfile -t mreg-cli --build-arg python_version=$PYTHON_VERSION ..
48
+
49
+ # start mreg+postgres in containers
50
+ if [[ -n "$GITHUB_ACTIONS" ]]; then
51
+ if [[ "$PODMAN" == "1" ]]; then
52
+ docker compose $GH_PARAMS pull >/dev/null 2>&1
53
+ else
54
+ docker compose $GH_PARAMS pull --quiet
55
+ fi
56
+ docker compose $GH_PARAMS up -d
57
+ else
58
+ docker compose up -d
59
+ fi
60
+
61
+ # give mreg some time to create the database schema and start up
62
+ sleep 5s
63
+
64
+ # create a superuser
65
+ if docker exec -t mreg uv version >/dev/null; then
66
+ docker exec -t mreg uv run /app/manage.py create_mreg_superuser --username test --password test
67
+ else
68
+ docker exec -t mreg /app/manage.py create_mreg_superuser --username test --password test
69
+ fi
70
+
71
+ # test connectivity
72
+ #docker run --rm --tty --network host --entrypoint curl mreg-cli --head http://127.0.0.1:8000/admin/login/
73
+
74
+ # start the mreg-cli container, which will automatically run the test suite
75
+ echo "Running the tests..."
76
+ docker run --name mreg-cli --network host --tty mreg-cli
77
+ docker commit mreg-cli finished-mreg-tests # because inside is the file new_testsuite_log.json which we want to look at
78
+
79
+ # show a detailed diff (and review if running locally)
80
+ if [[ -n "$GITHUB_ACTIONS" ]]; then
81
+ docker run --rm --tty --entrypoint bash finished-mreg-tests -c 'cd /build/ci; /root/.local/bin/uv run diff.py testsuite-result.json new_testsuite_log.json'
82
+ exit $?
83
+ else
84
+ docker run -it --name finished-mreg-tests --entrypoint bash finished-mreg-tests -c 'cd /build/ci; /root/.local/bin/uv run diff.py testsuite-result.json new_testsuite_log.json --review'
85
+ EXITCODE=$?
86
+ docker cp finished-mreg-tests:/build/ci/testsuite-result.json .
87
+ exit $EXITCODE
88
+ fi
@@ -17441,7 +17441,7 @@
17441
17441
  "command_filter_negate": false,
17442
17442
  "command_issued": "policy host_remove fruit foo",
17443
17443
  "ok": [
17444
- "Removed host foo.example.org from role 'fruit'"
17444
+ "Removed host 'foo.example.org' from role 'fruit'"
17445
17445
  ],
17446
17446
  "warning": [],
17447
17447
  "error": [],
@@ -23730,57 +23730,6 @@
23730
23730
  "comment": "This is the comment",
23731
23731
  "zone": 1
23732
23732
  }
23733
- },
23734
- {
23735
- "method": "GET",
23736
- "url": "/api/v1/hosts/?id=14",
23737
- "data": {},
23738
- "status": 200,
23739
- "response": {
23740
- "count": 1,
23741
- "next": null,
23742
- "previous": null,
23743
- "results": [
23744
- {
23745
- "ipaddresses": [
23746
- {
23747
- "macaddress": "",
23748
- "ipaddress": "10.0.0.14",
23749
- "host": 14
23750
- },
23751
- {
23752
- "macaddress": "11:22:33:44:55:66",
23753
- "ipaddress": "10.0.0.15",
23754
- "host": 14
23755
- }
23756
- ],
23757
- "cnames": [
23758
- {
23759
- "name": "fubar.example.org",
23760
- "ttl": null,
23761
- "zone": 1,
23762
- "host": 14
23763
- }
23764
- ],
23765
- "mxs": [],
23766
- "txts": [
23767
- {
23768
- "txt": "v=spf1 -all",
23769
- "host": 14
23770
- }
23771
- ],
23772
- "ptr_overrides": [],
23773
- "hinfo": null,
23774
- "loc": null,
23775
- "bacnetid": null,
23776
- "name": "bar.example.org",
23777
- "contact": "me@example.org",
23778
- "ttl": null,
23779
- "comment": "This is the comment",
23780
- "zone": 1
23781
- }
23782
- ]
23783
- }
23784
23733
  }
23785
23734
  ],
23786
23735
  "time": null
@@ -27536,7 +27485,7 @@
27536
27485
  "command_issued": "host add -ip 10.0.0.0/24 -macaddress aa:bb:cc:cc:bb:aa directfail",
27537
27486
  "ok": [],
27538
27487
  "warning": [
27539
- "MAC address aa:bb:cc:cc:bb:aa is already associated with 10.0.0.4"
27488
+ "MAC address aa:bb:cc:cc:bb:aa is already associated with IP address 10.0.0.184, must force."
27540
27489
  ],
27541
27490
  "error": [],
27542
27491
  "output": [],
@@ -27553,7 +27502,7 @@
27553
27502
  "results": [
27554
27503
  {
27555
27504
  "macaddress": "aa:bb:cc:cc:bb:aa",
27556
- "ipaddress": "10.0.0.4",
27505
+ "ipaddress": "10.0.0.184",
27557
27506
  "host": 18
27558
27507
  }
27559
27508
  ]
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.2.1'
16
- __version_tuple__ = version_tuple = (1, 2, 1)
15
+ __version__ = version = '1.2.2'
16
+ __version_tuple__ = version_tuple = (1, 2, 2)
@@ -0,0 +1,172 @@
1
+ """Custom field types for Pydantic models.
2
+
3
+ The types validate to basic types like str, int, etc., but with additional
4
+ validation added to them. The types are used in Pydantic models for consistent
5
+ validation of common fields such as hostnames, MAC addresses, etc.
6
+
7
+ Warning:
8
+ Values constructed from these types should NOT be checked at runtime with isinstance()!
9
+ Pydantic will always coerce these types to their schema types (str, int, etc.).
10
+
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ import re
17
+ from typing import Annotated, Any
18
+
19
+ from pydantic import AfterValidator, BeforeValidator, GetCoreSchemaHandler
20
+ from pydantic_core import core_schema
21
+ from pydantic_extra_types.mac_address import MacAddress as PydanticMacAddress
22
+
23
+ from mreg_cli.config import MregCliConfig
24
+ from mreg_cli.exceptions import InputFailure
25
+ from mreg_cli.types import get_type_adapter
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class HostName(str):
31
+ """Hostname string type."""
32
+
33
+ @classmethod
34
+ def parse(cls, obj: Any) -> HostName | None:
35
+ """Parse a hostname from a string. Returns None if the hostname is invalid.
36
+
37
+ :param obj: The object to parse.
38
+ :returns: The hostname as a string or None if it is invalid.
39
+ """
40
+ try:
41
+ return cls.parse_or_raise(obj)
42
+ except InputFailure:
43
+ return None
44
+
45
+ @classmethod
46
+ def parse_or_raise(cls, obj: Any) -> HostName:
47
+ """Parse a hostname from a string. Returns the hostname as a string.
48
+
49
+ :param obj: The object to parse.
50
+ :returns: The hostname as a string.
51
+ :raises ValueError: If the object is not a valid hostname.
52
+ """
53
+ try:
54
+ adapter = get_type_adapter(cls)
55
+ return cls(adapter.validate_python(obj))
56
+ except ValueError as e:
57
+ raise InputFailure(f"Invalid hostname '{obj}'") from e
58
+
59
+ @staticmethod
60
+ def validate_hostname(value: str) -> str:
61
+ """Validate the hostname."""
62
+ value = value.lower()
63
+
64
+ if re.search(r"^(\*\.)?([a-z0-9_][a-z0-9\-]*\.?)+$", value) is None:
65
+ raise InputFailure(f"Invalid input for hostname: {value}")
66
+
67
+ # Assume user is happy with domain, but strip the dot.
68
+ if value.endswith("."):
69
+ return value[:-1]
70
+
71
+ # If a dot in name, assume long name.
72
+ if "." in value:
73
+ return value
74
+
75
+ config = MregCliConfig()
76
+ default_domain = config.get("domain")
77
+ # Append domain name if in config and it does not end with it
78
+ if default_domain and not value.endswith(default_domain):
79
+ return f"{value}.{default_domain}"
80
+ return value
81
+
82
+ @classmethod
83
+ def __get_pydantic_core_schema__(
84
+ cls, source: type[Any], handler: GetCoreSchemaHandler
85
+ ) -> core_schema.CoreSchema:
86
+ """Return a Pydantic CoreSchema with the hostname validation.
87
+
88
+ :param source: The source type to be converted.
89
+ :param handler: The handler to get the CoreSchema.
90
+ :returns: A Pydantic CoreSchema with the hostname validation.
91
+
92
+ """
93
+ return core_schema.with_info_before_validator_function(
94
+ cls._validate,
95
+ core_schema.str_schema(),
96
+ )
97
+
98
+ @classmethod
99
+ def _validate(cls, __input_value: str, _: Any) -> str:
100
+ """Validate a hostname from the provided str value.
101
+
102
+ Args:
103
+ __input_value: The str value to be validated.
104
+ _: The source type to be converted.
105
+
106
+ Returns:
107
+ str: The parsed hostname.
108
+
109
+ """
110
+ return cls.validate_hostname(__input_value)
111
+
112
+
113
+ class MacAddress(PydanticMacAddress):
114
+ """MAC address string type used in Pydantic models."""
115
+
116
+ @classmethod
117
+ def parse(cls, obj: Any) -> MacAddress | None:
118
+ """Parse a MAC address from a string. Returns None if the MAC address is invalid.
119
+
120
+ :param obj: The object to parse.
121
+ :returns: The MAC address as a string or None if it is invalid.
122
+ """
123
+ try:
124
+ return cls.parse_or_raise(obj)
125
+ except InputFailure:
126
+ return None
127
+
128
+ @classmethod
129
+ def parse_or_raise(cls, obj: Any) -> MacAddress:
130
+ """Parse a MAC address from a string. Returns the MAC address as a string.
131
+
132
+ :param obj: The object to parse.
133
+ :returns: The MAC address as a string.
134
+ :raises ValueError: If the object is not a valid MAC address.
135
+ """
136
+ try:
137
+ adapter = get_type_adapter(cls)
138
+ return cls(adapter.validate_python(obj))
139
+ except ValueError as e:
140
+ raise InputFailure(f"Invalid MAC address '{obj}'") from e
141
+
142
+
143
+ def _extract_name(value: Any) -> str:
144
+ """Extract the "name" value from a dictionary.
145
+
146
+ :param v: Dictionary containing the name.
147
+ :returns: Extracted name as a string.
148
+ """
149
+ if isinstance(value, dict):
150
+ try:
151
+ return str(value["name"]) # pyright: ignore[reportUnknownArgumentType]
152
+ except KeyError:
153
+ logger.error("No 'name' key in %s", value) # pyright: ignore[reportUnknownArgumentType]
154
+ return ""
155
+ return value
156
+
157
+
158
+ def _remove_falsy_list_items(value: Any) -> Any:
159
+ """Remove falsy items from a list.
160
+
161
+ For use in validators only.
162
+ """
163
+ if isinstance(value, list):
164
+ return [i for i in value if i] # pyright: ignore[reportUnknownVariableType]
165
+ return value
166
+
167
+
168
+ NameList = Annotated[
169
+ list[Annotated[str, BeforeValidator(_extract_name)]],
170
+ AfterValidator(_remove_falsy_list_items),
171
+ ]
172
+ """List of names extracted from a list of dicts."""