Skip to content

Commit

Permalink
feat: Add region/country support for client and user models (#95)
Browse files Browse the repository at this point in the history
* feat: Add region/country support for client and user models

* chore: improve NPID processing to extract region and country names using pycountry

* chore: replace regions properties with methods

* test: add integration tests for client and user region retrieval

* docs: update README to use get_region() method for client and user region retrieval

* test: update test_client__get_region.json to simplify account device data

* chore: add newline at end of file in test_client.py and test_user.py

* refactor: Change get_region to return Country obj from Pycountries

- Change get_region to return Country obj from Pycountries
- Update test based on the change above
- Update dependencies
- Re-run unit tests to generate new cassettes
- Fix an issue in test_search__get_game_content_id

---------

Co-authored-by: Yoshikage Kira <[email protected]>
  • Loading branch information
Dev-R and isFakeAccount authored Jan 12, 2025
1 parent 99247e4 commit 78d43b3
Show file tree
Hide file tree
Showing 90 changed files with 4,609 additions and 3,199 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ Retrieve User Information, Trophies, Game and Store data from the PlayStation Ne
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/license/MIT)

<!-- Pytest Coverage Comment:Begin -->
\n<!-- Pytest Coverage Comment:End -->
<!-- Pytest Coverage Comment:Begin -->
\n<!-- Pytest Coverage Comment:End -->

## How to install

Expand Down Expand Up @@ -60,6 +60,7 @@ psnawp = PSNAWP("<64 character npsso code>")
client = psnawp.me()
print(f"Online ID: {client.online_id}")
print(f"Account ID: {client.account_id}")
print(f"Region: {client.get_region()}")
print(f"Profile: {client.get_profile_legacy()} \n")

# Your Registered Devices
Expand Down Expand Up @@ -111,6 +112,7 @@ for title in titles_with_stats:
example_user_1 = psnawp.user(online_id="VaultTec-Co") # Get a PSN player by their Online ID
print(f"User 1 Online ID: {example_user_1.online_id}")
print(f"User 1 Account ID: {example_user_1.account_id}")
print(f"User 1 Region: {example_user_1.get_region()}")

print(example_user_1.profile())
print(example_user_1.prev_online_id)
Expand Down
23 changes: 12 additions & 11 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,31 +24,32 @@ classifiers = [
packages = [{ include = "psnawp_api", from = "src" }]

[tool.poetry.dependencies]
python = "^3.9"
attrs = "23.2.0"
python = "^3.10"
attrs = "24.2.0"
requests = "^2.32.3"
typing-extensions = "^4.12.2"
requests-ratelimiter = "^0.7.0"
pyright = "^1.1.370"
pycountry = "^24.6.1"

[tool.poetry.group.typing.dependencies]
mypy = "^1.10.1"
mypy = "^1.14.1"
pyright = "^1.1.391"
types-requests = "^2.31.0.6"

[tool.poetry.group.linting.dependencies]
ruff = "^0.5.1"
pre-commit = "^3.7.1"
ruff = "^0.9.1"
pre-commit = "^4.0.0"

[tool.poetry.group.docs.dependencies]
sphinx = "^7.3.7"
furo = "^2024.04.27"
myst-parser = { extras = ["linkify"], version = "^3.0.1" }
sphinx = "^8.1.3"
furo = "^2024.8.6"
myst-parser = { extras = ["linkify"], version = "^4.0.0" }

[tool.poetry.group.tests.dependencies]
pytest = "^8.2.2"
python-dotenv = "^1.0.1"
vcrpy = "^6.0.1"
pytest-cov = "^5.0.0"
vcrpy = "^7.0.0"
pytest-cov = "^6.0.0"
pytest-vcr = "^1.0.2"
jsonschema = "^4.23.0"

Expand Down
2 changes: 1 addition & 1 deletion pytest.xml
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<?xml version="1.0" encoding="utf-8"?><testsuites><testsuite name="pytest" errors="0" failures="0" skipped="0" tests="81" time="4.000" timestamp="2024-07-14T04:11:00.916633" hostname="fv-az1493-266"><testcase classname="tests.integration_tests.integration_test_psnawp_api.core.test_authenticator" name="test_authenticator__authentication" time="0.113" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.core.test_authenticator" name="test_authenticator__access_token_from_refresh_token" time="0.049" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.core.test_authenticator" name="test_authenticator__incorrect_npsso" time="0.016" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_client" name="test_client__online_id" time="0.031" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_client" name="test_client__account_id" time="0.012" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_client" name="test_client__get_profile_legacy" time="0.024" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_client" name="test_client__account_devices" time="0.012" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_client" name="test_client__get_friends" time="0.027" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_client" name="test_client__friend_requests" time="0.018" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_client" name="test_client__get_groups" time="0.012" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_client" name="test_client__available_to_play" time="0.012" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_client" name="test_client__blocked_list" time="0.012" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_client" name="test_client__trophy_summary" time="0.012" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_client" name="test_client__trophy_titles" time="0.012" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_client" name="test_client__trophy_titles_for_title" time="0.013" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_client" name="test_client__trophies" time="0.013" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_client" name="test_client__trophies_with_progress" time="0.019" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_client" name="test_client__trophy_groups_summary" time="0.012" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_client" name="test_client__trophy_groups_summary_with_progress" time="0.019" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_client" name="test_client__title_stats" time="0.012" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_client" name="test_client__repr_and_str" time="0.019" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_game_title" name="test_game_title__np_communication_id" time="0.012" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_game_title" name="test_game_title__wrong_title_id" time="0.012" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_game_title" name="test_game_title__get_title_details" time="0.019" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_game_title" name="test_game_title__trophies" time="0.020" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_game_title" name="test_game_title__trophy_groups_summary" time="0.019" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_game_title" name="test_game_title__trophies_game_not_owned_by_user" time="0.014" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_game_title" name="test_game_title__trophy_groups_summary_game_not_owned_by_user" time="0.013" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_game_title" name="test_game_title__trophies_invalid_np_communication_id" time="0.012" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_game_title" name="test_game_title__trophy_groups_summary_invalid_np_communication_id" time="0.013" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_group" name="test_group__group_incorrect_args_None" time="0.006" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_group" name="test_group__group_with_wrong_id" time="0.014" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_group" name="test_group__group_with_users" time="0.946" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_group" name="test_group__group_with_id" time="0.021" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_group" name="test_group__get_group_information" time="0.023" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_group" name="test_group__repr_and_str" time="0.013" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_group" name="test_group__change_name_dm" time="0.021" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_group" name="test_group__dming_blocked_user" time="0.364" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_group" name="test_group__change_name" time="0.020" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_group" name="test_group__kick_member" time="0.021" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_group" name="test_group__kick_member_not_found" time="0.021" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_group" name="test_group__invite_members" time="0.021" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_group" name="test_group__invite_members_blocked" time="0.021" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_group" name="test_group__leave_group" time="0.021" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_search" name="test_search__universal_search" time="0.016" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_search" name="test_search__get_game_content_id" time="0.017" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_search" name="test_search__get_addon_content_id" time="0.031" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__user" time="0.013" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__user_account_id" time="0.013" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__user_no_argument" time="0.006" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__user_wrong_acc_id" time="0.013" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__prev_online_id" time="0.013" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__user_not_found" time="0.013" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__user_acct_id_not_found" time="0.013" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__get_profile" time="0.021" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__get_presence" time="0.012" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__get_presence_forbidden" time="0.011" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__friendship" time="0.015" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__accept_friend_request" time="0.012" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__remove_friend" time="0.012" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__get_friends" time="0.012" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__get_friends_forbidden" time="0.011" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__is_blocked" time="0.011" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__trophy_summary" time="0.021" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__trophy_summary_forbidden" time="0.012" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__trophy_titles" time="0.032" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__trophy_titles_forbidden" time="0.011" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__trophy_titles_pagination_test" time="0.291" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__trophy_titles_for_title" time="0.022" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__trophy_titles_for_title_forbidden" time="0.012" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__trophies" time="0.040" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__trophies_with_progress_forbidden" time="0.018" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__trophies_pagination_test" time="0.041" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__trophy_groups_summary" time="0.040" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__trophy_groups_summary_forbidden" time="0.018" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__title_stats" time="0.011" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__title_stats_with_limit" time="0.224" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__title_stats_with_jump" time="0.043" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.models.test_user" name="test_user__repr_and_str" time="0.006" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.utils.test_utils_misc" name="test_play_duration_to_timedelta_valid_inputs" time="0.001" /><testcase classname="tests.integration_tests.integration_test_psnawp_api.utils.test_utils_misc" name="test_play_duration_to_timedelta_invalid_inputs" time="0.001" /></testsuite></testsuites>
<?xml version="1.0" encoding="utf-8"?><testsuites><testsuite name="pytest" errors="0" failures="0" skipped="0" tests="0" time="0.130" timestamp="2025-01-11T22:28:23.349043-05:00" hostname="pop-os" /></testsuites>
23 changes: 22 additions & 1 deletion src/psnawp_api/models/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
TrophyWithProgressIterator,
)
from psnawp_api.models.user import User
from psnawp_api.utils import API_PATH, BASE_PATH
from psnawp_api.utils import API_PATH, BASE_PATH, extract_region_from_npid

if TYPE_CHECKING:
from pycountry.db import Country

from psnawp_api.core import Authenticator
from psnawp_api.models.trophies import PlatformType, TrophyGroupsSummary, TrophyGroupSummary, TrophyGroupSummaryWithProgress

Expand Down Expand Up @@ -68,6 +70,25 @@ def account_id(self) -> str:
account_id: str = response["accountId"]
return account_id

def get_region(self) -> Optional[Country]:
"""Gets the region of the client logged in the api.
:returns: Returns Country object from Pycountry for logged in user or None if not found.
.. code-block:: Python
client = psnawp.me()
print(client.get_region())
.. note::
See https://github.com/pycountry/pycountry for more info on Country object.
"""
response = self.get_profile_legacy()
npid = response.get("profile", {}).get("npId", "")
return extract_region_from_npid(npid)

def get_profile_legacy(self) -> dict[str, Any]:
"""Gets the profile info from legacy api endpoint. Useful for legacy console (PS3, PS4) presence.
Expand Down
5 changes: 1 addition & 4 deletions src/psnawp_api/models/trophies/trophy_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,7 @@ def __init__(

def __str__(self) -> str:
return (
f"TrophyGroupsSummary(Title: {self.trophy_title_name}, "
f"Defined: {self.defined_trophies}, "
f"Earned: {self.earned_trophies}, "
f"Progress: {self.progress})"
f"TrophyGroupsSummary(Title: {self.trophy_title_name}, Defined: {self.defined_trophies}, Earned: {self.earned_trophies}, Progress: {self.progress})"
)

def __repr__(self) -> str:
Expand Down
52 changes: 51 additions & 1 deletion src/psnawp_api/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
TrophyTitleIterator,
TrophyWithProgressIterator,
)
from psnawp_api.utils import API_PATH, BASE_PATH
from psnawp_api.utils import API_PATH, BASE_PATH, extract_region_from_npid

if TYPE_CHECKING:
from pycountry.db import Country

from psnawp_api.core import Authenticator
from psnawp_api.models.trophies import PlatformType, TrophyGroupsSummary, TrophyGroupSummary, TrophyGroupSummaryWithProgress

Expand Down Expand Up @@ -111,6 +113,54 @@ def profile(self) -> dict[str, Any]:
response: dict[str, Any] = self.authenticator.get(url=f"{BASE_PATH['profile_uri']}{API_PATH['profiles'].format(account_id=self.account_id)}").json()
return response

def get_region(self) -> Optional[Country]:
"""Gets the region of the user.
:returns: Returns Country object from Pycountry of the User or None if not found.
.. code-block:: Python
user_example = psnawp.user(online_id="VaultTec_Trading")
print(user_example.get_region())
.. note::
See https://github.com/pycountry/pycountry for more info on Country object.
"""
response = self.get_profile_legacy()
npid = response.get("profile", {}).get("npId", "")
return extract_region_from_npid(npid)

def get_profile_legacy(self) -> dict[str, Any]:
"""Gets the user profile info from legacy api endpoint. Useful for legacy console (PS3, PS4) presence.
:returns: A dict containing info similar to what is shown below:
.. literalinclude:: examples/client/get_profile_legacy.json
:language: json
.. code-block:: Python
user_example = psnawp.user(online_id="VaultTec_Trading")
print(user_example.get_profile_legacy())
"""
params = {
"fields": "npId,onlineId,accountId,avatarUrls,plus,aboutMe,languagesUsed,trophySummary(@default,level,progress,earnedTrophies),"
"isOfficiallyVerified,personalDetail(@default,profilePictureUrls),personalDetailSharing,personalDetailSharingRequestMessageFlag,"
"primaryOnlineStatus,presences(@default,@titleInfo,platform,lastOnlineDate,hasBroadcastData),requestMessageFlag,blocking,friendRelation,"
"following,consoleAvailability"
}

response: dict[str, Any] = self.authenticator.get(
url=f"{BASE_PATH['legacy_profile_uri']}{API_PATH['legacy_profile'].format(online_id=self.online_id)}",
params=params,
).json()

return response

def get_presence(self) -> dict[str, Any]:
"""Gets the presences of a user. If the profile is private
Expand Down
4 changes: 2 additions & 2 deletions src/psnawp_api/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from psnawp_api.utils.endpoints import API_PATH, BASE_PATH
from psnawp_api.utils.misc import iso_format_to_datetime
from psnawp_api.utils.misc import extract_region_from_npid, iso_format_to_datetime

__all__ = ["BASE_PATH", "API_PATH", "iso_format_to_datetime"]
__all__ = ["BASE_PATH", "API_PATH", "iso_format_to_datetime", "extract_region_from_npid"]
Loading

0 comments on commit 78d43b3

Please sign in to comment.