From 014f7115bb3b54c3be90a7955be36da9f11fa679 Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Thu, 30 Jul 2026 20:51:16 +0000 Subject: [PATCH 1/2] feat: add new address fields to Formatter --- google/ads/datamanager_util/format.py | 106 +++++++++++++++++++++++- pyproject.toml | 2 +- tests/formatter_test.py | 111 ++++++++++++++++++++++++++ 3 files changed, 216 insertions(+), 3 deletions(-) diff --git a/google/ads/datamanager_util/format.py b/google/ads/datamanager_util/format.py index e33a87a..46e012f 100644 --- a/google/ads/datamanager_util/format.py +++ b/google/ads/datamanager_util/format.py @@ -184,6 +184,60 @@ def format_region_code(self, region_code: str) -> str: raise ValueError("Region code must be two characters") return region_code + 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. + """ + if address_line is None: + raise ValueError("Address line is None") + address_line = address_line.strip().lower() + # Removes all punctuation and special characters, leaving only + # alphanumeric characters and whitespace. + address_line = re.sub(r"[^\w\s]|_", "", address_line) + if not address_line: + raise ValueError("Address line is blank or empty") + return 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. + """ + if city is None: + raise ValueError("City is None") + city = city.strip().lower() + # Removes all punctuation and special characters, leaving only + # alphanumeric characters and whitespace. + city = re.sub(r"[^\w\s]|_", "", city) + if not city: + raise ValueError("City is blank or empty") + return 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. + """ + if administrative_area is None: + raise ValueError("Administrative area is None") + administrative_area = administrative_area.strip().lower() + # Removes all punctuation and special characters, leaving only + # alphanumeric characters and whitespace. + administrative_area = re.sub(r"[^\w\s]|_", "", administrative_area) + if not administrative_area: + raise ValueError("Administrative area is blank or empty") + return administrative_area + def hash_string(self, s: str) -> bytes: """Returns bytes containing the hash of the string. @@ -195,8 +249,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 +403,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"), + ) + From 7c8094219231fc939f4f251f3fb70e14a5899b7e Mon Sep 17 00:00:00 2001 From: Lindsey Volta Date: Mon, 3 Aug 2026 14:42:03 +0000 Subject: [PATCH 2/2] add _format_location_string for formatting address fields --- google/ads/datamanager_util/format.py | 49 ++++++++++++--------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/google/ads/datamanager_util/format.py b/google/ads/datamanager_util/format.py index 46e012f..56a5af9 100644 --- a/google/ads/datamanager_util/format.py +++ b/google/ads/datamanager_util/format.py @@ -184,6 +184,25 @@ 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. @@ -192,15 +211,7 @@ def format_address_line(self, address_line: str) -> str: Raises: ValueError: If the provided address line is invalid. """ - if address_line is None: - raise ValueError("Address line is None") - address_line = address_line.strip().lower() - # Removes all punctuation and special characters, leaving only - # alphanumeric characters and whitespace. - address_line = re.sub(r"[^\w\s]|_", "", address_line) - if not address_line: - raise ValueError("Address line is blank or empty") - return address_line + 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. @@ -210,15 +221,7 @@ def format_city(self, city: str) -> str: Raises: ValueError: If the provided city is invalid. """ - if city is None: - raise ValueError("City is None") - city = city.strip().lower() - # Removes all punctuation and special characters, leaving only - # alphanumeric characters and whitespace. - city = re.sub(r"[^\w\s]|_", "", city) - if not city: - raise ValueError("City is blank or empty") - return city + 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. @@ -228,15 +231,7 @@ def format_administrative_area(self, administrative_area: str) -> str: Raises: ValueError: If the provided administrative area is invalid. """ - if administrative_area is None: - raise ValueError("Administrative area is None") - administrative_area = administrative_area.strip().lower() - # Removes all punctuation and special characters, leaving only - # alphanumeric characters and whitespace. - administrative_area = re.sub(r"[^\w\s]|_", "", administrative_area) - if not administrative_area: - raise ValueError("Administrative area is blank or empty") - return administrative_area + return self._format_location_string(administrative_area, "Administrative area") def hash_string(self, s: str) -> bytes: """Returns bytes containing the hash of the string.