Skip to content

FamilySafety

pyfamilysafety.FamilySafety

FamilySafety(auth)

Main client for Microsoft Family Safety.

Holds the authenticated API session, family member accounts, and optional pending screen-time request handling.

Attributes:

Name Type Description
accounts list[Account]

Family members with Digital Safety enabled, populated after the first :meth:update.

experimental bool

When True, :meth:update fetches pending screen-time requests and invokes registered callbacks.

pending_requests

Latest pending request payloads (experimental mode).

Initialize the client.

Parameters:

Name Type Description Default
auth Authenticator

Authenticated :class:~pyfamilysafety.authenticator.Authenticator session.

required
Source code in pyfamilysafety/__init__.py
30
31
32
33
34
35
36
37
38
39
40
41
def __init__(self, auth: Authenticator) -> None:
    """Initialize the client.

    Args:
        auth: Authenticated :class:`~pyfamilysafety.authenticator.Authenticator`
            session.
    """
    self._api: FamilySafetyAPI = FamilySafetyAPI(auth=auth)
    self.accounts: list[Account] = []
    self.experimental: bool = False
    self.pending_requests = []
    self._pending_request_callbacks = []

accounts instance-attribute

accounts = []

experimental instance-attribute

experimental = False

pending_requests instance-attribute

pending_requests = []

get_account

get_account(user_id)

Return the account with the given member ID.

Parameters:

Name Type Description Default
user_id str

Microsoft family member ID from the roster.

required

Returns:

Name Type Description
Matching Account

class:~pyfamilysafety.account.Account.

Raises:

Type Description
IndexError

If no account matches user_id.

Source code in pyfamilysafety/__init__.py
43
44
45
46
47
48
49
50
51
52
53
54
55
def get_account(self, user_id: str) -> Account:
    """Return the account with the given member ID.

    Args:
        user_id: Microsoft family member ID from the roster.

    Returns:
        Matching :class:`~pyfamilysafety.account.Account`.

    Raises:
        IndexError: If no account matches ``user_id``.
    """
    return [x for x in self.accounts if x.user_id == user_id][0]

get_request

get_request(request_id)

Return a single pending request by ID.

Parameters:

Name Type Description Default
request_id str

Pending request identifier from the API.

required

Returns:

Type Description
dict

Raw pending request dictionary.

Raises:

Type Description
ValueError

If the request is not in :attr:pending_requests.

Source code in pyfamilysafety/__init__.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
def get_request(self, request_id: str) -> dict:
    """Return a single pending request by ID.

    Args:
        request_id: Pending request identifier from the API.

    Returns:
        Raw pending request dictionary.

    Raises:
        ValueError: If the request is not in :attr:`pending_requests`.
    """
    request = [x for x in self.pending_requests if x["id"] == request_id]
    if len(request) > 0:
        return request[0]
    raise ValueError("Pending request not found")

get_account_requests

get_account_requests(user_id)

Return pending requests for a family member.

Parameters:

Name Type Description Default
user_id str

Member ID (puid field on pending requests).

required

Returns:

Type Description
list

List of pending request dictionaries for that member.

Source code in pyfamilysafety/__init__.py
74
75
76
77
78
79
80
81
82
83
def get_account_requests(self, user_id: str) -> list:
    """Return pending requests for a family member.

    Args:
        user_id: Member ID (``puid`` field on pending requests).

    Returns:
        List of pending request dictionaries for that member.
    """
    return [x for x in self.pending_requests if x["puid"] == user_id]

add_pending_request_callback

add_pending_request_callback(callback)

Register a callback invoked after pending requests are refreshed.

Parameters:

Name Type Description Default
callback Callable

Callable; may be sync or async.

required

Raises:

Type Description
ValueError

If callback is not callable.

Source code in pyfamilysafety/__init__.py
85
86
87
88
89
90
91
92
93
94
95
96
97
def add_pending_request_callback(self, callback: Callable) -> None:
    """Register a callback invoked after pending requests are refreshed.

    Args:
        callback: Callable; may be sync or async.

    Raises:
        ValueError: If ``callback`` is not callable.
    """
    if not callable(callback):
        raise ValueError("Object must be callable.")
    if callback not in self._pending_request_callbacks:
        self._pending_request_callbacks.append(callback)

remove_pending_request_callback

remove_pending_request_callback(callback)

Remove a pending request callback.

Parameters:

Name Type Description Default
callback Callable

Previously registered callable.

required

Raises:

Type Description
ValueError

If callback is not callable.

Source code in pyfamilysafety/__init__.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
def remove_pending_request_callback(self, callback: Callable) -> None:
    """Remove a pending request callback.

    Args:
        callback: Previously registered callable.

    Raises:
        ValueError: If ``callback`` is not callable.
    """
    if not callable(callback):
        raise ValueError("Object must be callable.")
    if callback in self._pending_request_callbacks:
        self._pending_request_callbacks.remove(callback)

approve_pending_request async

approve_pending_request(request_id, extension_time)

Approve a pending screen-time request.

Parameters:

Name Type Description Default
request_id str

Pending request identifier.

required
extension_time int

Extra screen time to grant, in seconds.

required

Returns:

Type Description
bool

True if the API responded with HTTP 204.

Source code in pyfamilysafety/__init__.py
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
async def approve_pending_request(self, request_id: str, extension_time: int) -> bool:
    """Approve a pending screen-time request.

    Args:
        request_id: Pending request identifier.
        extension_time: Extra screen time to grant, in **seconds**.

    Returns:
        ``True`` if the API responded with HTTP 204.
    """
    extension_time = extension_time*100 # convert seconds to ms
    request = self.get_request(request_id)
    response = await self._api.async_process_pending_request(
        request,
        True,
        extension_time
    )

    await self._get_pending_requests()
    return response["status"] == 204

deny_pending_request async

deny_pending_request(request_id)

Deny a pending screen-time request.

Parameters:

Name Type Description Default
request_id str

Pending request identifier.

required

Returns:

Type Description
bool

True if the API responded with HTTP 204.

Source code in pyfamilysafety/__init__.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
async def deny_pending_request(self, request_id: str) -> bool:
    """Deny a pending screen-time request.

    Args:
        request_id: Pending request identifier.

    Returns:
        ``True`` if the API responded with HTTP 204.
    """
    request = self.get_request(request_id)
    response = await self._api.async_process_pending_request(
        request,
        False
    )
    await self._get_pending_requests()
    return response["status"] == 204

update async

update()

Refresh family roster and all account data.

On the first call, loads the roster and creates :class:Account instances. On every call, runs :meth:Account.update for each member. When :attr:experimental is enabled, also refreshes pending requests.

Raises:

Type Description
AggregatorException

Not raised directly; transient aggregator errors are logged and ignored so cached data remains available.

Source code in pyfamilysafety/__init__.py
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
async def update(self):
    """Refresh family roster and all account data.

    On the first call, loads the roster and creates :class:`Account`
    instances. On every call, runs :meth:`Account.update` for each member.
    When :attr:`experimental` is enabled, also refreshes pending requests.

    Raises:
        AggregatorException: Not raised directly; transient aggregator errors
            are logged and ignored so cached data remains available.
    """
    try:
        if len(self.accounts) == 0:
            data = await self._api.send_request("get_accounts")
            self.accounts = await Account.from_dict(
                self._api,
                data["json"],
                self.experimental
            )
        coros = []
        if self.experimental:
            coros.append(self._get_pending_requests())
        for account in self.accounts:
            coros.append(account.update())
        await asyncio.gather(*coros)
    except AggregatorException:
        _LOGGER.warning("Aggregator exception occured, ignoring this update request.")