feat: export all classes in top-level package

This commit is contained in:
Mike A.
2025-09-11 16:19:23 +02:00
parent d7af92653d
commit 2f1ca10def
7 changed files with 120 additions and 38 deletions

View File

@@ -1,15 +1,71 @@
"""A package providing everything you need to work with Apple's FindMy network."""
from . import errors, keys, plist, reports, scanner
from .accessory import FindMyAccessory
from .keys import KeyPair
from .accessory import FindMyAccessory, FindMyAccessoryMapping, RollingKeyPairSource
from .errors import (
InvalidCredentialsError,
InvalidStateError,
UnauthorizedError,
UnhandledProtocolError,
)
from .keys import HasHashedPublicKey, HasPublicKey, KeyPair, KeyPairMapping, KeyPairType
from .reports import (
AccountStateMapping,
AppleAccount,
AsyncAppleAccount,
AsyncSmsSecondFactor,
AsyncTrustedDeviceSecondFactor,
BaseAnisetteProvider,
BaseAppleAccount,
BaseSecondFactorMethod,
LocalAnisetteMapping,
LocalAnisetteProvider,
LoginState,
RemoteAnisetteMapping,
RemoteAnisetteProvider,
SmsSecondFactorMethod,
SyncSmsSecondFactor,
SyncTrustedDeviceSecondFactor,
TrustedDeviceSecondFactorMethod,
)
from .scanner import (
NearbyOfflineFindingDevice,
OfflineFindingDevice,
OfflineFindingScanner,
SeparatedOfflineFindingDevice,
)
__all__ = (
"AccountStateMapping",
"AppleAccount",
"AsyncAppleAccount",
"AsyncSmsSecondFactor",
"AsyncTrustedDeviceSecondFactor",
"BaseAnisetteProvider",
"BaseAppleAccount",
"BaseSecondFactorMethod",
"FindMyAccessory",
"FindMyAccessoryMapping",
"HasHashedPublicKey",
"HasPublicKey",
"InvalidCredentialsError",
"InvalidStateError",
"KeyPair",
"errors",
"keys",
"plist",
"reports",
"scanner",
"KeyPairMapping",
"KeyPairType",
"LocalAnisetteMapping",
"LocalAnisetteProvider",
"LoginState",
"NearbyOfflineFindingDevice",
"OfflineFindingDevice",
"OfflineFindingScanner",
"RemoteAnisetteMapping",
"RemoteAnisetteProvider",
"RollingKeyPairSource",
"SeparatedOfflineFindingDevice",
"SmsSecondFactorMethod",
"SyncSmsSecondFactor",
"SyncTrustedDeviceSecondFactor",
"TrustedDeviceSecondFactorMethod",
"UnauthorizedError",
"UnhandledProtocolError",
)

View File

@@ -16,7 +16,7 @@ from typing_extensions import override
from findmy.util.abc import Serializable
from findmy.util.files import read_data_json, read_data_plist, save_and_return_json
from .keys import KeyGenerator, KeyPair, KeyType
from .keys import KeyGenerator, KeyPair, KeyPairType
from .util import crypto
if TYPE_CHECKING:
@@ -116,8 +116,8 @@ class FindMyAccessory(RollingKeyPairSource, Serializable[FindMyAccessoryMapping]
:param skn: The SKN for the primary key.
:param sks: The SKS for the secondary key.
"""
self._primary_gen = AccessoryKeyGenerator(master_key, skn, KeyType.PRIMARY)
self._secondary_gen = AccessoryKeyGenerator(master_key, sks, KeyType.SECONDARY)
self._primary_gen = _AccessoryKeyGenerator(master_key, skn, KeyPairType.PRIMARY)
self._secondary_gen = _AccessoryKeyGenerator(master_key, sks, KeyPairType.SECONDARY)
self._paired_at: datetime = paired_at
if self._paired_at.tzinfo is None:
self._paired_at = self._paired_at.astimezone()
@@ -368,14 +368,14 @@ class FindMyAccessory(RollingKeyPairSource, Serializable[FindMyAccessoryMapping]
raise ValueError(msg) from None
class AccessoryKeyGenerator(KeyGenerator[KeyPair]):
class _AccessoryKeyGenerator(KeyGenerator[KeyPair]):
"""KeyPair generator. Uses the same algorithm internally as FindMy accessories do."""
def __init__(
self,
master_key: bytes,
initial_sk: bytes,
key_type: KeyType = KeyType.UNKNOWN,
key_type: KeyPairType = KeyPairType.UNKNOWN,
) -> None:
"""
Initialize the key generator.
@@ -411,7 +411,7 @@ class AccessoryKeyGenerator(KeyGenerator[KeyPair]):
return self._initial_sk
@property
def key_type(self) -> KeyType:
def key_type(self) -> KeyPairType:
"""The type of key this generator produces."""
return self._key_type

View File

@@ -22,7 +22,7 @@ if TYPE_CHECKING:
from pathlib import Path
class KeyType(Enum):
class KeyPairType(Enum):
"""Enum of possible key types."""
UNKNOWN = 0
@@ -133,7 +133,7 @@ class KeyPair(HasPublicKey, Serializable[KeyPairMapping]):
def __init__(
self,
private_key: bytes,
key_type: KeyType = KeyType.UNKNOWN,
key_type: KeyPairType = KeyPairType.UNKNOWN,
name: str | None = None,
) -> None:
"""Initialize the :meth:`KeyPair` with the private key bytes."""
@@ -147,7 +147,7 @@ class KeyPair(HasPublicKey, Serializable[KeyPairMapping]):
self._name = name
@property
def key_type(self) -> KeyType:
def key_type(self) -> KeyPairType:
"""Type of this key."""
return self._key_type
@@ -217,7 +217,7 @@ class KeyPair(HasPublicKey, Serializable[KeyPairMapping]):
try:
return cls(
private_key=base64.b64decode(val["private_key"]),
key_type=KeyType(val["key_type"]),
key_type=KeyPairType(val["key_type"]),
name=val["name"],
)
except KeyError as e:

View File

@@ -1,16 +1,40 @@
"""Code related to fetching location reports."""
from .account import AppleAccount, AsyncAppleAccount
from .anisette import BaseAnisetteProvider, RemoteAnisetteProvider
from .account import AccountStateMapping, AppleAccount, AsyncAppleAccount, BaseAppleAccount
from .anisette import (
BaseAnisetteProvider,
LocalAnisetteMapping,
LocalAnisetteProvider,
RemoteAnisetteMapping,
RemoteAnisetteProvider,
)
from .state import LoginState
from .twofactor import SmsSecondFactorMethod, TrustedDeviceSecondFactorMethod
from .twofactor import (
AsyncSmsSecondFactor,
AsyncTrustedDeviceSecondFactor,
BaseSecondFactorMethod,
SmsSecondFactorMethod,
SyncSmsSecondFactor,
SyncTrustedDeviceSecondFactor,
TrustedDeviceSecondFactorMethod,
)
__all__ = (
"AccountStateMapping",
"AppleAccount",
"AsyncAppleAccount",
"AsyncSmsSecondFactor",
"AsyncTrustedDeviceSecondFactor",
"BaseAnisetteProvider",
"BaseAppleAccount",
"BaseSecondFactorMethod",
"LocalAnisetteMapping",
"LocalAnisetteProvider",
"LoginState",
"RemoteAnisetteMapping",
"RemoteAnisetteProvider",
"SmsSecondFactorMethod",
"SyncSmsSecondFactor",
"SyncTrustedDeviceSecondFactor",
"TrustedDeviceSecondFactorMethod",
)

View File

@@ -106,7 +106,7 @@ _A = TypeVar("_A", bound="BaseAppleAccount")
_F = Callable[Concatenate[_A, _P], _R]
def require_login_state(*states: LoginState) -> Callable[[_F], _F]:
def _require_login_state(*states: LoginState) -> Callable[[_F], _F]:
"""Enforce a login state as precondition for a method."""
def decorator(func: _F) -> _F:
@@ -403,7 +403,7 @@ class AsyncAppleAccount(BaseAppleAccount):
return self._login_state
@property
@require_login_state(
@_require_login_state(
LoginState.LOGGED_IN,
LoginState.AUTHENTICATED,
LoginState.REQUIRE_2FA,
@@ -414,7 +414,7 @@ class AsyncAppleAccount(BaseAppleAccount):
return self._account_info["account_name"] if self._account_info else None
@property
@require_login_state(
@_require_login_state(
LoginState.LOGGED_IN,
LoginState.AUTHENTICATED,
LoginState.REQUIRE_2FA,
@@ -425,7 +425,7 @@ class AsyncAppleAccount(BaseAppleAccount):
return self._account_info["first_name"] if self._account_info else None
@property
@require_login_state(
@_require_login_state(
LoginState.LOGGED_IN,
LoginState.AUTHENTICATED,
LoginState.REQUIRE_2FA,
@@ -496,7 +496,7 @@ class AsyncAppleAccount(BaseAppleAccount):
except (RuntimeError, OSError, ConnectionError) as e:
logger.warning("Error closing HTTP session: %s", e)
@require_login_state(LoginState.LOGGED_OUT)
@_require_login_state(LoginState.LOGGED_OUT)
@override
async def login(self, username: str, password: str) -> LoginState:
"""See :meth:`BaseAppleAccount.login`."""
@@ -508,7 +508,7 @@ class AsyncAppleAccount(BaseAppleAccount):
# AUTHENTICATED -> LOGGED_IN
return await self._login_mobileme()
@require_login_state(LoginState.REQUIRE_2FA)
@_require_login_state(LoginState.REQUIRE_2FA)
@override
async def get_2fa_methods(self) -> Sequence[AsyncSecondFactorMethod]:
"""See :meth:`BaseAppleAccount.get_2fa_methods`."""
@@ -537,7 +537,7 @@ class AsyncAppleAccount(BaseAppleAccount):
return methods
@require_login_state(LoginState.REQUIRE_2FA)
@_require_login_state(LoginState.REQUIRE_2FA)
@override
async def sms_2fa_request(self, phone_number_id: int) -> None:
"""See :meth:`BaseAppleAccount.sms_2fa_request`."""
@@ -549,7 +549,7 @@ class AsyncAppleAccount(BaseAppleAccount):
data,
)
@require_login_state(LoginState.REQUIRE_2FA)
@_require_login_state(LoginState.REQUIRE_2FA)
@override
async def sms_2fa_submit(self, phone_number_id: int, code: str) -> LoginState:
"""See :meth:`BaseAppleAccount.sms_2fa_submit`."""
@@ -574,7 +574,7 @@ class AsyncAppleAccount(BaseAppleAccount):
# AUTHENTICATED -> LOGGED_IN
return await self._login_mobileme()
@require_login_state(LoginState.REQUIRE_2FA)
@_require_login_state(LoginState.REQUIRE_2FA)
@override
async def td_2fa_request(self) -> None:
"""See :meth:`BaseAppleAccount.td_2fa_request`."""
@@ -588,7 +588,7 @@ class AsyncAppleAccount(BaseAppleAccount):
headers=headers,
)
@require_login_state(LoginState.REQUIRE_2FA)
@_require_login_state(LoginState.REQUIRE_2FA)
@override
async def td_2fa_submit(self, code: str) -> LoginState:
"""See :meth:`BaseAppleAccount.td_2fa_submit`."""
@@ -612,7 +612,7 @@ class AsyncAppleAccount(BaseAppleAccount):
# AUTHENTICATED -> LOGGED_IN
return await self._login_mobileme()
@require_login_state(LoginState.LOGGED_IN)
@_require_login_state(LoginState.LOGGED_IN)
async def fetch_raw_reports(
self,
devices: list[tuple[list[str], list[str]]],
@@ -740,7 +740,7 @@ class AsyncAppleAccount(BaseAppleAccount):
keys: Sequence[HasHashedPublicKey | RollingKeyPairSource],
) -> dict[HasHashedPublicKey | RollingKeyPairSource, LocationReport | None]: ...
@require_login_state(LoginState.LOGGED_IN)
@_require_login_state(LoginState.LOGGED_IN)
@override
async def fetch_location(
self,
@@ -759,7 +759,7 @@ class AsyncAppleAccount(BaseAppleAccount):
return {dev: sorted(reports)[-1] if reports else None for dev, reports in hist.items()}
@require_login_state(LoginState.LOGGED_OUT, LoginState.REQUIRE_2FA, LoginState.LOGGED_IN)
@_require_login_state(LoginState.LOGGED_OUT, LoginState.REQUIRE_2FA, LoginState.LOGGED_IN)
async def _gsa_authenticate(
self,
username: str | None = None,
@@ -856,7 +856,7 @@ class AsyncAppleAccount(BaseAppleAccount):
msg = f"Unknown auth value: {au}"
raise UnhandledProtocolError(msg)
@require_login_state(LoginState.AUTHENTICATED)
@_require_login_state(LoginState.AUTHENTICATED)
async def _login_mobileme(self) -> LoginState:
logger.info("Logging into com.apple.mobileme")
data = plistlib.dumps(

View File

@@ -17,7 +17,7 @@ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from typing_extensions import override
from findmy.accessory import RollingKeyPairSource
from findmy.keys import HasHashedPublicKey, KeyPair, KeyPairMapping, KeyType
from findmy.keys import HasHashedPublicKey, KeyPair, KeyPairMapping, KeyPairType
from findmy.util.abc import Serializable
from findmy.util.files import read_data_json, save_and_return_json
@@ -463,10 +463,10 @@ class LocationReportsFetcher:
# split into primary and secondary keys
# (UNKNOWN keys are filed as primary)
new_keys_primary: set[str] = {
key.hashed_adv_key_b64 for key in key_batch if key.key_type == KeyType.PRIMARY
key.hashed_adv_key_b64 for key in key_batch if key.key_type == KeyPairType.PRIMARY
}
new_keys_secondary: set[str] = {
key.hashed_adv_key_b64 for key in key_batch if key.key_type != KeyType.PRIMARY
key.hashed_adv_key_b64 for key in key_batch if key.key_type != KeyPairType.PRIMARY
}
# 290 seems to be the maximum number of keys that Apple accepts in a single request,

View File

@@ -2,12 +2,14 @@
from .scanner import (
NearbyOfflineFindingDevice,
OfflineFindingDevice,
OfflineFindingScanner,
SeparatedOfflineFindingDevice,
)
__all__ = (
"NearbyOfflineFindingDevice",
"OfflineFindingDevice",
"OfflineFindingScanner",
"SeparatedOfflineFindingDevice",
)