python-xbox 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. python_xbox-0.1.0.dist-info/METADATA +217 -0
  2. python_xbox-0.1.0.dist-info/RECORD +64 -0
  3. python_xbox-0.1.0.dist-info/WHEEL +4 -0
  4. python_xbox-0.1.0.dist-info/entry_points.txt +6 -0
  5. python_xbox-0.1.0.dist-info/licenses/LICENSE +20 -0
  6. pythonxbox/__init__.py +4 -0
  7. pythonxbox/api/__init__.py +0 -0
  8. pythonxbox/api/client.py +166 -0
  9. pythonxbox/api/language.py +76 -0
  10. pythonxbox/api/provider/__init__.py +0 -0
  11. pythonxbox/api/provider/account/__init__.py +73 -0
  12. pythonxbox/api/provider/account/models.py +11 -0
  13. pythonxbox/api/provider/achievements/__init__.py +164 -0
  14. pythonxbox/api/provider/achievements/models.py +133 -0
  15. pythonxbox/api/provider/baseprovider.py +22 -0
  16. pythonxbox/api/provider/catalog/__init__.py +86 -0
  17. pythonxbox/api/provider/catalog/const.py +15 -0
  18. pythonxbox/api/provider/catalog/models.py +428 -0
  19. pythonxbox/api/provider/cqs/__init__.py +85 -0
  20. pythonxbox/api/provider/cqs/models.py +59 -0
  21. pythonxbox/api/provider/gameclips/__init__.py +167 -0
  22. pythonxbox/api/provider/gameclips/models.py +58 -0
  23. pythonxbox/api/provider/lists/__init__.py +71 -0
  24. pythonxbox/api/provider/lists/models.py +33 -0
  25. pythonxbox/api/provider/mediahub/__init__.py +64 -0
  26. pythonxbox/api/provider/mediahub/models.py +82 -0
  27. pythonxbox/api/provider/message/__init__.py +135 -0
  28. pythonxbox/api/provider/message/models.py +96 -0
  29. pythonxbox/api/provider/people/__init__.py +193 -0
  30. pythonxbox/api/provider/people/models.py +252 -0
  31. pythonxbox/api/provider/presence/__init__.py +110 -0
  32. pythonxbox/api/provider/presence/models.py +53 -0
  33. pythonxbox/api/provider/profile/__init__.py +140 -0
  34. pythonxbox/api/provider/profile/models.py +47 -0
  35. pythonxbox/api/provider/ratelimitedprovider.py +79 -0
  36. pythonxbox/api/provider/screenshots/__init__.py +167 -0
  37. pythonxbox/api/provider/screenshots/models.py +56 -0
  38. pythonxbox/api/provider/smartglass/__init__.py +402 -0
  39. pythonxbox/api/provider/smartglass/models.py +186 -0
  40. pythonxbox/api/provider/titlehub/__init__.py +143 -0
  41. pythonxbox/api/provider/titlehub/models.py +106 -0
  42. pythonxbox/api/provider/usersearch/__init__.py +29 -0
  43. pythonxbox/api/provider/usersearch/models.py +17 -0
  44. pythonxbox/api/provider/userstats/__init__.py +164 -0
  45. pythonxbox/api/provider/userstats/models.py +44 -0
  46. pythonxbox/authentication/__init__.py +0 -0
  47. pythonxbox/authentication/manager.py +161 -0
  48. pythonxbox/authentication/models.py +162 -0
  49. pythonxbox/authentication/xal.py +348 -0
  50. pythonxbox/common/__init__.py +0 -0
  51. pythonxbox/common/exceptions.py +59 -0
  52. pythonxbox/common/filetimes.py +81 -0
  53. pythonxbox/common/models.py +34 -0
  54. pythonxbox/common/ratelimits/__init__.py +268 -0
  55. pythonxbox/common/ratelimits/models.py +23 -0
  56. pythonxbox/common/request_signer.py +190 -0
  57. pythonxbox/common/signed_session.py +60 -0
  58. pythonxbox/py.typed +0 -0
  59. pythonxbox/scripts/__init__.py +15 -0
  60. pythonxbox/scripts/authenticate.py +159 -0
  61. pythonxbox/scripts/change_gamertag.py +111 -0
  62. pythonxbox/scripts/friends.py +80 -0
  63. pythonxbox/scripts/search.py +43 -0
  64. pythonxbox/scripts/xal.py +113 -0
@@ -0,0 +1,140 @@
1
+ """
2
+ Profile
3
+
4
+ Get Userprofiles by XUID or Gamertag
5
+ """
6
+
7
+ from pythonxbox.api.provider.profile.models import ProfileResponse, ProfileSettings
8
+ from pythonxbox.api.provider.ratelimitedprovider import RateLimitedProvider
9
+
10
+
11
+ class ProfileProvider(RateLimitedProvider):
12
+ PROFILE_URL = "https://profile.xboxlive.com"
13
+ HEADERS_PROFILE = {"x-xbl-contract-version": "3"}
14
+ SEPARATOR = ","
15
+
16
+ RATE_LIMITS = {"burst": 10, "sustain": 30}
17
+
18
+ async def get_profiles(self, xuid_list: list[str], **kwargs) -> ProfileResponse:
19
+ """
20
+ Get profile info for list of xuids
21
+
22
+ Args:
23
+ xuid_list (list): List of xuids
24
+
25
+ Returns:
26
+ :class:`ProfileResponse`: Profile Response
27
+ """
28
+ post_data = {
29
+ "settings": [
30
+ ProfileSettings.GAME_DISPLAY_NAME,
31
+ ProfileSettings.APP_DISPLAY_NAME,
32
+ ProfileSettings.APP_DISPLAYPIC_RAW,
33
+ ProfileSettings.GAMERSCORE,
34
+ ProfileSettings.GAMERTAG,
35
+ ProfileSettings.GAME_DISPLAYPIC_RAW,
36
+ ProfileSettings.ACCOUNT_TIER,
37
+ ProfileSettings.TENURE_LEVEL,
38
+ ProfileSettings.XBOX_ONE_REP,
39
+ ProfileSettings.PREFERRED_COLOR,
40
+ ProfileSettings.LOCATION,
41
+ ProfileSettings.BIOGRAPHY,
42
+ ProfileSettings.WATERMARKS,
43
+ ProfileSettings.REAL_NAME,
44
+ ],
45
+ "userIds": xuid_list,
46
+ }
47
+ url = self.PROFILE_URL + "/users/batch/profile/settings"
48
+ resp = await self.client.session.post(
49
+ url,
50
+ json=post_data,
51
+ headers=self.HEADERS_PROFILE,
52
+ rate_limits=self.rate_limit_read,
53
+ **kwargs,
54
+ )
55
+ resp.raise_for_status()
56
+ return ProfileResponse(**resp.json())
57
+
58
+ async def get_profile_by_xuid(self, target_xuid: str, **kwargs) -> ProfileResponse:
59
+ """
60
+ Get Userprofile by xuid
61
+
62
+ Args:
63
+ target_xuid: XUID to get profile for
64
+
65
+ Returns:
66
+ :class:`ProfileResponse`: Profile Response
67
+ """
68
+ url = self.PROFILE_URL + f"/users/xuid({target_xuid})/profile/settings"
69
+ params = {
70
+ "settings": self.SEPARATOR.join(
71
+ [
72
+ ProfileSettings.GAMERTAG,
73
+ ProfileSettings.MODERN_GAMERTAG,
74
+ ProfileSettings.MODERN_GAMERTAG_SUFFIX,
75
+ ProfileSettings.UNIQUE_MODERN_GAMERTAG,
76
+ ProfileSettings.REAL_NAME_OVERRIDE,
77
+ ProfileSettings.BIOGRAPHY,
78
+ ProfileSettings.LOCATION,
79
+ ProfileSettings.GAMERSCORE,
80
+ ProfileSettings.GAME_DISPLAYPIC_RAW,
81
+ ProfileSettings.TENURE_LEVEL,
82
+ ProfileSettings.ACCOUNT_TIER,
83
+ ProfileSettings.XBOX_ONE_REP,
84
+ ProfileSettings.PREFERRED_COLOR,
85
+ ProfileSettings.WATERMARKS,
86
+ ProfileSettings.IS_QUARANTINED,
87
+ ]
88
+ )
89
+ }
90
+ resp = await self.client.session.get(
91
+ url,
92
+ params=params,
93
+ headers=self.HEADERS_PROFILE,
94
+ rate_limits=self.rate_limit_read,
95
+ **kwargs,
96
+ )
97
+ resp.raise_for_status()
98
+ return ProfileResponse(**resp.json())
99
+
100
+ async def get_profile_by_gamertag(self, gamertag: str, **kwargs) -> ProfileResponse:
101
+ """
102
+ Get Userprofile by gamertag
103
+
104
+ Args:
105
+ gamertag: Gamertag to get profile for
106
+
107
+ Returns:
108
+ :class:`ProfileResponse`: Profile Response
109
+ """
110
+ url = self.PROFILE_URL + f"/users/gt({gamertag})/profile/settings"
111
+ params = {
112
+ "settings": self.SEPARATOR.join(
113
+ [
114
+ ProfileSettings.GAMERTAG,
115
+ ProfileSettings.MODERN_GAMERTAG,
116
+ ProfileSettings.MODERN_GAMERTAG_SUFFIX,
117
+ ProfileSettings.UNIQUE_MODERN_GAMERTAG,
118
+ ProfileSettings.REAL_NAME_OVERRIDE,
119
+ ProfileSettings.BIOGRAPHY,
120
+ ProfileSettings.LOCATION,
121
+ ProfileSettings.GAMERSCORE,
122
+ ProfileSettings.GAME_DISPLAYPIC_RAW,
123
+ ProfileSettings.TENURE_LEVEL,
124
+ ProfileSettings.ACCOUNT_TIER,
125
+ ProfileSettings.XBOX_ONE_REP,
126
+ ProfileSettings.PREFERRED_COLOR,
127
+ ProfileSettings.WATERMARKS,
128
+ ProfileSettings.IS_QUARANTINED,
129
+ ]
130
+ )
131
+ }
132
+ resp = await self.client.session.get(
133
+ url,
134
+ params=params,
135
+ headers=self.HEADERS_PROFILE,
136
+ rate_limits=self.rate_limit_read,
137
+ **kwargs,
138
+ )
139
+ resp.raise_for_status()
140
+ return ProfileResponse(**resp.json())
@@ -0,0 +1,47 @@
1
+ from enum import Enum
2
+
3
+ from pythonxbox.common.models import CamelCaseModel
4
+
5
+
6
+ class ProfileSettings(str, Enum):
7
+ """
8
+ Profile settings, used as parameter for Profile API
9
+ """
10
+
11
+ GAME_DISPLAY_NAME = "GameDisplayName"
12
+ APP_DISPLAY_NAME = "AppDisplayName"
13
+ APP_DISPLAYPIC_RAW = "AppDisplayPicRaw"
14
+ GAME_DISPLAYPIC_RAW = "GameDisplayPicRaw"
15
+ PUBLIC_GAMERPIC = "PublicGamerpic"
16
+ SHOW_USER_AS_AVATAR = "ShowUserAsAvatar"
17
+ GAMERSCORE = "Gamerscore"
18
+ GAMERTAG = "Gamertag"
19
+ MODERN_GAMERTAG = "ModernGamertag"
20
+ MODERN_GAMERTAG_SUFFIX = "ModernGamertagSuffix"
21
+ UNIQUE_MODERN_GAMERTAG = "UniqueModernGamertag"
22
+ ACCOUNT_TIER = "AccountTier"
23
+ TENURE_LEVEL = "TenureLevel"
24
+ XBOX_ONE_REP = "XboxOneRep"
25
+ PREFERRED_COLOR = "PreferredColor"
26
+ LOCATION = "Location"
27
+ BIOGRAPHY = "Bio"
28
+ WATERMARKS = "Watermarks"
29
+ REAL_NAME = "RealName"
30
+ REAL_NAME_OVERRIDE = "RealNameOverride"
31
+ IS_QUARANTINED = "IsQuarantined"
32
+
33
+
34
+ class Setting(CamelCaseModel):
35
+ id: str
36
+ value: str
37
+
38
+
39
+ class ProfileUser(CamelCaseModel):
40
+ id: str
41
+ host_id: str
42
+ settings: list[Setting]
43
+ is_sponsored_user: bool
44
+
45
+
46
+ class ProfileResponse(CamelCaseModel):
47
+ profile_users: list[ProfileUser]
@@ -0,0 +1,79 @@
1
+ """
2
+ RateLimitedProvider
3
+
4
+ Subclassed by providers with rate limit support
5
+ """
6
+
7
+ from typing import TYPE_CHECKING
8
+ from pythonxbox.api.provider.baseprovider import BaseProvider
9
+ from pythonxbox.common.exceptions import XboxException
10
+ from pythonxbox.common.ratelimits import CombinedRateLimit
11
+ from pythonxbox.common.ratelimits.models import LimitType, ParsedRateLimit, TimePeriod
12
+
13
+ if TYPE_CHECKING:
14
+ from pythonxbox.api.client import XboxLiveClient
15
+
16
+
17
+ class RateLimitedProvider(BaseProvider):
18
+ # dict -> Dict (typing.dict) https://stackoverflow.com/a/63460173
19
+ RATE_LIMITS: dict[str, int | dict[str, int]]
20
+
21
+ def __init__(self, client: "XboxLiveClient") -> None:
22
+ """
23
+ Initialize Baseclass
24
+
25
+ Args:
26
+ client (:class:`XboxLiveClient`): Instance of XboxLiveClient
27
+ """
28
+ super().__init__(client)
29
+
30
+ # Check that RATE_LIMITS set defined in the child class
31
+ if hasattr(self, "RATE_LIMITS"):
32
+ # Note: we cannot check (type(self.RATE_LIMITS) == dict) as the type hints have already defined it as such
33
+ if "burst" and "sustain" in self.RATE_LIMITS:
34
+ # We have the required keys, attempt to parse.
35
+ # (type-checking for the values is performed in __parse_rate_limit_key)
36
+ self.__handle_rate_limit_setup()
37
+ else:
38
+ raise XboxException(
39
+ "RATE_LIMITS object missing required keys 'burst', 'sustain'"
40
+ )
41
+ else:
42
+ raise XboxException(
43
+ "RateLimitedProvider as parent class but RATE_LIMITS not set!"
44
+ )
45
+
46
+ def __handle_rate_limit_setup(self) -> None:
47
+ # Retrieve burst and sustain from the dict
48
+ burst_key = self.RATE_LIMITS["burst"]
49
+ sustain_key = self.RATE_LIMITS["sustain"]
50
+
51
+ # Parse the rate limit dict values
52
+ burst_rate_limits = self.__parse_rate_limit_key(burst_key, TimePeriod.BURST)
53
+ sustain_rate_limits = self.__parse_rate_limit_key(
54
+ sustain_key, TimePeriod.SUSTAIN
55
+ )
56
+
57
+ # Instanciate CombinedRateLimits for read and write respectively
58
+ self.rate_limit_read = CombinedRateLimit(
59
+ burst_rate_limits, sustain_rate_limits, type=LimitType.READ
60
+ )
61
+ self.rate_limit_write = CombinedRateLimit(
62
+ burst_rate_limits, sustain_rate_limits, type=LimitType.WRITE
63
+ )
64
+
65
+ def __parse_rate_limit_key(
66
+ self, key: int | dict[str, int], period: TimePeriod
67
+ ) -> ParsedRateLimit:
68
+ if isinstance(key, int) and not isinstance(key, bool):
69
+ # bool is a subclass of int, hence the explicit check
70
+ return ParsedRateLimit(read=key, write=key, period=period)
71
+ elif isinstance(key, dict):
72
+ # TODO: schema here?
73
+ # Since the key-value pairs match we can just pass the dict to the model
74
+ return ParsedRateLimit(**key, period=period)
75
+ # return ParsedRateLimit(read=key["read"], write=key["write"])
76
+ else:
77
+ raise XboxException(
78
+ "RATE_LIMITS value types not recognised. Must be one of 'int, 'dict'."
79
+ )
@@ -0,0 +1,167 @@
1
+ """
2
+ Screenshots - Get screenshot info
3
+ """
4
+
5
+ from pythonxbox.api.provider.baseprovider import BaseProvider
6
+ from pythonxbox.api.provider.screenshots.models import ScreenshotResponse
7
+
8
+
9
+ class ScreenshotsProvider(BaseProvider):
10
+ SCREENSHOTS_METADATA_URL = "https://screenshotsmetadata.xboxlive.com"
11
+ HEADERS_SCREENSHOTS_METADATA = {"x-xbl-contract-version": "5"}
12
+
13
+ async def get_recent_community_screenshots_by_title_id(
14
+ self, title_id: str, **kwargs
15
+ ) -> ScreenshotResponse:
16
+ """
17
+ Get recent community screenshots by Title Id
18
+
19
+ Args:
20
+ title_id: Title Id to get screenshots for
21
+
22
+ Returns:
23
+ :class:`ScreenshotResponse`: Screenshot Response
24
+ """
25
+ url = self.SCREENSHOTS_METADATA_URL + f"/public/titles/{title_id}/screenshots"
26
+ params = {"qualifier": "created"}
27
+ resp = await self.client.session.get(
28
+ url, params=params, headers=self.HEADERS_SCREENSHOTS_METADATA, **kwargs
29
+ )
30
+ resp.raise_for_status()
31
+ return ScreenshotResponse(**resp.json())
32
+
33
+ async def get_recent_own_screenshots(
34
+ self, title_id: str = None, skip_items: int = 0, max_items: int = 25, **kwargs
35
+ ) -> ScreenshotResponse:
36
+ """
37
+ Get own recent screenshots, optionally filter for title Id
38
+
39
+ Args:
40
+ title_id: Title ID to filter
41
+ skip_items: Item count to skip
42
+ max_items: Maximum item count to load
43
+
44
+ Returns:
45
+ :class:`ScreenshotResponse`: Screenshot Response
46
+ """
47
+ url = self.SCREENSHOTS_METADATA_URL + "/users/me"
48
+ if title_id:
49
+ url += f"/titles/{title_id}"
50
+ url += "/screenshots"
51
+
52
+ params = {"skipItems": skip_items, "maxItems": max_items}
53
+ resp = await self.client.session.get(
54
+ url, params=params, headers=self.HEADERS_SCREENSHOTS_METADATA, **kwargs
55
+ )
56
+ resp.raise_for_status()
57
+ return ScreenshotResponse(**resp.json())
58
+
59
+ async def get_recent_screenshots_by_xuid(
60
+ self,
61
+ xuid: str,
62
+ title_id: str = None,
63
+ skip_items: int = 0,
64
+ max_items: int = 25,
65
+ **kwargs,
66
+ ) -> ScreenshotResponse:
67
+ """
68
+ Get recent screenshots by XUID, optionally filter for title Id
69
+
70
+ Args:
71
+ xuid: XUID of user to get screenshots from
72
+ title_id: Optional title id filter
73
+ skip_items: Item count to skip
74
+ max_items: Maximum item count to load
75
+
76
+ Returns:
77
+ :class:`ScreenshotResponse`: Screenshot Response
78
+ """
79
+ url = self.SCREENSHOTS_METADATA_URL + f"/users/xuid({xuid})"
80
+ if title_id:
81
+ url += f"/titles/{title_id}"
82
+ url += "/screenshots"
83
+
84
+ params = {"skipItems": skip_items, "maxItems": max_items}
85
+ resp = await self.client.session.get(
86
+ url, params=params, headers=self.HEADERS_SCREENSHOTS_METADATA, **kwargs
87
+ )
88
+ resp.raise_for_status()
89
+ return ScreenshotResponse(**resp.json())
90
+
91
+ async def get_saved_community_screenshots_by_title_id(
92
+ self, title_id: str, **kwargs
93
+ ) -> ScreenshotResponse:
94
+ """
95
+ Get saved community screenshots by Title Id
96
+
97
+ Args:
98
+ title_id: Title Id to get screenshots for
99
+
100
+ Returns:
101
+ :class:`ScreenshotResponse`: Screenshot Response
102
+ """
103
+ url = f"{self.SCREENSHOTS_METADATA_URL}/public/titles/{title_id}/screenshots/saved"
104
+ params = {"qualifier": "created"}
105
+ resp = await self.client.session.get(
106
+ url, params=params, headers=self.HEADERS_SCREENSHOTS_METADATA, **kwargs
107
+ )
108
+ resp.raise_for_status()
109
+ return ScreenshotResponse(**resp.json())
110
+
111
+ async def get_saved_own_screenshots(
112
+ self, title_id: str = None, skip_items: int = 0, max_items: int = 25, **kwargs
113
+ ) -> ScreenshotResponse:
114
+ """
115
+ Get own saved screenshots, optionally filter for title Id an
116
+
117
+ Args:
118
+ title_id: Optional Title ID to filter
119
+ skip_items: Item count to skip
120
+ max_items: Maximum item count to load
121
+
122
+ Returns:
123
+ :class:`ScreenshotResponse`: Screenshot Response
124
+ """
125
+ url = self.SCREENSHOTS_METADATA_URL + "/users/me"
126
+ if title_id:
127
+ url += f"/titles/{title_id}"
128
+ url += "/screenshots/saved"
129
+
130
+ params = {"skipItems": skip_items, "maxItems": max_items}
131
+ resp = await self.client.session.get(
132
+ url, params=params, headers=self.HEADERS_SCREENSHOTS_METADATA, **kwargs
133
+ )
134
+ resp.raise_for_status()
135
+ return ScreenshotResponse(**resp.json())
136
+
137
+ async def get_saved_screenshots_by_xuid(
138
+ self,
139
+ xuid: str,
140
+ title_id: str = None,
141
+ skip_items: int = 0,
142
+ max_items: int = 25,
143
+ **kwargs,
144
+ ) -> ScreenshotResponse:
145
+ """
146
+ Get saved screenshots by XUID, optionally filter for title Id
147
+
148
+ Args:
149
+ xuid: XUID of user to get screenshots from
150
+ title_id: Optional title id filter
151
+ skip_items: Item count to skip
152
+ max_items: Maximum item count to load
153
+
154
+ Returns:
155
+ :class:`ScreenshotResponse`: Screenshot Response
156
+ """
157
+ url = self.SCREENSHOTS_METADATA_URL + f"/users/xuid({xuid})"
158
+ if title_id:
159
+ url += f"/titles/{title_id}"
160
+ url += "/screenshots/saved"
161
+
162
+ params = {"skipItems": skip_items, "maxItems": max_items}
163
+ resp = await self.client.session.get(
164
+ url, params=params, headers=self.HEADERS_SCREENSHOTS_METADATA, **kwargs
165
+ )
166
+ resp.raise_for_status()
167
+ return ScreenshotResponse(**resp.json())
@@ -0,0 +1,56 @@
1
+ from datetime import datetime
2
+ from typing import Any
3
+
4
+ from pythonxbox.common.models import CamelCaseModel
5
+
6
+
7
+ class Thumbnail(CamelCaseModel):
8
+ uri: str
9
+ file_size: int
10
+ thumbnail_type: int
11
+
12
+
13
+ class ScreenshotUri(CamelCaseModel):
14
+ uri: str
15
+ file_size: int
16
+ uri_type: int
17
+ expiration: datetime
18
+
19
+
20
+ class Screenshot(CamelCaseModel):
21
+ screenshot_id: str
22
+ resolution_height: int
23
+ resolution_width: int
24
+ state: int
25
+ date_published: datetime
26
+ date_taken: datetime
27
+ last_modified: datetime
28
+ user_caption: str
29
+ type: int
30
+ scid: str
31
+ title_id: int
32
+ rating: float
33
+ rating_count: int
34
+ views: int
35
+ title_data: str
36
+ system_properties: str
37
+ saved_by_user: bool
38
+ achievement_id: str
39
+ greatest_moment_id: Any = None
40
+ thumbnails: list[Thumbnail]
41
+ screenshot_uris: list[ScreenshotUri]
42
+ xuid: str
43
+ screenshot_name: str
44
+ title_name: str
45
+ screenshot_locale: str
46
+ screenshot_content_attributes: int
47
+ device_type: str
48
+
49
+
50
+ class PagingInfo(CamelCaseModel):
51
+ continuation_token: str | None = None
52
+
53
+
54
+ class ScreenshotResponse(CamelCaseModel):
55
+ screenshots: list[Screenshot]
56
+ paging_info: PagingInfo