Skip to content

Add reviews client for getting app reviews #57

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 23, 2025
Merged
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
3 changes: 3 additions & 0 deletions asconnect/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from asconnect.app_info_client import AppInfoClient
from asconnect.beta_review_client import BetaReviewClient
from asconnect.build_client import BuildClient
from asconnect.reviews_client import ReviewsClient
from asconnect.screenshot_client import ScreenshotClient
from asconnect.users_client import UsersClient
from asconnect.version_client import VersionClient
Expand All @@ -28,6 +29,7 @@ class Client:
app_info: AppInfoClient
beta_review: BetaReviewClient
build: BuildClient
reviews: ReviewsClient
screenshots: ScreenshotClient
users: UsersClient
version: VersionClient
Expand Down Expand Up @@ -61,6 +63,7 @@ def __init__(
self.app_info = AppInfoClient(http_client=self.http_client, log=self.log)
self.beta_review = BetaReviewClient(http_client=self.http_client, log=self.log)
self.build = BuildClient(http_client=self.http_client, log=self.log)
self.reviews = ReviewsClient(http_client=self.http_client, log=self.log)
self.screenshots = ScreenshotClient(http_client=self.http_client, log=self.log)
self.users = UsersClient(http_client=self.http_client, log=self.log)
self.version = VersionClient(http_client=self.http_client, log=self.log)
1 change: 1 addition & 0 deletions asconnect/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
from asconnect.models.builds import *
from asconnect.models.idfa import *
from asconnect.models.localization import *
from asconnect.models.reviews import *
from asconnect.models.screenshots import *
from asconnect.models.users import *
11 changes: 6 additions & 5 deletions asconnect/models/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,7 @@ class ContentRightsDeclaration(enum.Enum):
USES_THIRD_PARTY_CONTENT = "USES_THIRD_PARTY_CONTENT"


@deserialize.key("bundle_id", "bundleId")
@deserialize.key("primary_locale", "primaryLocale")
@deserialize.key("available_in_new_territories", "availableInNewTerritories")
@deserialize.key("content_rights_declaration", "contentRightsDeclaration")
@deserialize.key("is_or_ever_was_made_for_kids", "isOrEverWasMadeForKids")
@deserialize.auto_snake()
class AppAttributes(BaseAttributes):
"""Represents app attributes."""

Expand All @@ -29,6 +25,11 @@ class AppAttributes(BaseAttributes):
available_in_new_territories: bool | None
content_rights_declaration: ContentRightsDeclaration | None
is_or_ever_was_made_for_kids: bool
subscription_status_url: str | None
subscription_status_url_version: str | None
subscription_status_url_for_sandbox: str | None
subscription_status_url_version_for_sandbox: str | None
streamlined_purchasing_enabled: bool | None


@deserialize.key("identifier", "id")
Expand Down
30 changes: 30 additions & 0 deletions asconnect/models/reviews.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""User Models for the API"""

import datetime

import deserialize

from asconnect.models.common import BaseAttributes, Links, Relationship, Resource


@deserialize.auto_snake()
@deserialize.parser("created_date", datetime.datetime.fromisoformat)
class CustomerReviewAttributes(BaseAttributes):
"""Represents build attributes."""

body: str
created_date: datetime.datetime
rating: int
reviewer_nickname: str
title: str
territory: str


@deserialize.key("identifier", "id")
class CustomerReview(Resource):
"""Represents a user."""

identifier: str
attributes: CustomerReviewAttributes
relationships: dict[str, Relationship] | None
links: Links
75 changes: 75 additions & 0 deletions asconnect/reviews_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Wrapper around the Apple App Store Connect APIs."""

# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

import logging
from typing import Iterator

from asconnect.httpclient import HttpClient

from asconnect.models import CustomerReview
from asconnect.sorting import CustomerReviewSort
from asconnect.utilities import update_query_parameters


class ReviewsClient:
"""Wrapper class around the ASC API."""

log: logging.Logger
http_client: HttpClient

def __init__(
self,
*,
http_client: HttpClient,
log: logging.Logger,
) -> None:
"""Construct a new client object.

:param http_client: The API HTTP client
:param log: Any base logger to be used (one will be created if not supplied)
"""

self.http_client = http_client
self.log = log.getChild("reviews")

def get_reviews(
self,
app_id: str,
sort_order: CustomerReviewSort | None = None,
territory_filter: list[str] | None = None,
published_response: bool | None = None,
) -> Iterator[CustomerReview]:
"""Get customer reviews for an app.

:param app_id: The app ID to get reviews for
:param sort_order: The order to sort the reviews in. Defaults to None.
:param territory_filter: The territory to filter the reviews by. Defaults to None.
:param published_response: If set to True, only reviews with a published response will be
returned. If set to False, only reviews without a published
response will be returned. Defaults to None which returns all.

:yields: An iterator of reviews
"""

self.log.info("Getting users...")

url = self.http_client.generate_url(f"apps/{app_id}/customerReviews")

query_parameters = {}

if sort_order:
query_parameters["sort"] = sort_order.value

if territory_filter:
if isinstance(territory_filter, str):
territory_filter = [territory_filter]
query_parameters["filter[territory]"] = ",".join(territory_filter)

if published_response is not None:
query_parameters["exists[publishedResponse]"] = str(published_response).lower()

url = update_query_parameters(url, query_parameters)

yield from self.http_client.get(url=url, data_type=list[CustomerReview])
9 changes: 9 additions & 0 deletions asconnect/sorting.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,12 @@ class BuildsSort(enum.Enum):
UPLOADED_DATE_REVERSED = "-uploadedDate"
VERSION = "version"
VERSION_REVERSED = "-version"


class CustomerReviewSort(enum.Enum):
"""Orders that customer reviews can be sorted."""

RATING_ASC = "rating"
RATING_DESC = "-rating"
CREATED_DATE_ASC = "createdDate"
CREATED_DATE_DESC = "-createdDate"
19 changes: 19 additions & 0 deletions tests/test_reviews.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

"""Tests for the package."""

import os
import sys

sys.path.insert(0, os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..")))
import asconnect # pylint: disable=wrong-import-order


def test_get_reviews(client: asconnect.Client, app_id: str) -> None:
"""Test that we can wait for a build."""

counter = 0
for review in client.reviews.get_reviews(app_id=app_id):
counter += 1
print(review.attributes.body)