diff --git a/google/ads/datamanager_util/format.py b/google/ads/datamanager_util/format.py index e33a87a..56a5af9 100644 --- a/google/ads/datamanager_util/format.py +++ b/google/ads/datamanager_util/format.py @@ -184,6 +184,55 @@ def format_region_code(self, region_code: str) -> str: raise ValueError("Region code must be two characters") return region_code + def _format_location_string(self, value: str, label: str) -> str: + """Returns the normalized and formatted location string. + + Args: + value: the string to format. + label: the label used for error messages. + Raises: + ValueError: If the provided value is invalid. + """ + if value is None: + raise ValueError(f"{label} is None") + value = value.strip().lower() + # Removes all punctuation and special characters, leaving only + # alphanumeric characters and whitespace. + value = re.sub(r"[^\w\s]|_", "", value) + if not value: + raise ValueError(f"{label} is blank or empty") + return value + + def format_address_line(self, address_line: str) -> str: + """Returns the normalized and formatted address line as a string. + + Args: + address_line: the address line. + Raises: + ValueError: If the provided address line is invalid. + """ + return self._format_location_string(address_line, "Address line") + + def format_city(self, city: str) -> str: + """Returns the normalized and formatted city as a string. + + Args: + city: the city. + Raises: + ValueError: If the provided city is invalid. + """ + return self._format_location_string(city, "City") + + def format_administrative_area(self, administrative_area: str) -> str: + """Returns the normalized and formatted administrative area as a string. + + Args: + administrative_area: the administrative area. + Raises: + ValueError: If the provided administrative area is invalid. + """ + return self._format_location_string(administrative_area, "Administrative area") + def hash_string(self, s: str) -> bytes: """Returns bytes containing the hash of the string. @@ -195,8 +244,7 @@ def hash_string(self, s: str) -> bytes: """ if s is None: raise ValueError("String is None") - s = "".join(s.split()) - if len(s) == 0: + if len(s.strip()) == 0: raise ValueError("String is blank or empty") return hashlib.sha256(s.encode()).digest() @@ -350,6 +398,55 @@ def process_postal_code(self, postal_code: str) -> str: """ return self.format_postal_code(postal_code) + def process_address_line( + self, + address_line: str, + encoding: Encoding, + encrypter: Optional[Encrypter] = None, + ) -> str: + """Formats, hashes, and encodes an address line. + + Args: + address_line: The address line to process. + encoding: The encoding to use. + encrypter: An optional Encrypter to use for encryption. + + Returns: + The processed address line. + """ + formatted_address_line = self.format_address_line(address_line) + if encrypter: + return self._hash_encode_and_encrypt( + formatted_address_line, encoding, encrypter + ) + return self._hash_and_encode(formatted_address_line, encoding) + + def process_city(self, city: str) -> str: + """Processes a city. + + This is a convenience method that simply calls format_city. + + Args: + city: The city to process. + + Returns: + The processed city. + """ + return self.format_city(city) + + def process_administrative_area(self, administrative_area: str) -> str: + """Processes an administrative area. + + This is a convenience method that simply calls format_administrative_area. + + Args: + administrative_area: The administrative area to process. + + Returns: + The processed administrative area. + """ + return self.format_administrative_area(administrative_area) + def _hash_and_encode( self, normalized_string: str, encoding: Encoding ) -> str: diff --git a/pyproject.toml b/pyproject.toml index c78e298..4512b80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ build-backend = "setuptools.build_meta" [project] name = "google-ads-datamanager-util" -version = "0.3.0" +version = "0.4.0" description = "Utilities for the Data Manager API" readme = "./README.rst" requires-python = ">=3.9, <3.14" diff --git a/tests/formatter_test.py b/tests/formatter_test.py index 796eea5..c6fdd03 100644 --- a/tests/formatter_test.py +++ b/tests/formatter_test.py @@ -326,3 +326,114 @@ def test_process_postal_code_valid_inputs(self): self.assertEqual( "1229-076", self.formatter.process_postal_code(" 1229-076 ") ) + + def test_format_address_line_valid_inputs(self): + self.assertEqual( + "1800 amphibious blvd", + self.formatter.format_address_line(" 1800 Amphibious Blvd. "), + ) + + def test_format_address_line_invalid_inputs(self): + with self.assertRaises(ValueError): + self.formatter.format_address_line(None) + with self.assertRaises(ValueError): + self.formatter.format_address_line("") + with self.assertRaises(ValueError): + self.formatter.format_address_line(" ") + + def test_format_city_valid_inputs(self): + self.assertEqual( + "mountain view", + self.formatter.format_city(" Mountain View "), + ) + self.assertEqual( + "mountain view", + self.formatter.format_city("Mountain View,"), + ) + + def test_format_city_invalid_inputs(self): + with self.assertRaises(ValueError): + self.formatter.format_city(None) + with self.assertRaises(ValueError): + self.formatter.format_city("") + with self.assertRaises(ValueError): + self.formatter.format_city(" ") + + def test_format_administrative_area_valid_inputs(self): + self.assertEqual( + "ca", + self.formatter.format_administrative_area(" CA "), + ) + self.assertEqual( + "california", + self.formatter.format_administrative_area(" California "), + ) + self.assertEqual( + "ca", + self.formatter.format_administrative_area("C.A."), + ) + self.assertEqual( + "ca", + self.formatter.format_administrative_area("CA."), + ) + + def test_format_administrative_area_invalid_inputs(self): + with self.assertRaises(ValueError): + self.formatter.format_administrative_area(None) + with self.assertRaises(ValueError): + self.formatter.format_administrative_area("") + with self.assertRaises(ValueError): + self.formatter.format_administrative_area(" ") + + def test_process_address_line_valid_inputs_hex_encoding(self): + encoded_hash = ( + "ff75e73a0e768cc1fa28a64faebbceccb562d7c05f2ffcdd8d100abad73e4579" + ) + self.assertEqual( + encoded_hash, + self.formatter.process_address_line( + " 1800 Amphibious Blvd. ", Encoding.HEX + ), + ) + + def test_process_address_line_valid_inputs_base64_encoding(self): + encoded_hash = "/3XnOg52jMH6KKZPrrvOzLVi18BfL/zdjRAKutc+RXk=" + self.assertEqual( + encoded_hash, + self.formatter.process_address_line( + " 1800 Amphibious Blvd. ", Encoding.BASE64 + ), + ) + + def test_process_city_valid_inputs(self): + self.assertEqual( + "mountain view", + self.formatter.process_city(" Mountain View "), + ) + + def test_process_administrative_area_valid_inputs(self): + self.assertEqual( + "ca", + self.formatter.process_administrative_area(" CA "), + ) + + def test_format_example_address(self): + # 1800 Amphibious Blvd. + # Mountain View, CA 94045 + self.assertEqual( + "1800 amphibious blvd", + self.formatter.format_address_line("1800 Amphibious Blvd."), + ) + self.assertEqual( + "mountain view", + self.formatter.format_city("Mountain View"), + ) + self.assertEqual( + "ca", + self.formatter.format_administrative_area("CA."), + ) + self.assertEqual( + "94045", + self.formatter.format_postal_code("94045"), + ) +