Add account family + storage services (#250)

This commit is contained in:
Quentame 2020-04-04 00:48:32 +02:00 committed by GitHub
parent 91ac1d956e
commit e3bdcea15a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 523 additions and 45 deletions

View file

@ -13,34 +13,68 @@ class AccountService(object):
self.session = session self.session = session
self.params = params self.params = params
self._service_root = service_root self._service_root = service_root
self._devices = [] self._devices = []
self._family = []
self._storage = None
self._acc_endpoint = "%s/setup/web/device" % self._service_root self._acc_endpoint = "%s/setup/web" % self._service_root
self._account_devices_url = "%s/getDevices" % self._acc_endpoint self._acc_devices_url = "%s/device/getDevices" % self._acc_endpoint
self._acc_family_details_url = "%s/family/getFamilyDetails" % self._acc_endpoint
req = self.session.get(self._account_devices_url, params=self.params) self._acc_family_member_photo_url = (
self.response = req.json() "%s/family/getMemberPhoto" % self._acc_endpoint
)
for device_info in self.response["devices"]: self._acc_storage_url = "https://setup.icloud.com/setup/ws/1/storageUsageInfo"
# device_id = device_info['udid']
# self._devices[device_id] = AccountDevice(device_info)
self._devices.append(AccountDevice(device_info))
@property @property
def devices(self): def devices(self):
"""Gets the account devices.""" """Returns current paired devices."""
if not self._devices:
req = self.session.get(self._acc_devices_url, params=self.params)
response = req.json()
for device_info in response["devices"]:
self._devices.append(AccountDevice(device_info))
return self._devices return self._devices
@property
def family(self):
"""Returns family members."""
if not self._family:
req = self.session.get(self._acc_family_details_url, params=self.params)
response = req.json()
for member_info in response["familyMembers"]:
self._family.append(
FamilyMember(
member_info,
self.session,
self.params,
self._acc_family_member_photo_url,
)
)
return self._family
@property
def storage(self):
"""Returns storage infos."""
if not self._storage:
req = self.session.get(self._acc_storage_url, params=self.params)
response = req.json()
self._storage = AccountStorage(response)
return self._storage
@six.python_2_unicode_compatible @six.python_2_unicode_compatible
class AccountDevice(dict): class AccountDevice(dict):
"""Account device.""" """Account device."""
def __getattr__(self, name): def __getattr__(self, key):
try: return self[underscore_to_camelcase(key)]
return self[underscore_to_camelcase(name)]
except KeyError:
raise AttributeError(name)
def __str__(self): def __str__(self):
return u"{display_name}: {name}".format( return u"{display_name}: {name}".format(
@ -55,3 +89,251 @@ class AccountDevice(dict):
else six.text_type(self).encode("utf8", "replace") else six.text_type(self).encode("utf8", "replace")
) )
) )
class FamilyMember(object):
"""A family member."""
def __init__(self, member_info, session, params, acc_family_member_photo_url):
self._attrs = member_info
self._session = session
self._params = params
self._acc_family_member_photo_url = acc_family_member_photo_url
@property
def last_name(self):
"""Gets the last name."""
return self._attrs.get("lastName")
@property
def dsid(self):
"""Gets the dsid."""
return self._attrs.get("dsid")
@property
def original_invitation_email(self):
"""Gets the original invitation."""
return self._attrs.get("originalInvitationEmail")
@property
def full_name(self):
"""Gets the full name."""
return self._attrs.get("fullName")
@property
def age_classification(self):
"""Gets the age classification."""
return self._attrs.get("ageClassification")
@property
def apple_id_for_purchases(self):
"""Gets the apple id for purchases."""
return self._attrs.get("appleIdForPurchases")
@property
def apple_id(self):
"""Gets the apple id."""
return self._attrs.get("appleId")
@property
def family_id(self):
"""Gets the family id."""
return self._attrs.get("familyId")
@property
def first_name(self):
"""Gets the first name."""
return self._attrs.get("firstName")
@property
def has_parental_privileges(self):
"""Has parental privileges."""
return self._attrs.get("hasParentalPrivileges")
@property
def has_screen_time_enabled(self):
"""Has screen time enabled."""
return self._attrs.get("hasScreenTimeEnabled")
@property
def has_ask_to_buy_enabled(self):
"""Has to ask for buying."""
return self._attrs.get("hasAskToBuyEnabled")
@property
def has_share_purchases_enabled(self):
"""Has share purshases."""
return self._attrs.get("hasSharePurchasesEnabled")
@property
def share_my_location_enabled_family_members(self):
"""Has share my location with family."""
return self._attrs.get("shareMyLocationEnabledFamilyMembers")
@property
def has_share_my_location_enabled(self):
"""Has share my location."""
return self._attrs.get("hasShareMyLocationEnabled")
@property
def dsid_for_purchases(self):
"""Gets the dsid for purchases."""
return self._attrs.get("dsidForPurchases")
def get_photo(self):
"""Returns the photo."""
params_photo = dict(self._params)
params_photo.update({"memberId": self.dsid})
return self._session.get(
self._acc_family_member_photo_url, params=params_photo, stream=True
)
def __getitem__(self, key):
if self._attrs.get(key):
return self._attrs[key]
return getattr(self, key)
def __str__(self):
return u"{full_name}: {age_classification}".format(
full_name=self.full_name, age_classification=self.age_classification,
)
def __repr__(self):
return "<{display}>".format(
display=(
six.text_type(self)
if sys.version_info[0] >= 3
else six.text_type(self).encode("utf8", "replace")
)
)
class AccountStorageUsageForMedia(object):
"""Storage used for a specific media type into the account."""
def __init__(self, usage_data):
self.usage_data = usage_data
@property
def key(self):
"""Gets the key."""
return self.usage_data["mediaKey"]
@property
def label(self):
"""Gets the label."""
return self.usage_data["displayLabel"]
@property
def color(self):
"""Gets the HEX color."""
return self.usage_data["displayColor"]
@property
def usage_in_bytes(self):
"""Gets the usage in bytes."""
return self.usage_data["usageInBytes"]
def __str__(self):
return u"{key}: {usage}".format(key=self.key, usage=self.usage_in_bytes)
def __repr__(self):
return "<{display}>".format(
display=(
six.text_type(self)
if sys.version_info[0] >= 3
else six.text_type(self).encode("utf8", "replace")
)
)
class AccountStorageUsage(object):
"""Storage used for a specific media type into the account."""
def __init__(self, usage_data, quota_data):
self.usage_data = usage_data
self.quota_data = quota_data
@property
def comp_storage_in_bytes(self):
"""Gets the comp storage in bytes."""
return self.usage_data["compStorageInBytes"]
@property
def used_storage_in_bytes(self):
"""Gets the used storage in bytes."""
return self.usage_data["usedStorageInBytes"]
@property
def used_storage_in_percent(self):
"""Gets the used storage in percent."""
return self.used_storage_in_bytes * 100 / self.total_storage_in_bytes
@property
def available_storage_in_bytes(self):
"""Gets the available storage in bytes."""
return self.total_storage_in_bytes - self.used_storage_in_bytes
@property
def available_storage_in_percent(self):
"""Gets the available storage in percent."""
return self.available_storage_in_bytes * 100 / self.total_storage_in_bytes
@property
def total_storage_in_bytes(self):
"""Gets the total storage in bytes."""
return self.usage_data["totalStorageInBytes"]
@property
def commerce_storage_in_bytes(self):
"""Gets the commerce storage in bytes."""
return self.usage_data["commerceStorageInBytes"]
@property
def quota_over(self):
"""Gets the over quota."""
return self.quota_data["overQuota"]
@property
def quota_tier_max(self):
"""Gets the max tier quota."""
return self.quota_data["haveMaxQuotaTier"]
@property
def quota_almost_full(self):
"""Gets the almost full quota."""
return self.quota_data["almost-full"]
@property
def quota_paid(self):
"""Gets the paid quota."""
return self.quota_data["paidQuota"]
def __str__(self):
return u"{used_percent}%% used of {total} bytes".format(
used_percent=self.used_storage_in_percent, total=self.total_storage_in_bytes
)
def __repr__(self):
return "<{display}>".format(
display=(
six.text_type(self)
if sys.version_info[0] >= 3
else six.text_type(self).encode("utf8", "replace")
)
)
class AccountStorage(object):
"""Storage of the account."""
def __init__(self, storage_data):
self.usage = AccountStorageUsage(
storage_data.get("storageUsageInfo"), storage_data.get("quotaStatus")
)
self.usages_by_media = {}
for usage_media in storage_data.get("storageUsageByMedia"):
self.usages_by_media[usage_media["mediaKey"]] = AccountStorageUsageForMedia(
usage_media
)

View file

@ -20,8 +20,9 @@ from .const_login import (
VERIFICATION_CODE_OK, VERIFICATION_CODE_OK,
VERIFICATION_CODE_KO, VERIFICATION_CODE_KO,
) )
from .const_account import ACCOUNT_DEVICES_WORKING from .const_account import ACCOUNT_DEVICES_WORKING, ACCOUNT_STORAGE_WORKING
from .const_findmyiphone import FMI_FMLY_WORKING from .const_account_family import ACCOUNT_FAMILY_WORKING
from .const_findmyiphone import FMI_FAMILY_WORKING
class ResponseMock(Response): class ResponseMock(Response):
@ -76,10 +77,14 @@ class PyiCloudSessionMock(base.PyiCloudSession):
# Account # Account
if "device/getDevices" in url and method == "GET": if "device/getDevices" in url and method == "GET":
return ResponseMock(ACCOUNT_DEVICES_WORKING) return ResponseMock(ACCOUNT_DEVICES_WORKING)
if "family/getFamilyDetails" in url and method == "GET":
return ResponseMock(ACCOUNT_FAMILY_WORKING)
if "setup/ws/1/storageUsageInfo" in url and method == "GET":
return ResponseMock(ACCOUNT_STORAGE_WORKING)
# Find My iPhone # Find My iPhone
if "fmi" in url and method == "POST": if "fmi" in url and method == "POST":
return ResponseMock(FMI_FMLY_WORKING) return ResponseMock(FMI_FAMILY_WORKING)
return None return None

View file

@ -1,5 +1,5 @@
"""Test constants.""" """Test constants."""
from .const_login import PRIMARY_EMAIL, APPLE_ID_EMAIL, ICLOUD_ID_EMAIL from .const_account_family import PRIMARY_EMAIL, APPLE_ID_EMAIL, ICLOUD_ID_EMAIL
# Base # Base
AUTHENTICATED_USER = PRIMARY_EMAIL AUTHENTICATED_USER = PRIMARY_EMAIL

View file

@ -75,3 +75,45 @@ ACCOUNT_DEVICES_WORKING = {
}, },
], ],
} }
ACCOUNT_STORAGE_WORKING = {
"storageUsageByMedia": [
{
"mediaKey": "photos",
"displayLabel": "Photos et vidéos",
"displayColor": "ffcc00",
"usageInBytes": 0,
},
{
"mediaKey": "backup",
"displayLabel": "Sauvegarde",
"displayColor": "5856d6",
"usageInBytes": 799008186,
},
{
"mediaKey": "docs",
"displayLabel": "Documents",
"displayColor": "ff9500",
"usageInBytes": 449092146,
},
{
"mediaKey": "mail",
"displayLabel": "Mail",
"displayColor": "007aff",
"usageInBytes": 1101522944,
},
],
"storageUsageInfo": {
"compStorageInBytes": 0,
"usedStorageInBytes": 2348632876,
"totalStorageInBytes": 5368709120,
"commerceStorageInBytes": 0,
},
"quotaStatus": {
"overQuota": False,
"haveMaxQuotaTier": False,
"almost-full": False,
"paidQuota": False,
},
}

View file

@ -0,0 +1,97 @@
"""Account family test constants."""
# Fakers
FIRST_NAME = "Quentin"
LAST_NAME = "TARANTINO"
FULL_NAME = FIRST_NAME + " " + LAST_NAME
PERSON_ID = (FIRST_NAME + LAST_NAME).lower()
PRIMARY_EMAIL = PERSON_ID + "@hotmail.fr"
APPLE_ID_EMAIL = PERSON_ID + "@me.com"
ICLOUD_ID_EMAIL = PERSON_ID + "@icloud.com"
MEMBER_1_FIRST_NAME = "John"
MEMBER_1_LAST_NAME = "TRAVOLTA"
MEMBER_1_FULL_NAME = MEMBER_1_FIRST_NAME + " " + MEMBER_1_LAST_NAME
MEMBER_1_PERSON_ID = (MEMBER_1_FIRST_NAME + MEMBER_1_LAST_NAME).lower()
MEMBER_1_APPLE_ID = MEMBER_1_PERSON_ID + "@icloud.com"
MEMBER_2_FIRST_NAME = "Uma"
MEMBER_2_LAST_NAME = "THURMAN"
MEMBER_2_FULL_NAME = MEMBER_2_FIRST_NAME + " " + MEMBER_2_LAST_NAME
MEMBER_2_PERSON_ID = (MEMBER_2_FIRST_NAME + MEMBER_2_LAST_NAME).lower()
MEMBER_2_APPLE_ID = MEMBER_2_PERSON_ID + "@outlook.fr"
FAMILY_ID = "family_" + PERSON_ID
# Data
ACCOUNT_FAMILY_WORKING = {
"status-message": "Member of a family.",
"familyInvitations": [],
"outgoingTransferRequests": [],
"isMemberOfFamily": True,
"family": {
"familyId": FAMILY_ID,
"transferRequests": [],
"invitations": [],
"organizer": PERSON_ID,
"members": [PERSON_ID, MEMBER_2_PERSON_ID, MEMBER_1_PERSON_ID],
"outgoingTransferRequests": [],
"etag": "12",
},
"familyMembers": [
{
"lastName": LAST_NAME,
"dsid": PERSON_ID,
"originalInvitationEmail": PRIMARY_EMAIL,
"fullName": FULL_NAME,
"ageClassification": "ADULT",
"appleIdForPurchases": PRIMARY_EMAIL,
"appleId": PRIMARY_EMAIL,
"familyId": FAMILY_ID,
"firstName": FIRST_NAME,
"hasParentalPrivileges": True,
"hasScreenTimeEnabled": False,
"hasAskToBuyEnabled": False,
"hasSharePurchasesEnabled": True,
"shareMyLocationEnabledFamilyMembers": [],
"hasShareMyLocationEnabled": True,
"dsidForPurchases": PERSON_ID,
},
{
"lastName": MEMBER_2_LAST_NAME,
"dsid": MEMBER_2_PERSON_ID,
"originalInvitationEmail": MEMBER_2_APPLE_ID,
"fullName": MEMBER_2_FULL_NAME,
"ageClassification": "ADULT",
"appleIdForPurchases": MEMBER_2_APPLE_ID,
"appleId": MEMBER_2_APPLE_ID,
"familyId": FAMILY_ID,
"firstName": MEMBER_2_FIRST_NAME,
"hasParentalPrivileges": False,
"hasScreenTimeEnabled": False,
"hasAskToBuyEnabled": False,
"hasSharePurchasesEnabled": False,
"hasShareMyLocationEnabled": False,
"dsidForPurchases": MEMBER_2_PERSON_ID,
},
{
"lastName": MEMBER_1_LAST_NAME,
"dsid": MEMBER_1_PERSON_ID,
"originalInvitationEmail": MEMBER_1_APPLE_ID,
"fullName": MEMBER_1_FULL_NAME,
"ageClassification": "ADULT",
"appleIdForPurchases": MEMBER_1_APPLE_ID,
"appleId": MEMBER_1_APPLE_ID,
"familyId": FAMILY_ID,
"firstName": MEMBER_1_FIRST_NAME,
"hasParentalPrivileges": False,
"hasScreenTimeEnabled": False,
"hasAskToBuyEnabled": False,
"hasSharePurchasesEnabled": True,
"hasShareMyLocationEnabled": True,
"dsidForPurchases": MEMBER_1_PERSON_ID,
},
],
"status": 0,
"showAddMemberButton": True,
}

View file

@ -1,17 +1,19 @@
"""Find my iPhone test constants.""" """Find my iPhone test constants."""
from .const import CLIENT_ID from .const import CLIENT_ID
from .const_login import FIRST_NAME, LAST_NAME, PERSON_ID, FULL_NAME from .const_account_family import (
FIRST_NAME,
# Base LAST_NAME,
MEMBER_1_FIRST_NAME = "John" PERSON_ID,
MEMBER_1_LAST_NAME = "TRAVOLTA" FULL_NAME,
MEMBER_1_PERSON_ID = (MEMBER_1_FIRST_NAME + MEMBER_1_LAST_NAME).lower() MEMBER_1_FIRST_NAME,
MEMBER_1_APPLE_ID = MEMBER_1_PERSON_ID + "@icloud.com" MEMBER_1_APPLE_ID,
MEMBER_1_LAST_NAME,
MEMBER_2_FIRST_NAME = "Uma" MEMBER_1_PERSON_ID,
MEMBER_2_LAST_NAME = "THURMAN" MEMBER_2_APPLE_ID,
MEMBER_2_PERSON_ID = (MEMBER_2_FIRST_NAME + MEMBER_2_LAST_NAME).lower() MEMBER_2_FIRST_NAME,
MEMBER_2_APPLE_ID = MEMBER_2_PERSON_ID + "@outlook.fr" MEMBER_2_LAST_NAME,
MEMBER_2_PERSON_ID,
)
# Fakers # Fakers
UUID = "ABCDEFGH-1234-5678-1234-ABCDEFGHIJKL" UUID = "ABCDEFGH-1234-5678-1234-ABCDEFGHIJKL"
@ -23,7 +25,7 @@ LOCATION_LONGITUDE = 6.1234567890123456
# id = rawDeviceModel + prsId (if not None) # id = rawDeviceModel + prsId (if not None)
# baUUID = UUID + id # baUUID = UUID + id
# So they can still be faked and unique # So they can still be faked and unique
FMI_FMLY_WORKING = { FMI_FAMILY_WORKING = {
"userInfo": { "userInfo": {
"accountFormatter": 0, "accountFormatter": 0,
"firstName": FIRST_NAME, "firstName": FIRST_NAME,

View file

@ -1,20 +1,19 @@
"""Login test constants.""" """Login test constants."""
from .const_account_family import (
# Base FIRST_NAME,
FIRST_NAME = "Quentin" LAST_NAME,
LAST_NAME = "TARANTINO" PERSON_ID,
FULL_NAME = FIRST_NAME + " " + LAST_NAME FULL_NAME,
PRIMARY_EMAIL,
APPLE_ID_EMAIL,
ICLOUD_ID_EMAIL,
)
PERSON_ID = (FIRST_NAME + LAST_NAME).lower() PERSON_ID = (FIRST_NAME + LAST_NAME).lower()
NOTIFICATION_ID = "12345678-1234-1234-1234-123456789012" + PERSON_ID NOTIFICATION_ID = "12345678-1234-1234-1234-123456789012" + PERSON_ID
A_DS_ID = "123456-12-12345678-1234-1234-1234-123456789012" + PERSON_ID A_DS_ID = "123456-12-12345678-1234-1234-1234-123456789012" + PERSON_ID
WIDGET_KEY = "widget_key" + PERSON_ID WIDGET_KEY = "widget_key" + PERSON_ID
PRIMARY_EMAIL = PERSON_ID + "@hotmail.fr"
APPLE_ID_EMAIL = PERSON_ID + "@me.com"
ICLOUD_ID_EMAIL = PERSON_ID + "@icloud.com"
# Data # Data
LOGIN_WORKING = { LOGIN_WORKING = {
"dsInfo": { "dsInfo": {

View file

@ -14,6 +14,7 @@ class AccountServiceTest(TestCase):
def test_devices(self): def test_devices(self):
"""Tests devices.""" """Tests devices."""
assert self.service.devices
assert len(self.service.devices) == 2 assert len(self.service.devices) == 2
for device in self.service.devices: for device in self.service.devices:
@ -31,3 +32,53 @@ class AccountServiceTest(TestCase):
assert device["modelSmallPhotoURL2x"] assert device["modelSmallPhotoURL2x"]
assert device["modelSmallPhotoURL1x"] assert device["modelSmallPhotoURL1x"]
assert device["modelDisplayName"] assert device["modelDisplayName"]
def test_family(self):
"""Tests family members."""
assert self.service.family
assert len(self.service.family) == 3
for member in self.service.family:
assert member.last_name
assert member.dsid
assert member.original_invitation_email
assert member.full_name
assert member.age_classification
assert member.apple_id_for_purchases
assert member.apple_id
assert member.first_name
assert not member.has_screen_time_enabled
assert not member.has_ask_to_buy_enabled
assert not member.share_my_location_enabled_family_members
assert member.dsid_for_purchases
def test_storage(self):
"""Tests storage."""
assert self.service.storage
assert self.service.storage.usage
assert (
self.service.storage.usage.comp_storage_in_bytes
or self.service.storage.usage.comp_storage_in_bytes == 0
)
assert self.service.storage.usage.used_storage_in_bytes
assert self.service.storage.usage.used_storage_in_percent
assert self.service.storage.usage.available_storage_in_bytes
assert self.service.storage.usage.available_storage_in_percent
assert self.service.storage.usage.total_storage_in_bytes
assert (
self.service.storage.usage.commerce_storage_in_bytes
or self.service.storage.usage.commerce_storage_in_bytes == 0
)
assert not self.service.storage.usage.quota_over
assert not self.service.storage.usage.quota_tier_max
assert not self.service.storage.usage.quota_almost_full
assert not self.service.storage.usage.quota_paid
assert self.service.storage.usages_by_media
for usage_media in self.service.storage.usages_by_media.values():
assert usage_media.key
assert usage_media.label
assert usage_media.color
assert usage_media.usage_in_bytes or usage_media.usage_in_bytes == 0

View file

@ -2,7 +2,7 @@
from pyicloud import cmdline from pyicloud import cmdline
from . import PyiCloudServiceMock from . import PyiCloudServiceMock
from .const import AUTHENTICATED_USER, REQUIRES_2SA_USER, VALID_PASSWORD from .const import AUTHENTICATED_USER, REQUIRES_2SA_USER, VALID_PASSWORD
from .const_findmyiphone import FMI_FMLY_WORKING from .const_findmyiphone import FMI_FAMILY_WORKING
import os import os
import sys import sys
@ -103,7 +103,7 @@ class TestCmdline(TestCase):
]) ])
# fmt: on # fmt: on
devices = FMI_FMLY_WORKING.get("content") devices = FMI_FAMILY_WORKING.get("content")
for device in devices: for device in devices:
file_name = device.get("name").strip().lower() + ".fmip_snapshot" file_name = device.get("name").strip().lower() + ".fmip_snapshot"