From 02c3c925f80cb4b4e22749f2c875d1d9a187efe7 Mon Sep 17 00:00:00 2001 From: ikaroskun Date: Wed, 20 Jul 2022 20:37:14 +0800 Subject: [PATCH 1/5] feat(catalog): :sparkles: add api for get user catalog --- .../api/instagram_business/resource/user.py | 36 ++++++++++++++----- pyfacebook/models/ig_business_models.py | 26 ++++++++++++++ 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/pyfacebook/api/instagram_business/resource/user.py b/pyfacebook/api/instagram_business/resource/user.py index 1c371dad..b99db33d 100644 --- a/pyfacebook/api/instagram_business/resource/user.py +++ b/pyfacebook/api/instagram_business/resource/user.py @@ -16,6 +16,7 @@ IgBusMentionedCommentResponse, IgBusMentionedMediaResponse, IgBusHashtagsResponse, + IgBusCatalogsResponse, ) from pyfacebook.utils.params_utils import enf_comma_separated @@ -217,7 +218,7 @@ def get_media( until: Optional[str] = None, count: Optional[int] = 10, limit: Optional[int] = 10, - return_json=False, + return_json: bool = False, ) -> Union[IgBusMediaResponse, dict]: """ Get user's media. This only can return max 10k medias. @@ -258,7 +259,7 @@ def get_live_media( until: Optional[str] = None, count: Optional[int] = 10, limit: Optional[int] = 10, - return_json=False, + return_json: bool = False, ) -> Union[IgBusMediaResponse, dict]: """ Represents a collection of live video IG Media on an IG User. @@ -297,7 +298,7 @@ def get_mentioned_comment( self, comment_id: str, fields: Optional[Union[str, list, tuple]] = None, - return_json=False, + return_json: bool = False, ) -> Union[IgBusMentionedCommentResponse, dict]: """ Get data on an IG Comment in which user has been @mentioned by another Instagram user @@ -328,7 +329,7 @@ def get_mentioned_media( self, media_id: str, fields: Optional[Union[str, list, tuple]] = None, - return_json=False, + return_json: bool = False, ) -> Union[IgBusMentionedMediaResponse, dict]: """ Get data on an IG Media in which user has been @mentioned in a caption by another Instagram user. @@ -357,7 +358,7 @@ def get_mentioned_media( def get_hashtag_search( self, q: str, - return_json=False, + return_json: bool = False, ) -> Union[IgBusHashtagsResponse, dict]: """ Get ID for hashtag. @@ -385,7 +386,7 @@ def get_recently_searched_hashtags( fields: Optional[Union[str, list, tuple]] = None, count: Optional[int] = 25, limit: Optional[int] = 25, - return_json=False, + return_json: bool = False, ) -> Union[IgBusHashtagsResponse, dict]: """ Get the IG Hashtags that user has searched for within the last 7 days. @@ -419,7 +420,7 @@ def get_stories( fields: Optional[Union[str, list, tuple]] = None, count: Optional[int] = 10, limit: Optional[int] = 10, - return_json=False, + return_json: bool = False, ) -> Union[IgBusMediaResponse, dict]: """ Get list of story IG Media objects on user. @@ -454,7 +455,7 @@ def get_tagged_media( fields: Optional[Union[str, list, tuple]] = None, count: Optional[int] = 25, limit: Optional[int] = 25, - return_json=False, + return_json: bool = False, ) -> Union[IgBusMediaResponse, dict]: """ Get list of Media objects in which user has been tagged by another Instagram user. @@ -483,3 +484,22 @@ def get_tagged_media( return data else: return IgBusMediaResponse.new_from_json_dict(data) + + def get_available_catalogs( + self, return_json: bool = False + ) -> Union[IgBusCatalogsResponse, dict]: + """ + Get the product catalog in an IG User's Instagram Shop. + + :param: return_json + :return: catalog data + """ + + data = self.client.get_connection( + object_id=self.client.instagram_business_id, + connection="available_catalogs", + ) + if return_json: + return data + else: + return IgBusCatalogsResponse.new_from_json_dict(data) diff --git a/pyfacebook/models/ig_business_models.py b/pyfacebook/models/ig_business_models.py index 666e4db2..8193b10d 100644 --- a/pyfacebook/models/ig_business_models.py +++ b/pyfacebook/models/ig_business_models.py @@ -27,6 +27,7 @@ class IgBusUser(BaseModel): media_count: Optional[int] = field() name: Optional[int] = field() profile_picture_url: Optional[str] = field() + shopping_product_tag_eligibility: Optional[bool] = field() username: Optional[str] = field(repr=True) website: Optional[str] = field() @@ -314,3 +315,28 @@ class IgBusHashtagsResponse(BaseModel): data: List[IgBusHashtag] = field(repr=True) paging: Optional[Paging] = field(repr=True) + + +@dataclass +class IgBusCatalog(BaseModel): + """ + A class representing the catalog. + + Refer: https://developers.facebook.com/docs/instagram-api/reference/ig-user/available_catalogs + """ + + catalog_id: Optional[str] = field(repr=True) + catalog_name: Optional[str] = field(repr=True) + shop_name: Optional[str] = field(repr=True) + product_count: Optional[int] = field(repr=True) + + +@dataclass +class IgBusCatalogsResponse(BaseModel): + """ + A class representing the catalog list response. + + Refer: https://developers.facebook.com/docs/instagram-api/reference/ig-user/available_catalogs + """ + + data: List[IgBusCatalog] = field(repr=True) From 93eb8245283461b04519cd7aecf4c5c39be558ff Mon Sep 17 00:00:00 2001 From: ikaroskun Date: Thu, 21 Jul 2022 10:43:09 +0800 Subject: [PATCH 2/5] feat(product): :sparkles: add api for get catalog product search --- .../api/instagram_business/resource/user.py | 39 ++++++++++++++++++- pyfacebook/models/ig_business_models.py | 36 +++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/pyfacebook/api/instagram_business/resource/user.py b/pyfacebook/api/instagram_business/resource/user.py index b99db33d..0cbe0a0a 100644 --- a/pyfacebook/api/instagram_business/resource/user.py +++ b/pyfacebook/api/instagram_business/resource/user.py @@ -17,6 +17,7 @@ IgBusMentionedMediaResponse, IgBusHashtagsResponse, IgBusCatalogsResponse, + IgBusProductsResponse, ) from pyfacebook.utils.params_utils import enf_comma_separated @@ -491,7 +492,8 @@ def get_available_catalogs( """ Get the product catalog in an IG User's Instagram Shop. - :param: return_json + :param: return_json: Set to false will return a dataclass for IgBusCatalogsResponse. + Or return json data. Default is false. :return: catalog data """ @@ -503,3 +505,38 @@ def get_available_catalogs( return data else: return IgBusCatalogsResponse.new_from_json_dict(data) + + def get_catalog_product_search( + self, + catalog_id: str, + q: Optional[str] = None, + count: Optional[int] = 10, + limit: int = 10, + return_json: bool = False, + ) -> Union[IgBusProductsResponse, dict]: + """ + Get a collection of products that match a given search string within the targeted IG User's Instagram Shop catalog. + + :param catalog_id: ID of catalog to search. + :param q: A string to search for in each product's name or SKU number (SKU numbers can be added in the Content ID column in the catalog management interface). + If no string is specified, all tag-eligible products will be returned. + :param count: The count will retrieve objects. Default is None will get all data. + :param limit: Each request retrieve objects count. + For most connections should no more than 100. Default is None will use api default limit. + :param return_json: Set to false will return a dataclass for IgBusProductsResponse. + Or return json data. Default is false. + :return: Catalog products data. + """ + + data = self.client.get_full_connections( + object_id=self.client.instagram_business_id, + connection="catalog_product_search", + catalog_id=catalog_id, + q=q, + count=count, + limit=limit, + ) + if return_json: + return data + else: + return IgBusProductsResponse.new_from_json_dict(data) diff --git a/pyfacebook/models/ig_business_models.py b/pyfacebook/models/ig_business_models.py index 8193b10d..811e812e 100644 --- a/pyfacebook/models/ig_business_models.py +++ b/pyfacebook/models/ig_business_models.py @@ -340,3 +340,39 @@ class IgBusCatalogsResponse(BaseModel): """ data: List[IgBusCatalog] = field(repr=True) + + +@dataclass +class IgBusProductVariant(BaseModel): + product_id: Optional[int] = field(repr=True) + variant_name: Optional[str] = field(repr=True) + + +@dataclass +class IgBusProduct(BaseModel): + """ + A class representing the catalog product. + + Refer: https://developers.facebook.com/docs/instagram-api/reference/ig-user/catalog_product_search + """ + + product_id: Optional[int] = field(repr=True) + merchant_id: Optional[int] = field(repr=True) + product_name: Optional[str] = field(repr=True) + image_url: Optional[str] = field() + retailer_id: Optional[str] = field() + review_status: Optional[str] = field() + is_checkout_flow: Optional[bool] = field() + product_variants: Optional[List[IgBusProductVariant]] = field() + + +@dataclass +class IgBusProductsResponse(BaseModel): + """ + A class representing the catalog product list response. + + Refer: https://developers.facebook.com/docs/instagram-api/reference/ig-user/catalog_product_search + """ + + data: List[IgBusProduct] = field(repr=True) + paging: Optional[Paging] = field(repr=True) From d71ac428750c84e7bafa1e9f837992591ba4c25f Mon Sep 17 00:00:00 2001 From: ikaroskun Date: Thu, 21 Jul 2022 11:00:52 +0800 Subject: [PATCH 3/5] feat(product tag): :sparkles: add api for get media product tags --- .../api/instagram_business/resource/media.py | 23 ++++++++++++- pyfacebook/models/ig_business_models.py | 32 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/pyfacebook/api/instagram_business/resource/media.py b/pyfacebook/api/instagram_business/resource/media.py index a6b5c5f9..484ccc06 100644 --- a/pyfacebook/api/instagram_business/resource/media.py +++ b/pyfacebook/api/instagram_business/resource/media.py @@ -10,6 +10,7 @@ IgBusCommentResponse, IgBusMediaChildren, IgBusInsightsResponse, + IgBusProductTagsResponse, ) from pyfacebook.utils.params_utils import enf_comma_separated @@ -154,7 +155,7 @@ def get_insights( :param media_id: ID for the media. :param metric: A comma-separated list of Metrics you want returned. You can also pass this with an id list, tuple. - :param return_json: Set to false will return a dataclass for IgBusPublishLimitResponse. + :param return_json: Set to false will return a dataclass for IgBusInsightsResponse. Or return json data. Default is false. :return: Media insights response information. """ @@ -168,3 +169,23 @@ def get_insights( return data else: return IgBusInsightsResponse.new_from_json_dict(data) + + def get_product_tags( + self, media_id: str, return_json: bool = False + ) -> Union[IgBusProductTagsResponse, dict]: + """ + Get a collection of product tags on an IG Media. + :param media_id: ID for the media. + :param return_json: Set to false will return a dataclass for IgBusProductTagsResponse. + Or return json data. Default is false. + :return: Media product tags + """ + + data = self.client.get_connection( + object_id=media_id, + connection="product_tags", + ) + if return_json: + return data + else: + return IgBusProductTagsResponse.new_from_json_dict(data) diff --git a/pyfacebook/models/ig_business_models.py b/pyfacebook/models/ig_business_models.py index 811e812e..163d6724 100644 --- a/pyfacebook/models/ig_business_models.py +++ b/pyfacebook/models/ig_business_models.py @@ -376,3 +376,35 @@ class IgBusProductsResponse(BaseModel): data: List[IgBusProduct] = field(repr=True) paging: Optional[Paging] = field(repr=True) + + +@dataclass +class IgBusProductTag(BaseModel): + """ + A class representing the product tag. + + Refer: https://developers.facebook.com/docs/instagram-api/reference/ig-media/product_tags#ig-media-product-tags + """ + + product_id: Optional[int] = field(repr=True) + merchant_id: Optional[int] = field(repr=True) + name: Optional[str] = field(repr=True) + price_string: Optional[str] = field() + image_url: Optional[str] = field() + review_status: Optional[str] = field() + is_checkout: Optional[bool] = field() + stripped_price_string: Optional[str] = field() + string_sale_price_string: Optional[str] = field() + x: Optional[float] = field() + y: Optional[float] = field() + + +@dataclass +class IgBusProductTagsResponse(BaseModel): + """ + A class representing the product tag list response. + + Refer: https://developers.facebook.com/docs/instagram-api/reference/ig-media/product_tags#ig-media-product-tags + """ + + data: List[IgBusProductTag] = field(repr=True) From 2a9f68773a6081fd437ee74f5f1541f72a91dbec Mon Sep 17 00:00:00 2001 From: ikaroskun Date: Thu, 21 Jul 2022 11:50:57 +0800 Subject: [PATCH 4/5] feat(product appeal): :sparkles: Add api for get product appeal status --- .../api/instagram_business/resource/user.py | 25 +++++++++++++++++++ pyfacebook/models/ig_business_models.py | 24 ++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/pyfacebook/api/instagram_business/resource/user.py b/pyfacebook/api/instagram_business/resource/user.py index 0cbe0a0a..a5d2f791 100644 --- a/pyfacebook/api/instagram_business/resource/user.py +++ b/pyfacebook/api/instagram_business/resource/user.py @@ -18,6 +18,7 @@ IgBusHashtagsResponse, IgBusCatalogsResponse, IgBusProductsResponse, + IgBusProductAppealsResponse, ) from pyfacebook.utils.params_utils import enf_comma_separated @@ -540,3 +541,27 @@ def get_catalog_product_search( return data else: return IgBusProductsResponse.new_from_json_dict(data) + + def get_product_appeal( + self, + product_id: str, + return_json: bool = False, + ) -> Union[IgBusProductAppealsResponse, dict]: + """ + Get appeal status of a rejected product. + + :param product_id: Product ID. + :param return_json: Set to false will return a dataclass for IgBusProductAppealsResponse. + Or return json data. Default is false. + :return: Product appeals data. + """ + + data = self.client.get_connection( + object_id=self.client.instagram_business_id, + connection="product_appeal", + product_id=product_id, + ) + if return_json: + return data + else: + return IgBusProductAppealsResponse.new_from_json_dict(data) diff --git a/pyfacebook/models/ig_business_models.py b/pyfacebook/models/ig_business_models.py index 163d6724..8dfd9ed0 100644 --- a/pyfacebook/models/ig_business_models.py +++ b/pyfacebook/models/ig_business_models.py @@ -408,3 +408,27 @@ class IgBusProductTagsResponse(BaseModel): """ data: List[IgBusProductTag] = field(repr=True) + + +@dataclass +class IgBusProductAppeal(BaseModel): + """ + A class representing the product appeal. + + Refer: https://developers.facebook.com/docs/instagram-api/reference/ig-user/product_appeal + """ + + eligible_for_appeal: Optional[bool] = field(repr=True) + product_id: Optional[int] = field(repr=True) + review_status: Optional[str] = field(repr=True) + + +@dataclass +class IgBusProductAppealsResponse(BaseModel): + """ + A class representing the product appeal list + + Refer: https://developers.facebook.com/docs/instagram-api/reference/ig-user/product_appeal#response-2 + """ + + data: List[IgBusProductAppeal] = field(repr=True) From 1176e8afea88d16f57b4cadbaa9cb3f5390a8034 Mon Sep 17 00:00:00 2001 From: ikaroskun Date: Thu, 21 Jul 2022 14:18:33 +0800 Subject: [PATCH 5/5] test(tests): :white_check_mark: update tests for product tagging --- .../apidata/medias/product_tags.json | 1 + .../apidata/users/available_catalogs.json | 1 + .../apidata/users/catalog_product_search.json | 1 + .../apidata/users/product_appeal.json | 1 + tests/instagram_business/test_media.py | 19 ++++++ tests/instagram_business/test_user.py | 60 +++++++++++++++++++ 6 files changed, 83 insertions(+) create mode 100644 testdata/instagram/apidata/medias/product_tags.json create mode 100644 testdata/instagram/apidata/users/available_catalogs.json create mode 100644 testdata/instagram/apidata/users/catalog_product_search.json create mode 100644 testdata/instagram/apidata/users/product_appeal.json diff --git a/testdata/instagram/apidata/medias/product_tags.json b/testdata/instagram/apidata/medias/product_tags.json new file mode 100644 index 00000000..25948c36 --- /dev/null +++ b/testdata/instagram/apidata/medias/product_tags.json @@ -0,0 +1 @@ +{"data":[{"product_id":3231775643511089,"merchant_id":90010177253934,"name":"Gummy Bears","price_string":"$3.50","image_url":"https://scont...","review_status":"approved","is_checkout":true,"stripped_price_string":"$3.50","stripped_sale_price_string":"$3","x":0.5,"y":0.80000001192093}]} \ No newline at end of file diff --git a/testdata/instagram/apidata/users/available_catalogs.json b/testdata/instagram/apidata/users/available_catalogs.json new file mode 100644 index 00000000..8994d098 --- /dev/null +++ b/testdata/instagram/apidata/users/available_catalogs.json @@ -0,0 +1 @@ +{"data":[{"catalog_id":"960179311066902","catalog_name":"Jay's Favorite Snacks","shop_name":"Jay's Bespoke","product_count":11}]} \ No newline at end of file diff --git a/testdata/instagram/apidata/users/catalog_product_search.json b/testdata/instagram/apidata/users/catalog_product_search.json new file mode 100644 index 00000000..7f511f48 --- /dev/null +++ b/testdata/instagram/apidata/users/catalog_product_search.json @@ -0,0 +1 @@ +{"data":[{"product_id":3231775643511089,"merchant_id":90010177253934,"product_name":"Gummy Wombats","image_url":"https://scont...","retailer_id":"oh59p9vzei","review_status":"approved","is_checkout_flow":true,"product_variants":[{"product_id":5209223099160494},{"product_id":7478222675582505,"variant_name":"Green Gummy Wombats"}]}]} \ No newline at end of file diff --git a/testdata/instagram/apidata/users/product_appeal.json b/testdata/instagram/apidata/users/product_appeal.json new file mode 100644 index 00000000..8f81f6d3 --- /dev/null +++ b/testdata/instagram/apidata/users/product_appeal.json @@ -0,0 +1 @@ +{"data":[{"product_id":4029274203846188,"review_status":"approved","eligible_for_appeal":false}]} \ No newline at end of file diff --git a/tests/instagram_business/test_media.py b/tests/instagram_business/test_media.py index bc0142e8..dc4fe558 100644 --- a/tests/instagram_business/test_media.py +++ b/tests/instagram_business/test_media.py @@ -122,3 +122,22 @@ def test_get_insights(helpers, api): return_json=True, ) assert insights_json["data"][0]["name"] == "impressions" + + +def test_get_product_tags(helpers, api): + media_id = "90010778325754" + with responses.RequestsMock() as m: + m.add( + method=responses.GET, + url=f"https://graph.facebook.com/{api.version}/{media_id}/product_tags", + json=helpers.load_json( + "testdata/instagram/apidata/medias/product_tags.json" + ), + ) + + tags = api.media.get_product_tags(media_id=media_id) + assert len(tags.data) == 1 + assert tags.data[0].product_id == 3231775643511089 + + tags_json = api.media.get_product_tags(media_id=media_id, return_json=True) + assert tags_json["data"][0]["review_status"] == "approved" diff --git a/tests/instagram_business/test_user.py b/tests/instagram_business/test_user.py index e42cf8ba..e53e9562 100644 --- a/tests/instagram_business/test_user.py +++ b/tests/instagram_business/test_user.py @@ -275,3 +275,63 @@ def test_get_live_media(helpers, api): medias_json = api.user.get_live_media(return_json=True) assert len(medias_json["data"]) == 1 + + +def test_get_available_catalogs(helpers, api): + with responses.RequestsMock() as m: + m.add( + method=responses.GET, + url=f"https://graph.facebook.com/{api.version}/{api.instagram_business_id}/available_catalogs", + json=helpers.load_json( + "testdata/instagram/apidata/users/available_catalogs.json" + ), + ) + + catalogs = api.user.get_available_catalogs() + assert len(catalogs.data) == 1 + assert catalogs.data[0].catalog_id == "960179311066902" + + catalogs_json = api.user.get_available_catalogs(return_json=True) + assert len(catalogs_json["data"]) == 1 + + +def test_get_catalog_product_search(helpers, api): + catalog_id = "960179311066902" + with responses.RequestsMock() as m: + m.add( + method=responses.GET, + url=f"https://graph.facebook.com/{api.version}/{api.instagram_business_id}/catalog_product_search", + json=helpers.load_json( + "testdata/instagram/apidata/users/catalog_product_search.json" + ), + ) + + products = api.user.get_catalog_product_search(catalog_id=catalog_id) + assert len(products.data) == 1 + assert products.data[0].product_id == 3231775643511089 + + products_json = api.user.get_catalog_product_search( + catalog_id=catalog_id, return_json=True + ) + assert len(products_json["data"]) == 1 + + +def test_get_product_appeal(helpers, api): + product_id = 4029274203846188 + with responses.RequestsMock() as m: + m.add( + method=responses.GET, + url=f"https://graph.facebook.com/{api.version}/{api.instagram_business_id}/product_appeal", + json=helpers.load_json( + "testdata/instagram/apidata/users/product_appeal.json" + ), + ) + + appeals = api.user.get_product_appeal(product_id=product_id) + assert len(appeals.data) == 1 + assert appeals.data[0].product_id == product_id + + appeals_json = api.user.get_product_appeal( + product_id=product_id, return_json=True + ) + assert len(appeals_json["data"]) == 1