-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Move LCC ID conversion to new conventions.py, allow hex not zero padd…
…ed if using dot notation in new form validation function, and add tests.
- Loading branch information
Showing
6 changed files
with
208 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,14 @@ | ||
import re | ||
|
||
|
||
hex_pairs_rc = re.compile(r"^([0-9A-Fa-f]{2})+$") | ||
# {2}: Exactly two characters found (only match if pair) | ||
# +: at least one match plus 0 or more additional matches | ||
|
||
|
||
def only_hex_pairs(value): | ||
"""Check if string contains only machine-readable hex pairs. | ||
See openlcb.conventions submodule for LCC ID dot notation | ||
functions (less restrictive). | ||
""" | ||
return hex_pairs_rc.fullmatch(value) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
from logging import getLogger | ||
|
||
from openlcb import only_hex_pairs | ||
|
||
logger = getLogger(__name__) | ||
|
||
LCC_ID_SEP = "." | ||
|
||
|
||
def hex_to_dotted_lcc_id(hex_s): | ||
return LCC_ID_SEP.join([hex_s[i*2:i*2+2] for i in range(len(hex_s)//2)]) | ||
|
||
|
||
def validate_lcc_id(lcc_id_s): | ||
"""Convert an LCC ID in dot notation to a hex and error pair. | ||
Get a tuple of a hex string and a validation error (or None) | ||
suitable for form validation (Done that way so this is the | ||
only function that does LCC ID analysis). | ||
Args: | ||
lcc_id_s (str): An LCC ID string. Examples: 02.01.57.00.04.9C or | ||
2.1.57.0.4.9C (both give same 12-digit hex string). | ||
Returns: | ||
tuple(str, str): tuple of hex string and error: | ||
- Hex string is 12 characters uppercase, or None if input is bad. | ||
- Error is only not None if hex string is None. | ||
""" | ||
if not lcc_id_s: | ||
error = "[dotted_lcc_id_to_hex] Got {}".format(repr(lcc_id_s)) | ||
# ^ repr shows '' or None | ||
return None, error | ||
parts = lcc_id_s.split(".") | ||
if len(parts) != 6: | ||
error = "Not 6 parts: {}".format(lcc_id_s) | ||
return None, error | ||
hex_s = "" | ||
for part in parts: | ||
if len(part) == 2: | ||
hex_s += part | ||
elif len(part) == 1: # Add leading 0 since not required. | ||
hex_s += "0" + part | ||
elif len(part) < 1: | ||
error = "Extra '.' in {} (not an LCC ID)".format(repr(lcc_id_s)) | ||
return None, error | ||
else: | ||
error = "Wrong length for {}".format(repr(part)) | ||
return None, error | ||
if not only_hex_pairs(hex_s): | ||
error = "Non-hex found in {} (expected 0-9/A-F)".format(repr(lcc_id_s)) | ||
return None, error | ||
return hex_s.upper(), None | ||
|
||
|
||
def dotted_lcc_id_to_hex(lcc_id_s): | ||
hex_s, error = validate_lcc_id(lcc_id_s) | ||
if error: | ||
logger.info(error) | ||
return None | ||
return hex_s | ||
|
||
|
||
def is_hex_lcc_id(value): | ||
"""Check if it is a 12-character LCC ID in pure hex format. | ||
Uppercase or lowercase is valid if 12 characters. If dotted, you | ||
must first use dotted_lcc_id_to_hex to make it machine readable | ||
(including to add zero padding) or see if result is None from that | ||
before calling this. | ||
""" | ||
# if (len(value) < 12) and (len(value) >= minimum_length): | ||
# value = value.zfill(12) # pad left with zeroes | ||
# ^ Commented since dotted_lcc_id_to_hex can be used to get | ||
# a clean one if possible. | ||
if len(value) != 12: | ||
logger.info("Not 12 characters: {}".format(value)) | ||
return False | ||
|
||
return only_hex_pairs(value) | ||
|
||
|
||
def is_dotted_lcc_id(value): | ||
"""It is an LCC ID in dot notation (human readable) | ||
Examples: 02.01.57.00.04.9C or 2.1.57.0.4.9C (same effect) | ||
To generate LCC IDs, first allocate a range at | ||
https://registry.openlcb.org/uniqueidranges | ||
""" | ||
hex_str = dotted_lcc_id_to_hex(value) | ||
if not hex_str: # warning/info logged by dotted_lcc_id_to_hex | ||
return False | ||
return only_hex_pairs(hex_str) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import os | ||
import sys | ||
import unittest | ||
|
||
from logging import getLogger | ||
|
||
logger = getLogger(__name__) | ||
|
||
|
||
if __name__ == "__main__": | ||
# Allow importing repo copy of openlcb if running tests from repo manually. | ||
TESTS_DIR = os.path.dirname(os.path.realpath(__file__)) | ||
REPO_DIR = os.path.dirname(TESTS_DIR) | ||
if os.path.isfile(os.path.join(REPO_DIR, "openlcb", "__init__.py")): | ||
sys.path.insert(0, REPO_DIR) | ||
else: | ||
logger.warning( | ||
"Reverting to installed copy if present (or imports will fail)," | ||
" since test running from repo but could not find openlcb in {}." | ||
.format(repr(REPO_DIR))) | ||
|
||
|
||
from openlcb.conventions import ( # noqa: E402 | ||
dotted_lcc_id_to_hex, | ||
is_dotted_lcc_id, | ||
is_hex_lcc_id, | ||
hex_to_dotted_lcc_id, | ||
) | ||
|
||
|
||
class TestConventions(unittest.TestCase): | ||
"""Cover conventions.py | ||
(is_hex_lcc_id, dotted_lcc_id_to_hex, and is_dotted_lcc_id | ||
cover validate_lcc_id other than checking which validation error | ||
occurred) | ||
""" | ||
|
||
def test_is_hex_lcc_id(self): | ||
self.assertTrue(is_hex_lcc_id("02015700049C")) | ||
self.assertTrue(is_hex_lcc_id("02015700049c")) | ||
|
||
self.assertFalse(is_hex_lcc_id("02")) # only_hex_pairs yet too short | ||
self.assertFalse(is_hex_lcc_id("2.1.57.0.4.9C")) # not converted | ||
self.assertFalse(is_hex_lcc_id("02.01.57.00.04.9C")) # not converted | ||
self.assertFalse(is_hex_lcc_id("02015700049C.")) | ||
self.assertFalse(is_hex_lcc_id("0")) | ||
self.assertFalse(is_hex_lcc_id("_02015700049C")) # contains start character | ||
self.assertFalse(is_hex_lcc_id("org_product_02015700049C")) # service name not split | ||
|
||
def test_dotted_lcc_id_to_hex(self): | ||
self.assertEqual(dotted_lcc_id_to_hex("2.1.57.0.4.9C"), | ||
"02015700049C") | ||
self.assertEqual(dotted_lcc_id_to_hex("02.01.57.00.04.9C"), | ||
"02015700049C") | ||
self.assertEqual(dotted_lcc_id_to_hex("02.01.57.00.04.9c"), | ||
"02015700049C") # converted to uppercase OK | ||
|
||
self.assertNotEqual(dotted_lcc_id_to_hex("02.01.57.00.04.9c"), | ||
"02015700049c") # function should convert to uppercase | ||
self.assertIsNone(dotted_lcc_id_to_hex("02015700049C")) | ||
self.assertIsNone(dotted_lcc_id_to_hex("02015700049c")) | ||
self.assertIsNone(dotted_lcc_id_to_hex("02")) # only_hex_pairs yet too short | ||
self.assertIsNone(dotted_lcc_id_to_hex("02015700049C.")) | ||
self.assertIsNone(dotted_lcc_id_to_hex("0")) | ||
self.assertIsNone(dotted_lcc_id_to_hex("_02015700049C")) # contains start character | ||
self.assertIsNone(dotted_lcc_id_to_hex("org_product_02015700049C")) # service name not split | ||
|
||
def test_is_dotted_lcc_id(self): | ||
self.assertTrue(is_dotted_lcc_id("02.01.57.00.04.9C")) | ||
self.assertTrue(is_dotted_lcc_id("2.01.57.00.04.9C")) | ||
self.assertTrue(is_dotted_lcc_id("2.1.57.0.4.9C")) | ||
self.assertTrue(is_dotted_lcc_id("2.1.57.0.4.9c")) | ||
|
||
self.assertFalse(is_dotted_lcc_id("02.01.57.00.04.9G")) # G is not hex | ||
self.assertFalse(is_dotted_lcc_id(".01.57.00.04.9C")) # empty pair | ||
self.assertFalse(is_dotted_lcc_id("01.57.00.04.9C")) # only 5 pairs | ||
self.assertFalse(is_dotted_lcc_id("02015700049C")) | ||
|
||
def test_hex_to_dotted_lcc_id(self): | ||
# NOTE: No case conversion occurs in this direction, | ||
# so that doesn't need to be checked. | ||
self.assertEqual(hex_to_dotted_lcc_id("02015700049C"), | ||
"02.01.57.00.04.9C") | ||
|
||
|
||
if __name__ == '__main__': | ||
unittest.main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters