From 1a99861a4d0785f39031a1bd5a6c8d2f405cd146 Mon Sep 17 00:00:00 2001 From: saivats Date: Mon, 26 Jan 2026 11:50:20 +0530 Subject: [PATCH 1/4] feat: add search_a_licious support to ProductResource --- openfoodfacts/api.py | 45 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/openfoodfacts/api.py b/openfoodfacts/api.py index 1f8b1507..5931e911 100644 --- a/openfoodfacts/api.py +++ b/openfoodfacts/api.py @@ -274,6 +274,51 @@ def text_search( auth=get_http_auth(self.api_config.environment), ) + def search_a_licious( + self, + query: str, + page: int = 1, + page_size: int = 24, + sort_by: Optional[str] = None, + **kwargs: Any, + ) -> Optional[JSONType]: + """Search products using the new high-performance Search-a-licious endpoint. + + :param query: the search query + :param page: requested page (starts at 1), defaults to 1 + :param page_size: number of items per page, defaults to 24 + :param sort_by: result sorting key, defaults to None + :param kwargs: additional filters + :return: the search results (standardized with 'products' list) + """ + # The new high-performance search endpoint + url = "https://search.openfoodfacts.org/search" + + params = { + "q": query, + "page": page, + "page_size": page_size, + } + + if sort_by: + params["sort_by"] = sort_by + + params.update(kwargs) + + # Get the raw response + result = send_get_request( + url=url, + api_config=self.api_config, + params=params, + auth=get_http_auth(self.api_config.environment), + ) + + # Compatibility Fix: Rename 'hits' to 'products' to match existing SDK behavior + if result and "hits" in result: + result["products"] = result.pop("hits") + + return result + def update(self, body: Dict[str, Any]): """Create a new product or update an existing one.""" if not body.get("code"): From 20eae8e32bd6073c806591897a19b65c1a3efe0e Mon Sep 17 00:00:00 2001 From: saivats Date: Mon, 26 Jan 2026 14:46:29 +0530 Subject: [PATCH 2/4] refactor: rename to search, add environment support and alpha warning --- openfoodfacts/api.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/openfoodfacts/api.py b/openfoodfacts/api.py index 5931e911..68b079e7 100644 --- a/openfoodfacts/api.py +++ b/openfoodfacts/api.py @@ -193,6 +193,9 @@ def __init__(self, api_config: APIConfig): environment=api_config.environment, country_code=self.api_config.country.name, ) + # Handle environment for Search-a-licious (Requested by review) + domain = "net" if self.api_config.environment == Environment.net else "org" + self.base_searchalicious_url = f"https://search.openfoodfacts.{domain}" def get( self, @@ -274,7 +277,7 @@ def text_search( auth=get_http_auth(self.api_config.environment), ) - def search_a_licious( + def search( self, query: str, page: int = 1, @@ -284,6 +287,9 @@ def search_a_licious( ) -> Optional[JSONType]: """Search products using the new high-performance Search-a-licious endpoint. + WARNING: This endpoint is currently in alpha version and subjected to + breaking changes. + :param query: the search query :param page: requested page (starts at 1), defaults to 1 :param page_size: number of items per page, defaults to 24 @@ -291,8 +297,8 @@ def search_a_licious( :param kwargs: additional filters :return: the search results (standardized with 'products' list) """ - # The new high-performance search endpoint - url = "https://search.openfoodfacts.org/search" + # Use the dynamic URL based on environment + url = f"{self.base_searchalicious_url}/search" params = { "q": query, From 34b5102c9f6fa295e80e9ea779d6f465743f1200 Mon Sep 17 00:00:00 2001 From: saivats Date: Wed, 28 Jan 2026 07:33:44 +0530 Subject: [PATCH 3/4] fix: URLBuilder for search --- openfoodfacts/api.py | 164 ++++++++++++++++---------------- openfoodfacts/utils/__init__.py | 8 ++ 2 files changed, 89 insertions(+), 83 deletions(-) diff --git a/openfoodfacts/api.py b/openfoodfacts/api.py index 68b079e7..19172fdc 100644 --- a/openfoodfacts/api.py +++ b/openfoodfacts/api.py @@ -184,98 +184,96 @@ def get_products( resp = cast(JSONType, resp) return resp - class ProductResource: def __init__(self, api_config: APIConfig): - self.api_config = api_config - self.base_url = URLBuilder.country( - self.api_config.flavor, - environment=api_config.environment, - country_code=self.api_config.country.name, - ) - # Handle environment for Search-a-licious (Requested by review) - domain = "net" if self.api_config.environment == Environment.net else "org" - self.base_searchalicious_url = f"https://search.openfoodfacts.{domain}" + self.api_config = api_config + self.base_url = URLBuilder.country( + self.api_config.flavor, + environment=api_config.environment, + country_code=self.api_config.country.name, + ) + # Handle environment for Search-a-licious using the new URLBuilder helper + self.base_search_url = URLBuilder.search(self.api_config.environment) def get( - self, - code: str, - fields: Optional[List[str]] = None, - raise_if_invalid: bool = False, - ) -> Optional[JSONType]: - """Return a product. - - If the product does not exist, None is returned. - - :param code: barcode of the product - :param fields: a list of fields to return. If None, all fields are - returned. - :param raise_if_invalid: if True, a ValueError is raised if the - barcode is invalid, defaults to False. - :return: the API response - """ - if len(code) == 0: - raise ValueError("code must be a non-empty string") - - fields = fields or [] - url = f"{self.base_url}/api/{self.api_config.version.value}/product/{code}" - - if fields: - # requests escape comma in URLs, as expected, but openfoodfacts - # server does not recognize escaped commas. - # See - # https://github.com/openfoodfacts/openfoodfacts-server/issues/1607 - url += "?fields={}".format(",".join(fields)) - - resp = send_get_request( - url=url, api_config=self.api_config, return_none_on_404=True - ) + self, + code: str, + fields: Optional[List[str]] = None, + raise_if_invalid: bool = False, + ) -> Optional[JSONType]: + """Return a product. + + If the product does not exist, None is returned. + + :param code: barcode of the product + :param fields: a list of fields to return. If None, all fields are + returned. + :param raise_if_invalid: if True, a ValueError is raised if the + barcode is invalid, defaults to False. + :return: the API response + """ + if len(code) == 0: + raise ValueError("code must be a non-empty string") + + fields = fields or [] + url = f"{self.base_url}/api/{self.api_config.version.value}/product/{code}" + + if fields: + # requests escape comma in URLs, as expected, but openfoodfacts + # server does not recognize escaped commas. + # See + # https://github.com/openfoodfacts/openfoodfacts-server/issues/1607 + url += "?fields={}".format(",".join(fields)) + + resp = send_get_request( + url=url, api_config=self.api_config, return_none_on_404=True + ) - if resp is None: - # product not found - return None + if resp is None: + # product not found + return None - if resp["status"] == 0: - # invalid barcode - if raise_if_invalid: - raise ValueError(f"invalid barcode: {code}") - return None + if resp["status"] == 0: + # invalid barcode + if raise_if_invalid: + raise ValueError(f"invalid barcode: {code}") + return None - return resp["product"] if resp is not None else None + return resp["product"] if resp is not None else None def text_search( - self, - query: str, - page: int = 1, - page_size: int = 20, - sort_by: Optional[str] = None, - ): - """Search products using a textual query. - - :param query: the search query - :param page: requested page (starts at 1), defaults to 1 - :param page_size: number of items per page, defaults to 20 - :param sort_by: result sorting key, defaults to None (no sorting) - :return: the search results - """ - # We force usage of v2 of API - params = { - "search_terms": query, - "page": page, - "page_size": page_size, - "sort_by": sort_by, - "json": "1", - } + self, + query: str, + page: int = 1, + page_size: int = 20, + sort_by: Optional[str] = None, + ): + """Search products using a textual query. + + :param query: the search query + :param page: requested page (starts at 1), defaults to 1 + :param page_size: number of items per page, defaults to 20 + :param sort_by: result sorting key, defaults to None (no sorting) + :return: the search results + """ + # We force usage of v2 of API + params = { + "search_terms": query, + "page": page, + "page_size": page_size, + "sort_by": sort_by, + "json": "1", + } - if sort_by is not None: - params["sort_by"] = sort_by + if sort_by is not None: + params["sort_by"] = sort_by - return send_get_request( - url=f"{self.base_url}/cgi/search.pl", - api_config=self.api_config, - params=params, - auth=get_http_auth(self.api_config.environment), - ) + return send_get_request( + url=f"{self.base_url}/cgi/search.pl", + api_config=self.api_config, + params=params, + auth=get_http_auth(self.api_config.environment), + ) def search( self, @@ -297,8 +295,8 @@ def search( :param kwargs: additional filters :return: the search results (standardized with 'products' list) """ - # Use the dynamic URL based on environment - url = f"{self.base_searchalicious_url}/search" + # Use the dynamic URL from init + url = f"{self.base_search_url}/search" params = { "q": query, diff --git a/openfoodfacts/utils/__init__.py b/openfoodfacts/utils/__init__.py index ee210ec8..46f6916a 100644 --- a/openfoodfacts/utils/__init__.py +++ b/openfoodfacts/utils/__init__.py @@ -105,6 +105,14 @@ def robotoff(environment: Environment) -> str: base_domain=Flavor.off.get_base_domain(), ) + @staticmethod + def search(environment: Environment) -> str: + """ + Return the URL of the search service (Search-a-licious). + """ + domain = "net" if environment == Environment.net else "org" + return f"https://search.openfoodfacts.{domain}" + @staticmethod def static(flavor: Flavor, environment: Environment) -> str: return URLBuilder._get_url( From ef78cb76a0541827d32643ffe31be6842a920c86 Mon Sep 17 00:00:00 2001 From: saivats Date: Thu, 29 Jan 2026 10:17:14 +0530 Subject: [PATCH 4/4] refactor: rename to search, use URLBuilder, and add alpha warning --- openfoodfacts/utils/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openfoodfacts/utils/__init__.py b/openfoodfacts/utils/__init__.py index 46f6916a..415036f2 100644 --- a/openfoodfacts/utils/__init__.py +++ b/openfoodfacts/utils/__init__.py @@ -110,8 +110,11 @@ def search(environment: Environment) -> str: """ Return the URL of the search service (Search-a-licious). """ - domain = "net" if environment == Environment.net else "org" - return f"https://search.openfoodfacts.{domain}" + return URLBuilder._get_url( + prefix="search", + tld=environment.value, + base_domain=Flavor.off.get_base_domain(), + ) @staticmethod def static(flavor: Flavor, environment: Environment) -> str: