mreg-cli 1.2.0__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.0 → mreg_cli-1.2.2}/.github/workflows/test.yml +4 -8
  2. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/CHANGELOG.md +18 -1
  3. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/CONTRIBUTING +4 -0
  4. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/PKG-INFO +1 -1
  5. mreg_cli-1.2.2/ci/Dockerfile +13 -0
  6. {mreg_cli-1.2.0 → 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.0 → mreg_cli-1.2.2}/ci/testsuite-result.json +7 -66
  9. {mreg_cli-1.2.0 → 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.0 → mreg_cli-1.2.2}/mreg_cli/api/models.py +300 -220
  12. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/cli.py +13 -4
  13. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/commands/dhcp.py +11 -17
  14. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/commands/group.py +3 -3
  15. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/commands/help.py +15 -6
  16. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/commands/host_submodules/a_aaaa.py +6 -5
  17. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/commands/host_submodules/bacnet.py +3 -3
  18. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/commands/host_submodules/cname.py +27 -20
  19. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/commands/host_submodules/core.py +29 -26
  20. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/commands/host_submodules/rr.py +36 -31
  21. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/commands/network.py +3 -4
  22. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/commands/permission.py +2 -2
  23. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/commands/policy.py +4 -4
  24. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/config.py +5 -0
  25. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/errorbuilder.py +23 -23
  26. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/exceptions.py +65 -14
  27. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/main.py +2 -2
  28. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/outputmanager.py +4 -5
  29. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/types.py +16 -0
  30. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/utilities/api.py +10 -2
  31. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli.egg-info/PKG-INFO +1 -1
  32. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli.egg-info/SOURCES.txt +3 -0
  33. mreg_cli-1.2.2/tests/api/test_fields.py +269 -0
  34. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/tests/api/test_models.py +26 -2
  35. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/tests/conftest.py +10 -0
  36. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/tests/test_errorbuilder.py +13 -2
  37. mreg_cli-1.2.2/tests/test_exceptions.py +175 -0
  38. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/uv.lock +1 -1
  39. mreg_cli-1.2.0/mreg_cli/api/fields.py +0 -122
  40. mreg_cli-1.2.0/tests/api/test_fields.py +0 -144
  41. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/.github/workflows/publish.yml +0 -0
  42. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/.gitignore +0 -0
  43. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/.markdownlint.json +0 -0
  44. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/AUTHORS +0 -0
  45. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/LICENSE +0 -0
  46. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/README.md +0 -0
  47. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/ci/docker-compose.yml +0 -0
  48. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/ci/run_testsuite_and_record.sh +0 -0
  49. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/ci/testsuite +0 -0
  50. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/data/mreg-cli.conf +0 -0
  51. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/__about__.py +0 -0
  52. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/__init__.py +0 -0
  53. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/__main__.py +0 -0
  54. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/api/__init__.py +0 -0
  55. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/api/abstracts.py +0 -0
  56. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/api/endpoints.py +0 -0
  57. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/api/history.py +0 -0
  58. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/commands/__init__.py +0 -0
  59. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/commands/base.py +0 -0
  60. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/commands/host.py +0 -0
  61. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/commands/host_submodules/__init__.py +0 -0
  62. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/commands/label.py +0 -0
  63. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/commands/logging.py +0 -0
  64. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/commands/recording.py +0 -0
  65. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/commands/registry.py +0 -0
  66. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/commands/root.py +0 -0
  67. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/commands/zone.py +0 -0
  68. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/help_formatter.py +0 -0
  69. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/prompt.py +0 -0
  70. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/py.typed +0 -0
  71. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/tags.txt +0 -0
  72. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/tokenfile.py +0 -0
  73. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/utilities/__init__.py +0 -0
  74. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/utilities/shared.py +0 -0
  75. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli/utilities/validators.py +0 -0
  76. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli.egg-info/dependency_links.txt +0 -0
  77. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli.egg-info/entry_points.txt +0 -0
  78. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli.egg-info/requires.txt +0 -0
  79. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/mreg_cli.egg-info/top_level.txt +0 -0
  80. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/pyproject.toml +0 -0
  81. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/run.py +0 -0
  82. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/setup.cfg +0 -0
  83. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/tests/__init__.py +0 -0
  84. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/tests/api/__init__.py +0 -0
  85. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/tests/test_prompt.py +0 -0
  86. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/tests/test_tokenfile.py +0 -0
  87. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/tests/utilities/__init__.py +0 -0
  88. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/tests/utilities/test_api.py +0 -0
  89. {mreg_cli-1.2.0 → mreg_cli-1.2.2}/tox.ini +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
@@ -5,7 +5,24 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- <!-- ## [Unreleased] -->
8
+ <!-- ## Unreleased -->
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
+
17
+ ## [1.2.1](https://github.com/unioslo/mreg-cli/releases/tag/1.2.1) - 2024-12-02
18
+
19
+ ### Fixed
20
+
21
+ - Host lookup with IPv6 addresses raising `ValidationError` when no host can be found with the given address.
22
+
23
+ ### Removed
24
+
25
+ - Support for fetching hosts that have a valid IP/MAC address as their hostname.
9
26
 
10
27
  ## [1.2.0](https://github.com/unioslo/mreg-cli/releases/tag/1.2.0) - 2024-11-25
11
28
 
@@ -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.0
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
@@ -6,7 +6,6 @@
6
6
  "command_issued": "zone create example.org hostmaster@example.org ns1.example.org # should require force because ns1 is unknown",
7
7
  "ok": [],
8
8
  "warning": [
9
- "Host ns1.example.org not found.",
10
9
  "ns1.example.org is not in mreg, must force"
11
10
  ],
12
11
  "error": [],
@@ -41,9 +40,7 @@
41
40
  "ok": [
42
41
  "Created zone example.org"
43
42
  ],
44
- "warning": [
45
- "Host ns1.example.org not found."
46
- ],
43
+ "warning": [],
47
44
  "error": [],
48
45
  "output": [],
49
46
  "api_requests": [
@@ -210,10 +207,9 @@
210
207
  "command": "zone set_ns example.org ns2.example.org",
211
208
  "command_filter": null,
212
209
  "command_filter_negate": false,
213
- "command_issued": "zone set_ns example.org ns2.example.org # requires force because ns2 is unknown",
210
+ "command_issued": "zone set_ns example.org ns2.example.org # requires force because ns2 is unknown",
214
211
  "ok": [],
215
212
  "warning": [
216
- "Host ns2.example.org not found.",
217
213
  "ns2.example.org is not in mreg, must force"
218
214
  ],
219
215
  "error": [],
@@ -271,9 +267,7 @@
271
267
  "ok": [
272
268
  "Updated nameservers for example.org"
273
269
  ],
274
- "warning": [
275
- "Host ns2.example.org not found."
276
- ],
270
+ "warning": [],
277
271
  "error": [],
278
272
  "output": [],
279
273
  "api_requests": [
@@ -15283,9 +15277,7 @@
15283
15277
  "ok": [
15284
15278
  "Created zone delegation wut.example.org"
15285
15279
  ],
15286
- "warning": [
15287
- "Host ns2.example.org not found."
15288
- ],
15280
+ "warning": [],
15289
15281
  "error": [],
15290
15282
  "output": [],
15291
15283
  "api_requests": [
@@ -17449,7 +17441,7 @@
17449
17441
  "command_filter_negate": false,
17450
17442
  "command_issued": "policy host_remove fruit foo",
17451
17443
  "ok": [
17452
- "Removed host foo.example.org from role 'fruit'"
17444
+ "Removed host 'foo.example.org' from role 'fruit'"
17453
17445
  ],
17454
17446
  "warning": [],
17455
17447
  "error": [],
@@ -23738,57 +23730,6 @@
23738
23730
  "comment": "This is the comment",
23739
23731
  "zone": 1
23740
23732
  }
23741
- },
23742
- {
23743
- "method": "GET",
23744
- "url": "/api/v1/hosts/?id=14",
23745
- "data": {},
23746
- "status": 200,
23747
- "response": {
23748
- "count": 1,
23749
- "next": null,
23750
- "previous": null,
23751
- "results": [
23752
- {
23753
- "ipaddresses": [
23754
- {
23755
- "macaddress": "",
23756
- "ipaddress": "10.0.0.14",
23757
- "host": 14
23758
- },
23759
- {
23760
- "macaddress": "11:22:33:44:55:66",
23761
- "ipaddress": "10.0.0.15",
23762
- "host": 14
23763
- }
23764
- ],
23765
- "cnames": [
23766
- {
23767
- "name": "fubar.example.org",
23768
- "ttl": null,
23769
- "zone": 1,
23770
- "host": 14
23771
- }
23772
- ],
23773
- "mxs": [],
23774
- "txts": [
23775
- {
23776
- "txt": "v=spf1 -all",
23777
- "host": 14
23778
- }
23779
- ],
23780
- "ptr_overrides": [],
23781
- "hinfo": null,
23782
- "loc": null,
23783
- "bacnetid": null,
23784
- "name": "bar.example.org",
23785
- "contact": "me@example.org",
23786
- "ttl": null,
23787
- "comment": "This is the comment",
23788
- "zone": 1
23789
- }
23790
- ]
23791
- }
23792
23733
  }
23793
23734
  ],
23794
23735
  "time": null
@@ -27544,7 +27485,7 @@
27544
27485
  "command_issued": "host add -ip 10.0.0.0/24 -macaddress aa:bb:cc:cc:bb:aa directfail",
27545
27486
  "ok": [],
27546
27487
  "warning": [
27547
- "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."
27548
27489
  ],
27549
27490
  "error": [],
27550
27491
  "output": [],
@@ -27561,7 +27502,7 @@
27561
27502
  "results": [
27562
27503
  {
27563
27504
  "macaddress": "aa:bb:cc:cc:bb:aa",
27564
- "ipaddress": "10.0.0.4",
27505
+ "ipaddress": "10.0.0.184",
27565
27506
  "host": 18
27566
27507
  }
27567
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.0'
16
- __version_tuple__ = version_tuple = (1, 2, 0)
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."""