Coverage for netbox_data_import/identity.py: 100.00%
32 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-30 09:47 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-30 09:47 +0000
1# SPDX-License-Identifier: Apache-2.0
2# SPDX-FileCopyrightText: 2026 Marcin Zieba <marcinpsk@gmail.com>
3"""The one name identity: Python builds keys with it, and PostgreSQL compares stored names with it.
5Both sides map one explicit whitespace set to a space, collapse each run of spaces to one, trim the
6ends, and then apply the full Unicode uppercase mapping. PostgreSQL uppercases under NetBox's
7`natural_sort` ICU collation, whose locale is the root locale, so no language tailors the mapping.
8No side applies NFC or NFKC. `test_identity` runs every storable code point through both sides.
9A Source Adapter imports this module, so Django loads only inside the database helpers.
10"""
12from __future__ import annotations
14import re
15from collections.abc import Iterable
17# U+0009-000D, U+001C-0020, U+0085, U+00A0, U+1680, U+2000-200A, U+2028, U+2029, U+202F, U+205F, U+3000.
18WHITESPACE = "".join(
19 map(
20 chr,
21 (
22 *range(0x09, 0x0E),
23 *range(0x1C, 0x21),
24 0x85,
25 0xA0,
26 0x1680,
27 *range(0x2000, 0x200B),
28 0x2028,
29 0x2029,
30 0x202F,
31 0x205F,
32 0x3000,
33 ),
34 )
35)
36# NetBox creates this collation with the ICU root locale, `und-u-kn-true`, for its own name columns.
37IDENTITY_COLLATION = "natural_sort"
38CANONICAL_NAME = "_ndi_canonical_name"
39# A bracket expression of literal characters, which Python and PostgreSQL read the same way under any collation.
40WHITESPACE_RUN = f"[{WHITESPACE}]+"
42_WHITESPACE_RUN = re.compile(WHITESPACE_RUN)
45def identity_text(value: str) -> str:
46 """Return the comparison key of one name."""
47 return _WHITESPACE_RUN.sub(" ", value).strip(" ").upper()
50def identity_expression(expression):
51 """Return the ORM expression that computes `identity_text` of one text value inside the database."""
52 from django.db.models import Func, TextField, Value
53 from django.db.models.functions import Cast, Collate, Upper
55 # The bytewise collation keeps the input column's own collation out of every step before UPPER.
56 text = Collate(Cast(expression, TextField()), "C")
57 spaced = Func(
58 text, Value(WHITESPACE_RUN), Value(" "), Value("g"), function="REGEXP_REPLACE", output_field=TextField()
59 )
60 trimmed = Func(spaced, Value(" "), function="BTRIM", output_field=TextField())
61 return Upper(Collate(trimmed, IDENTITY_COLLATION))
64def identity_in(field: str, keys: Iterable[str]):
65 """Return a filter that keeps the rows whose *field* has one of these identity keys."""
66 from django.db.models import F
67 from django.db.models.lookups import In
69 return In(identity_expression(F(field)), sorted(set(keys)))
72def with_name_identity(queryset):
73 """Annotate each row with the identity of its name, under `CANONICAL_NAME`."""
74 from django.db.models import F
76 return queryset.annotate(**{CANONICAL_NAME: identity_expression(F("name"))})
79def matching_search(queryset, search: str):
80 """Keep the named rows whose identity contains the identity of *search*; a blank search keeps every row."""
81 from django.db.models import F
82 from django.db.models.lookups import Contains
84 wanted = identity_text(search)
85 if not wanted:
86 return queryset
87 return queryset.filter(Contains(identity_expression(F("name")), wanted))
90__all__ = (
91 "CANONICAL_NAME",
92 "IDENTITY_COLLATION",
93 "WHITESPACE",
94 "WHITESPACE_RUN",
95 "identity_expression",
96 "identity_in",
97 "identity_text",
98 "matching_search",
99 "with_name_identity",
100)