Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 108 additions & 59 deletions openfoodfacts/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,96 +184,145 @@ 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,
)
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,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems you indented by mistake these methods. Can you fix it?
You should run black after each edit, to make sure the formatting stays consistent.

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")
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
)

fields = fields or []
url = f"{self.base_url}/api/{self.api_config.version.value}/product/{code}"
if resp is None:
# product not found
return None

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))
if resp["status"] == 0:
# invalid barcode
if raise_if_invalid:
raise ValueError(f"invalid barcode: {code}")
return None

resp = send_get_request(
url=url, api_config=self.api_config, return_none_on_404=True
)
return resp["product"] if resp is not None else None

if resp is None:
# product not found
return 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",
}

if resp["status"] == 0:
# invalid barcode
if raise_if_invalid:
raise ValueError(f"invalid barcode: {code}")
return None
if sort_by is not None:
params["sort_by"] = sort_by

return resp["product"] if resp is not None else None
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 text_search(
def search(
self,
query: str,
page: int = 1,
page_size: int = 20,
page_size: int = 24,
sort_by: Optional[str] = None,
):
"""Search products using a textual query.
**kwargs: Any,
) -> Optional[JSONType]:
"""Search products using the new high-performance Search-a-licious endpoint.
Comment thread
saivats marked this conversation as resolved.

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 20
:param sort_by: result sorting key, defaults to None (no sorting)
:return: the search results
: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)
"""
# We force usage of v2 of API
# Use the dynamic URL from init
url = f"{self.base_search_url}/search"

params = {
"search_terms": query,
"q": query,
"page": page,
"page_size": page_size,
"sort_by": sort_by,
"json": "1",
}

if sort_by is not None:
if sort_by:
params["sort_by"] = sort_by

return send_get_request(
url=f"{self.base_url}/cgi/search.pl",
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"):
Expand Down
11 changes: 11 additions & 0 deletions openfoodfacts/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,17 @@ 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).
"""
return URLBuilder._get_url(
prefix="search",
tld=environment.value,
base_domain=Flavor.off.get_base_domain(),
)

Comment thread
raphael0202 marked this conversation as resolved.
@staticmethod
def static(flavor: Flavor, environment: Environment) -> str:
return URLBuilder._get_url(
Expand Down