mirror of
https://github.com/malmeloo/FindMy.py.git
synced 2026-04-18 01:53:58 +02:00
Merge pull request #7 from malmeloo/feat/better-docs
Better documentation
This commit is contained in:
47
.github/workflows/docs.yml
vendored
47
.github/workflows/docs.yml
vendored
@@ -3,7 +3,7 @@ name: Deploy documentation
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
tags:
|
||||
- 'v[0-9]\.[0-9]+\.[0-9]+'
|
||||
|
||||
jobs:
|
||||
@@ -15,26 +15,29 @@ jobs:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv and set the python version
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Build documentation
|
||||
run: |
|
||||
cd docs
|
||||
uv run make html
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Upload Pages artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: 'docs/_build/html/'
|
||||
- name: Install uv and set the python version
|
||||
uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
- name: Install graphviz
|
||||
run: sudo apt-get install -y graphviz
|
||||
|
||||
- name: Build documentation
|
||||
run: |
|
||||
cd docs
|
||||
uv run make html
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Upload Pages artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: "docs/_build/html/"
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
1
docs/.gitignore
vendored
Normal file
1
docs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
reference/
|
||||
19
docs/conf.py
19
docs/conf.py
@@ -6,26 +6,39 @@
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
import os
|
||||
import re
|
||||
|
||||
project = "FindMy.py"
|
||||
copyright = "2024, Mike Almeloo"
|
||||
author = "Mike Almeloo"
|
||||
release = "0.2.1"
|
||||
version = re.sub("^v", "", os.popen("git describe --tags").read().strip()) # noqa: S605, S607
|
||||
release = version
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||
|
||||
extensions = ["sphinx.ext.duration", "autoapi.extension"]
|
||||
extensions = [
|
||||
"myst_parser",
|
||||
"sphinx.ext.duration",
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.inheritance_diagram",
|
||||
"autoapi.extension",
|
||||
]
|
||||
|
||||
templates_path = ["_templates"]
|
||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||
|
||||
# -- AutoAPI Options ---------------------------------------------------------
|
||||
autoapi_dirs = ["../findmy/"]
|
||||
autoapi_root = "reference/"
|
||||
autoapi_add_toctree_entry = False
|
||||
autoapi_keep_files = True
|
||||
autoapi_options = [
|
||||
"members",
|
||||
"undoc-members",
|
||||
"show-inheritance",
|
||||
"show-inheritance-diagram",
|
||||
"show-module-summary",
|
||||
"special-members",
|
||||
"imported-members",
|
||||
@@ -34,5 +47,5 @@ autoapi_options = [
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||
|
||||
html_theme = "alabaster"
|
||||
html_theme = "sphinx_book_theme"
|
||||
html_static_path = ["_static"]
|
||||
|
||||
7
docs/getstarted/01-account.md
Normal file
7
docs/getstarted/01-account.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Logging in
|
||||
|
||||
Some useful features of this library require an active login session with Apple in order to work correctly.
|
||||
The reason for this is that the remote endpoints require authentication to actually retrieve data.
|
||||
This page will guide you through the steps needed to log into an Apple account using FindMy.py.
|
||||
|
||||
|
||||
10
docs/getstarted/index.md
Normal file
10
docs/getstarted/index.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Getting Started
|
||||
|
||||
* * *
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
:glob:
|
||||
|
||||
*
|
||||
```
|
||||
35
docs/index.md
Normal file
35
docs/index.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# FindMy.py
|
||||
|
||||
FindMy.py is a Python library aiming to provide everything you need
|
||||
to interact with **Apple's FindMy Network**.
|
||||
Its primary aims are feature completeness, reliability and elegant API design.
|
||||
It abstracts all the heavy lifting away, while keeping the control flow in your hands.
|
||||
|
||||
## Jump To
|
||||
|
||||
[//]: # "This is hidden to prevent it from showing on the home page"
|
||||
|
||||
```{toctree}
|
||||
:hidden:
|
||||
|
||||
Home <self>
|
||||
```
|
||||
|
||||
[//]: # "Documentation can be expanded to maxdepth 2"
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
getstarted/index
|
||||
technical/index
|
||||
reveng/index
|
||||
```
|
||||
|
||||
[//]: # "Show these with a maxdepth of 1"
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
API Reference <reference/findmy/index>
|
||||
genindex
|
||||
```
|
||||
@@ -1,18 +0,0 @@
|
||||
.. FindMy.py documentation master file, created by
|
||||
sphinx-quickstart on Tue Jan 2 21:16:55 2024.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to FindMy.py's documentation!
|
||||
=====================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
84
docs/reveng/20-Network_Requests.md
Normal file
84
docs/reveng/20-Network_Requests.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Intercepting Network Requests
|
||||
|
||||
A big part of our understanding of how the FindMy network functions originates from
|
||||
network captures detailing how official apps query location reports.
|
||||
This page aims to provide a quickstart on setting up an environment in which you can
|
||||
freely inspect network requests made the FindMy app on a Mac.
|
||||
|
||||
```{note}
|
||||
This guide has only been tested on Sonoma, but it will likely work on other
|
||||
versions of MacOS as well.
|
||||
```
|
||||
|
||||
## Disabling SSL pinning
|
||||
|
||||
Applications on MacOS implement SSL pinning by default. This means that Apple can determine which server-side
|
||||
certificates are allowed to be used when an application makes a network request. This presents a problem
|
||||
when we want to inspect these requests: typically, to inspect encrypted traffic using a proxy, we need to perform
|
||||
a Man-In-The-Middle (MITM) attack on ourselves in order to 'swap out' the certificate with one that we have the private key of.
|
||||
This is not possible while SSL pinning is active, because the application will simply reject our certificate.
|
||||
|
||||
For this reason, we will first need to disable SSL pinning. We will do this by utilizing [Frida](https://frida.re/)
|
||||
to attach to the processes that we want to inspect, and then using a script to bypass SSL pinning.
|
||||
|
||||
Start off by downloading [this JavaScript file](https://gist.github.com/azenla/37f941de24c5dfe46f3b8e93d94ce909) and saving
|
||||
it to a location where you can easily find it again.
|
||||
|
||||
Next, let's actually install Frida by running the following command:
|
||||
|
||||
```bash
|
||||
pip install frida-tools==13.7.1
|
||||
```
|
||||
|
||||
```{hint}
|
||||
The above command installs an older version of Frida that is compatible with the script we are going to use.
|
||||
If you need to use a newer version for whatever reason, you need to apply [these fixes](https://gist.github.com/azenla/37f941de24c5dfe46f3b8e93d94ce909?permalink_comment_id=5675248#gistcomment-5675248)
|
||||
to the script we downloaded before continuing.
|
||||
|
||||
Note that I will not be able to provide support if you use a version other than the one suggested above.
|
||||
```
|
||||
|
||||
To inspect network requests for FindMy, we want to attach Frida to the `searchpartyuseragent` daemon.
|
||||
Open a terminal and enter the following command, substituting the path to the script if necessary:
|
||||
|
||||
```bash
|
||||
frida -l disable-ssl-pin.js searchpartyuseragent
|
||||
```
|
||||
|
||||
```{important}
|
||||
If the above command does not work, you may need to temporarily disable [System Integrity Protection](https://developer.apple.com/documentation/security/disabling_and_enabling_system_integrity_protection).
|
||||
Make sure to re-enable it once you're done intercepting!
|
||||
```
|
||||
|
||||
If all went well, Frida should now be running. Keep the terminal open while capturing network requests.
|
||||
|
||||
## Intercepting requests
|
||||
|
||||
If you're already familiar with MITM proxies, you can probably skip this step; just use your favorite proxy
|
||||
while Frida is running. If you're not, read on.
|
||||
|
||||
We will be using [mitmproxy](https://www.mitmproxy.org/) in order to intercept network requests. Install it before continuing:
|
||||
|
||||
```bash
|
||||
brew install --cask mitmproxy
|
||||
```
|
||||
|
||||
Mitmproxy supports several methods to intercept local traffic. We will be using `Local Capture` mode, as it's the easiest to set up
|
||||
and tear down afterwards. Run the following command to start the proxy:
|
||||
|
||||
```bash
|
||||
mitmweb --mode local
|
||||
```
|
||||
|
||||
```{tip}
|
||||
Mitmproxy / MacOS may bug you about enabling the correct profile in system settings. If it does, simply do what it says
|
||||
and come back here.
|
||||
```
|
||||
|
||||
```{tip}
|
||||
Applications other than FindMy may lose their network connection while the capture is running. Simply stop mitmproxy
|
||||
once you're done and it will go back to normal.
|
||||
```
|
||||
|
||||
If all went well, your browser should open the mitmweb interface. From here, you will see all network requests being made
|
||||
by `searchpartyuseragent`, as well as their responses.
|
||||
14
docs/reveng/index.md
Normal file
14
docs/reveng/index.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Reverse Engineering
|
||||
|
||||
Want to help reverse engineer pieces of the FindMy network? That's great!
|
||||
The pages in this category aim to provide documentation on how various parts
|
||||
of the FindMy network have been reverse engineered, and how you can replicate this setup.
|
||||
|
||||
---
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
:glob:
|
||||
|
||||
*
|
||||
```
|
||||
40
docs/technical/10-Network.md
Normal file
40
docs/technical/10-Network.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# The Network
|
||||
|
||||
This page aims to provide an overview of how the Find My-network works on a technical level.
|
||||
It does this by explaining in detail what data the tags are broadcasting, how this is picked up
|
||||
by surrounding iDevices, and how generated location reports can be retrieved.
|
||||
|
||||
```{note}
|
||||
While official AirTags (and compatible 3rd party tags) use the same principles as described
|
||||
in this document, they also offer a key rotation feature. To learn more about
|
||||
how this works, please check out the dedicated [AirTags](#11-AirTags) page.
|
||||
```
|
||||
|
||||
## Overview
|
||||
|
||||
Simply said, the FindMy-network works by having accessories such as AirTags broadcast a unique signal over bluetooth.
|
||||
Any nearby iDevice, such as iPhones or iPads, are able to receive this signal. Once the device is aware of the nearby
|
||||
accessory, it will upload its current location to Apple's servers, from where it can be retrieved by the owner of the accessory.
|
||||
|
||||
Apple has put a lot of effort into making the network as private as possible. By utilizing encryption,
|
||||
it is possible for finder devices to encrypt their location with a key that the accessory is broadcasting
|
||||
before sending it to Apple. This public key allows encryption of certain data, but not decryption. In order
|
||||
to download location reports for the accessory, we therefore need the private key corresponding to said public key.
|
||||
These keys, together called a key pair, were generated and exchanged when the accessory was first paired and are now
|
||||
stored on the owner's devices. By downloading the correct encrypted location reports and then locally decrypting
|
||||
said reports using the private key, users are able to track their devices without Apple ever being able to read the location.
|
||||
|
||||

|
||||
|
||||
_An overview of the FindMy-network. Source: [SEEMOO-LAB](https://www.petsymposium.org/2021/files/papers/issue3/popets-2021-0045.pdf)_.
|
||||
|
||||
Fetching (encrypted) location reports still requires an Apple account. It is worth noting however, that anyone can download
|
||||
anyone else's location reports for any of their devices; however, due to the encryption scheme discussed above,
|
||||
doing this would be rather useless as none of the retrieved reports could be decrypted. This scheme allows devices
|
||||
to operate without being linked to a specific Apple account. The below dependency diagram visually explains how this entire
|
||||
system hinges on the availability of the private key; without it, location reports could not be retrieved and decrypted.
|
||||
|
||||

|
||||
|
||||
_A dependency diagram of data in the network. An arrow pointing from A to B means that in order
|
||||
to retrieve B, we first require A._
|
||||
3
docs/technical/11-AirTags.md
Normal file
3
docs/technical/11-AirTags.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# AirTags
|
||||
|
||||
TODO
|
||||
BIN
docs/technical/dependency_diagram.png
Normal file
BIN
docs/technical/dependency_diagram.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
17
docs/technical/index.md
Normal file
17
docs/technical/index.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Technical Documentation
|
||||
|
||||
This category serves as a place to provide technical documentation about the Find My network.
|
||||
More specifically, it serves as a technical reference for how certain features in this
|
||||
library have been implemented.
|
||||
|
||||
Most of the knowledge in this section has been sourced from other genius minds.
|
||||
Make sure to check out the references section on the specific pages to read more about the topics.
|
||||
|
||||
* * *
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
:glob:
|
||||
|
||||
*
|
||||
```
|
||||
@@ -41,7 +41,7 @@ class FindMyAccessoryMapping(TypedDict):
|
||||
|
||||
|
||||
class RollingKeyPairSource(ABC):
|
||||
"""A class that generates rolling `KeyPair`s."""
|
||||
"""A class that generates rolling :meth:`KeyPair`s."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
|
||||
@@ -21,5 +21,5 @@ class InvalidStateError(RuntimeError):
|
||||
"""
|
||||
Raised when a method is used that is in conflict with the internal account state.
|
||||
|
||||
For example: calling `BaseAppleAccount.login` while already logged in.
|
||||
For example: calling :meth:`BaseAppleAccount.login` while already logged in.
|
||||
"""
|
||||
|
||||
@@ -91,7 +91,7 @@ class HasPublicKey(HasHashedPublicKey, ABC):
|
||||
@property
|
||||
@override
|
||||
def hashed_adv_key_bytes(self) -> bytes:
|
||||
"""See `HasHashedPublicKey.hashed_adv_key_bytes`."""
|
||||
"""See :meth:`HasHashedPublicKey.hashed_adv_key_bytes`."""
|
||||
return hashlib.sha256(self.adv_key_bytes).digest()
|
||||
|
||||
@property
|
||||
@@ -136,7 +136,7 @@ class KeyPair(HasPublicKey, Serializable[KeyPairMapping]):
|
||||
key_type: KeyType = KeyType.UNKNOWN,
|
||||
name: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize the `KeyPair` with the private key bytes."""
|
||||
"""Initialize the :meth:`KeyPair` with the private key bytes."""
|
||||
priv_int = crypto.bytes_to_int(private_key)
|
||||
self._priv_key = ec.derive_private_key(
|
||||
priv_int,
|
||||
@@ -162,15 +162,15 @@ class KeyPair(HasPublicKey, Serializable[KeyPairMapping]):
|
||||
|
||||
@classmethod
|
||||
def new(cls) -> KeyPair:
|
||||
"""Generate a new random `KeyPair`."""
|
||||
"""Generate a new random :meth:`KeyPair`."""
|
||||
return cls(secrets.token_bytes(28))
|
||||
|
||||
@classmethod
|
||||
def from_b64(cls, key_b64: str) -> KeyPair:
|
||||
"""
|
||||
Import an existing `KeyPair` from its base64-encoded representation.
|
||||
Import an existing :meth:`KeyPair` from its base64-encoded representation.
|
||||
|
||||
Same format as returned by `KeyPair.private_key_b64`.
|
||||
Same format as returned by :meth:`KeyPair.private_key_b64`.
|
||||
"""
|
||||
return cls(base64.b64decode(key_b64))
|
||||
|
||||
@@ -185,7 +185,7 @@ class KeyPair(HasPublicKey, Serializable[KeyPairMapping]):
|
||||
"""
|
||||
Return the private key as a base64-encoded string.
|
||||
|
||||
Can be re-imported using `KeyPair.from_b64`.
|
||||
Can be re-imported using :meth:`KeyPair.from_b64`.
|
||||
"""
|
||||
return base64.b64encode(self.private_key_bytes).decode("ascii")
|
||||
|
||||
@@ -233,10 +233,10 @@ class KeyPair(HasPublicKey, Serializable[KeyPairMapping]):
|
||||
return f'KeyPair(name="{self.name}", public_key="{self.adv_key_b64}", type={self.key_type})'
|
||||
|
||||
|
||||
K = TypeVar("K")
|
||||
_K = TypeVar("_K")
|
||||
|
||||
|
||||
class KeyGenerator(ABC, Generic[K]):
|
||||
class KeyGenerator(ABC, Generic[_K]):
|
||||
"""KeyPair generator."""
|
||||
|
||||
@abstractmethod
|
||||
@@ -244,17 +244,17 @@ class KeyGenerator(ABC, Generic[K]):
|
||||
return NotImplemented
|
||||
|
||||
@abstractmethod
|
||||
def __next__(self) -> K:
|
||||
def __next__(self) -> _K:
|
||||
return NotImplemented
|
||||
|
||||
@overload
|
||||
@abstractmethod
|
||||
def __getitem__(self, val: int) -> K: ...
|
||||
def __getitem__(self, val: int) -> _K: ...
|
||||
|
||||
@overload
|
||||
@abstractmethod
|
||||
def __getitem__(self, val: slice) -> Generator[K, None, None]: ...
|
||||
def __getitem__(self, val: slice) -> Generator[_K, None, None]: ...
|
||||
|
||||
@abstractmethod
|
||||
def __getitem__(self, val: int | slice) -> K | Generator[K, None, None]:
|
||||
def __getitem__(self, val: int | slice) -> _K | Generator[_K, None, None]:
|
||||
return NotImplemented
|
||||
|
||||
@@ -31,7 +31,7 @@ def get_key() -> bytes:
|
||||
|
||||
def decrypt_plist(encrypted: str | Path | bytes | IO[bytes], key: bytes) -> dict:
|
||||
"""
|
||||
Decrypts the encrypted plist file at `encrypted` using the provided `key`.
|
||||
Decrypts the encrypted plist file at :meth:`encrypted` using the provided :meth:`key`.
|
||||
|
||||
:param encrypted: If bytes or IO, the encrypted plist data.
|
||||
If str or Path, the path to the encrypted plist file, which is
|
||||
|
||||
@@ -200,7 +200,7 @@ class BaseAppleAccount(Closable, Serializable[AccountStateMapping], ABC):
|
||||
"""
|
||||
Request a 2FA code to be sent to a specific phone number ID.
|
||||
|
||||
Consider using `BaseSecondFactorMethod.request` instead.
|
||||
Consider using :meth:`BaseSecondFactorMethod.request` instead.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -209,7 +209,7 @@ class BaseAppleAccount(Closable, Serializable[AccountStateMapping], ABC):
|
||||
"""
|
||||
Submit a 2FA code that was sent to a specific phone number ID.
|
||||
|
||||
Consider using `BaseSecondFactorMethod.submit` instead.
|
||||
Consider using :meth:`BaseSecondFactorMethod.submit` instead.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -218,7 +218,7 @@ class BaseAppleAccount(Closable, Serializable[AccountStateMapping], ABC):
|
||||
"""
|
||||
Request a 2FA code to be sent to a trusted device.
|
||||
|
||||
Consider using `BaseSecondFactorMethod.request` instead.
|
||||
Consider using :meth:`BaseSecondFactorMethod.request` instead.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -227,7 +227,7 @@ class BaseAppleAccount(Closable, Serializable[AccountStateMapping], ABC):
|
||||
"""
|
||||
Submit a 2FA code that was sent to a trusted device.
|
||||
|
||||
Consider using `BaseSecondFactorMethod.submit` instead.
|
||||
Consider using :meth:`BaseSecondFactorMethod.submit` instead.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -270,9 +270,9 @@ class BaseAppleAccount(Closable, Serializable[AccountStateMapping], ABC):
|
||||
list[LocationReport] | dict[HasHashedPublicKey | RollingKeyPairSource, list[LocationReport]]
|
||||
]:
|
||||
"""
|
||||
Fetch location reports for `HasHashedPublicKey`s between `date_from` and `date_end`.
|
||||
Fetch location reports for :class:`HasHashedPublicKey`s between `date_from` and `date_end`.
|
||||
|
||||
Returns a dictionary mapping `HasHashedPublicKey`s to a list of their location reports.
|
||||
Returns a dictionary mapping :class:`HasHashedPublicKey`s to their location reports.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -311,9 +311,9 @@ class BaseAppleAccount(Closable, Serializable[AccountStateMapping], ABC):
|
||||
list[LocationReport] | dict[HasHashedPublicKey | RollingKeyPairSource, list[LocationReport]]
|
||||
]:
|
||||
"""
|
||||
Fetch location reports for a sequence of `HasHashedPublicKey`s for the last `hours` hours.
|
||||
Fetch location reports for :class:`HasHashedPublicKey`s for the last `hours` hours.
|
||||
|
||||
Utility method as an alternative to using `BaseAppleAccount.fetch_reports` directly.
|
||||
Utility method as an alternative to using :meth:`BaseAppleAccount.fetch_reports` directly.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -326,13 +326,13 @@ class BaseAppleAccount(Closable, Serializable[AccountStateMapping], ABC):
|
||||
"""
|
||||
Retrieve a complete dictionary of Anisette headers.
|
||||
|
||||
Utility method for `AnisetteProvider.get_headers` using this account's user and device ID.
|
||||
Utility method for :meth:`AnisetteProvider.get_headers` using this account's user/device ID.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AsyncAppleAccount(BaseAppleAccount):
|
||||
"""An async implementation of `BaseAppleAccount`."""
|
||||
"""An async implementation of :meth:`BaseAppleAccount`."""
|
||||
|
||||
# auth endpoints
|
||||
_ENDPOINT_GSA = "https://gsa.apple.com/grandslam/GsService2"
|
||||
@@ -357,7 +357,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
||||
"""
|
||||
Initialize the apple account.
|
||||
|
||||
:param anisette: An instance of `AsyncAnisetteProvider`.
|
||||
:param anisette: An instance of :meth:`AsyncAnisetteProvider`.
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
@@ -401,7 +401,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
||||
@property
|
||||
@override
|
||||
def login_state(self) -> LoginState:
|
||||
"""See `BaseAppleAccount.login_state`."""
|
||||
"""See :meth:`BaseAppleAccount.login_state`."""
|
||||
return self._login_state
|
||||
|
||||
@property
|
||||
@@ -412,7 +412,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
||||
)
|
||||
@override
|
||||
def account_name(self) -> str | None:
|
||||
"""See `BaseAppleAccount.account_name`."""
|
||||
"""See :meth:`BaseAppleAccount.account_name`."""
|
||||
return self._account_info["account_name"] if self._account_info else None
|
||||
|
||||
@property
|
||||
@@ -423,7 +423,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
||||
)
|
||||
@override
|
||||
def first_name(self) -> str | None:
|
||||
"""See `BaseAppleAccount.first_name`."""
|
||||
"""See :meth:`BaseAppleAccount.first_name`."""
|
||||
return self._account_info["first_name"] if self._account_info else None
|
||||
|
||||
@property
|
||||
@@ -434,7 +434,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
||||
)
|
||||
@override
|
||||
def last_name(self) -> str | None:
|
||||
"""See `BaseAppleAccount.last_name`."""
|
||||
"""See :meth:`BaseAppleAccount.last_name`."""
|
||||
return self._account_info["last_name"] if self._account_info else None
|
||||
|
||||
@override
|
||||
@@ -501,7 +501,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
||||
@require_login_state(LoginState.LOGGED_OUT)
|
||||
@override
|
||||
async def login(self, username: str, password: str) -> LoginState:
|
||||
"""See `BaseAppleAccount.login`."""
|
||||
"""See :meth:`BaseAppleAccount.login`."""
|
||||
# LOGGED_OUT -> (REQUIRE_2FA or AUTHENTICATED)
|
||||
new_state = await self._gsa_authenticate(username, password)
|
||||
if new_state == LoginState.REQUIRE_2FA: # pass control back to handle 2FA
|
||||
@@ -513,7 +513,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
||||
@require_login_state(LoginState.REQUIRE_2FA)
|
||||
@override
|
||||
async def get_2fa_methods(self) -> Sequence[AsyncSecondFactorMethod]:
|
||||
"""See `BaseAppleAccount.get_2fa_methods`."""
|
||||
"""See :meth:`BaseAppleAccount.get_2fa_methods`."""
|
||||
methods: list[AsyncSecondFactorMethod] = []
|
||||
|
||||
if self._account_info is None:
|
||||
@@ -542,7 +542,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
||||
@require_login_state(LoginState.REQUIRE_2FA)
|
||||
@override
|
||||
async def sms_2fa_request(self, phone_number_id: int) -> None:
|
||||
"""See `BaseAppleAccount.sms_2fa_request`."""
|
||||
"""See :meth:`BaseAppleAccount.sms_2fa_request`."""
|
||||
data = {"phoneNumber": {"id": phone_number_id}, "mode": "sms"}
|
||||
|
||||
await self._sms_2fa_request(
|
||||
@@ -554,7 +554,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
||||
@require_login_state(LoginState.REQUIRE_2FA)
|
||||
@override
|
||||
async def sms_2fa_submit(self, phone_number_id: int, code: str) -> LoginState:
|
||||
"""See `BaseAppleAccount.sms_2fa_submit`."""
|
||||
"""See :meth:`BaseAppleAccount.sms_2fa_submit`."""
|
||||
data = {
|
||||
"phoneNumber": {"id": phone_number_id},
|
||||
"securityCode": {"code": str(code)},
|
||||
@@ -579,7 +579,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
||||
@require_login_state(LoginState.REQUIRE_2FA)
|
||||
@override
|
||||
async def td_2fa_request(self) -> None:
|
||||
"""See `BaseAppleAccount.td_2fa_request`."""
|
||||
"""See :meth:`BaseAppleAccount.td_2fa_request`."""
|
||||
headers = {
|
||||
"Content-Type": "text/x-xml-plist",
|
||||
"Accept": "text/x-xml-plist",
|
||||
@@ -593,7 +593,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
||||
@require_login_state(LoginState.REQUIRE_2FA)
|
||||
@override
|
||||
async def td_2fa_submit(self, code: str) -> LoginState:
|
||||
"""See `BaseAppleAccount.td_2fa_submit`."""
|
||||
"""See :meth:`BaseAppleAccount.td_2fa_submit`."""
|
||||
headers = {
|
||||
"security-code": code,
|
||||
"Content-Type": "text/x-xml-plist",
|
||||
@@ -717,7 +717,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
||||
) -> (
|
||||
list[LocationReport] | dict[HasHashedPublicKey | RollingKeyPairSource, list[LocationReport]]
|
||||
):
|
||||
"""See `BaseAppleAccount.fetch_reports`."""
|
||||
"""See :meth:`BaseAppleAccount.fetch_reports`."""
|
||||
date_to = date_to or datetime.now().astimezone()
|
||||
|
||||
return await self._reports.fetch_reports(
|
||||
@@ -758,7 +758,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
||||
) -> (
|
||||
list[LocationReport] | dict[HasHashedPublicKey | RollingKeyPairSource, list[LocationReport]]
|
||||
):
|
||||
"""See `BaseAppleAccount.fetch_last_reports`."""
|
||||
"""See :meth:`BaseAppleAccount.fetch_last_reports`."""
|
||||
end = datetime.now(tz=timezone.utc)
|
||||
start = end - timedelta(hours=hours)
|
||||
|
||||
@@ -971,15 +971,15 @@ class AsyncAppleAccount(BaseAppleAccount):
|
||||
with_client_info: bool = False,
|
||||
serial: str = "0",
|
||||
) -> dict[str, str]:
|
||||
"""See `BaseAppleAccount.get_anisette_headers`."""
|
||||
"""See :meth:`BaseAppleAccount.get_anisette_headers`."""
|
||||
return await self._anisette.get_headers(self._uid, self._devid, serial, with_client_info)
|
||||
|
||||
|
||||
class AppleAccount(BaseAppleAccount):
|
||||
"""
|
||||
A sync implementation of `BaseappleAccount`.
|
||||
A sync implementation of :meth:`BaseappleAccount`.
|
||||
|
||||
Uses `AsyncappleAccount` internally.
|
||||
Uses :meth:`AsyncappleAccount` internally.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -988,7 +988,7 @@ class AppleAccount(BaseAppleAccount):
|
||||
*,
|
||||
state_info: AccountStateMapping | None = None,
|
||||
) -> None:
|
||||
"""See `AsyncAppleAccount.__init__`."""
|
||||
"""See :meth:`AsyncAppleAccount.__init__`."""
|
||||
self._asyncacc = AsyncAppleAccount(anisette=anisette, state_info=state_info)
|
||||
|
||||
try:
|
||||
@@ -1001,31 +1001,31 @@ class AppleAccount(BaseAppleAccount):
|
||||
|
||||
@override
|
||||
async def close(self) -> None:
|
||||
"""See `AsyncAppleAccount.close`."""
|
||||
"""See :meth:`AsyncAppleAccount.close`."""
|
||||
await self._asyncacc.close()
|
||||
|
||||
@property
|
||||
@override
|
||||
def login_state(self) -> LoginState:
|
||||
"""See `AsyncAppleAccount.login_state`."""
|
||||
"""See :meth:`AsyncAppleAccount.login_state`."""
|
||||
return self._asyncacc.login_state
|
||||
|
||||
@property
|
||||
@override
|
||||
def account_name(self) -> str | None:
|
||||
"""See `AsyncAppleAccount.login_state`."""
|
||||
"""See :meth:`AsyncAppleAccount.login_state`."""
|
||||
return self._asyncacc.account_name
|
||||
|
||||
@property
|
||||
@override
|
||||
def first_name(self) -> str | None:
|
||||
"""See `AsyncAppleAccount.first_name`."""
|
||||
"""See :meth:`AsyncAppleAccount.first_name`."""
|
||||
return self._asyncacc.first_name
|
||||
|
||||
@property
|
||||
@override
|
||||
def last_name(self) -> str | None:
|
||||
"""See `AsyncAppleAccount.last_name`."""
|
||||
"""See :meth:`AsyncAppleAccount.last_name`."""
|
||||
return self._asyncacc.last_name
|
||||
|
||||
@override
|
||||
@@ -1051,13 +1051,13 @@ class AppleAccount(BaseAppleAccount):
|
||||
|
||||
@override
|
||||
def login(self, username: str, password: str) -> LoginState:
|
||||
"""See `AsyncAppleAccount.login`."""
|
||||
"""See :meth:`AsyncAppleAccount.login`."""
|
||||
coro = self._asyncacc.login(username, password)
|
||||
return self._evt_loop.run_until_complete(coro)
|
||||
|
||||
@override
|
||||
def get_2fa_methods(self) -> Sequence[SyncSecondFactorMethod]:
|
||||
"""See `AsyncAppleAccount.get_2fa_methods`."""
|
||||
"""See :meth:`AsyncAppleAccount.get_2fa_methods`."""
|
||||
coro = self._asyncacc.get_2fa_methods()
|
||||
methods = self._evt_loop.run_until_complete(coro)
|
||||
|
||||
@@ -1078,25 +1078,25 @@ class AppleAccount(BaseAppleAccount):
|
||||
|
||||
@override
|
||||
def sms_2fa_request(self, phone_number_id: int) -> None:
|
||||
"""See `AsyncAppleAccount.sms_2fa_request`."""
|
||||
"""See :meth:`AsyncAppleAccount.sms_2fa_request`."""
|
||||
coro = self._asyncacc.sms_2fa_request(phone_number_id)
|
||||
return self._evt_loop.run_until_complete(coro)
|
||||
|
||||
@override
|
||||
def sms_2fa_submit(self, phone_number_id: int, code: str) -> LoginState:
|
||||
"""See `AsyncAppleAccount.sms_2fa_submit`."""
|
||||
"""See :meth:`AsyncAppleAccount.sms_2fa_submit`."""
|
||||
coro = self._asyncacc.sms_2fa_submit(phone_number_id, code)
|
||||
return self._evt_loop.run_until_complete(coro)
|
||||
|
||||
@override
|
||||
def td_2fa_request(self) -> None:
|
||||
"""See `AsyncAppleAccount.td_2fa_request`."""
|
||||
"""See :meth:`AsyncAppleAccount.td_2fa_request`."""
|
||||
coro = self._asyncacc.td_2fa_request()
|
||||
return self._evt_loop.run_until_complete(coro)
|
||||
|
||||
@override
|
||||
def td_2fa_submit(self, code: str) -> LoginState:
|
||||
"""See `AsyncAppleAccount.td_2fa_submit`."""
|
||||
"""See :meth:`AsyncAppleAccount.td_2fa_submit`."""
|
||||
coro = self._asyncacc.td_2fa_submit(code)
|
||||
return self._evt_loop.run_until_complete(coro)
|
||||
|
||||
@@ -1135,7 +1135,7 @@ class AppleAccount(BaseAppleAccount):
|
||||
) -> (
|
||||
list[LocationReport] | dict[HasHashedPublicKey | RollingKeyPairSource, list[LocationReport]]
|
||||
):
|
||||
"""See `AsyncAppleAccount.fetch_reports`."""
|
||||
"""See :meth:`AsyncAppleAccount.fetch_reports`."""
|
||||
coro = self._asyncacc.fetch_reports(keys, date_from, date_to)
|
||||
return self._evt_loop.run_until_complete(coro)
|
||||
|
||||
@@ -1170,7 +1170,7 @@ class AppleAccount(BaseAppleAccount):
|
||||
) -> (
|
||||
list[LocationReport] | dict[HasHashedPublicKey | RollingKeyPairSource, list[LocationReport]]
|
||||
):
|
||||
"""See `AsyncAppleAccount.fetch_last_reports`."""
|
||||
"""See :meth:`AsyncAppleAccount.fetch_last_reports`."""
|
||||
coro = self._asyncacc.fetch_last_reports(keys, hours)
|
||||
return self._evt_loop.run_until_complete(coro)
|
||||
|
||||
@@ -1180,6 +1180,6 @@ class AppleAccount(BaseAppleAccount):
|
||||
with_client_info: bool = False,
|
||||
serial: str = "0",
|
||||
) -> dict[str, str]:
|
||||
"""See `AsyncAppleAccount.get_anisette_headers`."""
|
||||
"""See :meth:`AsyncAppleAccount.get_anisette_headers`."""
|
||||
coro = self._asyncacc.get_anisette_headers(with_client_info, serial)
|
||||
return self._evt_loop.run_until_complete(coro)
|
||||
|
||||
@@ -135,7 +135,7 @@ class BaseAnisetteProvider(Closable, Serializable, ABC):
|
||||
"""
|
||||
Generate a complete dictionary of Anisette headers.
|
||||
|
||||
Consider using `BaseAppleAccount.get_anisette_headers` instead.
|
||||
Consider using :meth:`BaseAppleAccount.get_anisette_headers` instead.
|
||||
"""
|
||||
headers = {
|
||||
# Current Time
|
||||
@@ -207,7 +207,7 @@ class RemoteAnisetteProvider(BaseAnisetteProvider, Serializable[RemoteAnisetteMa
|
||||
|
||||
@override
|
||||
def to_json(self, dst: str | Path | None = None, /) -> RemoteAnisetteMapping:
|
||||
"""See `BaseAnisetteProvider.serialize`."""
|
||||
"""See :meth:`BaseAnisetteProvider.serialize`."""
|
||||
return save_and_return_json(
|
||||
{
|
||||
"type": "aniRemote",
|
||||
@@ -219,7 +219,7 @@ class RemoteAnisetteProvider(BaseAnisetteProvider, Serializable[RemoteAnisetteMa
|
||||
@classmethod
|
||||
@override
|
||||
def from_json(cls, val: str | Path | RemoteAnisetteMapping) -> RemoteAnisetteProvider:
|
||||
"""See `BaseAnisetteProvider.deserialize`."""
|
||||
"""See :meth:`BaseAnisetteProvider.deserialize`."""
|
||||
val = read_data_json(val)
|
||||
|
||||
assert val["type"] == "aniRemote"
|
||||
@@ -231,7 +231,7 @@ class RemoteAnisetteProvider(BaseAnisetteProvider, Serializable[RemoteAnisetteMa
|
||||
@property
|
||||
@override
|
||||
def otp(self) -> str:
|
||||
"""See `BaseAnisetteProvider.otp`_."""
|
||||
"""See :meth:`BaseAnisetteProvider.otp`."""
|
||||
otp = (self._anisette_data or {}).get("X-Apple-I-MD")
|
||||
if otp is None:
|
||||
logger.warning("X-Apple-I-MD header not found! Returning fallback...")
|
||||
@@ -240,7 +240,7 @@ class RemoteAnisetteProvider(BaseAnisetteProvider, Serializable[RemoteAnisetteMa
|
||||
@property
|
||||
@override
|
||||
def machine(self) -> str:
|
||||
"""See `BaseAnisetteProvider.machine`_."""
|
||||
"""See :meth:`BaseAnisetteProvider.machine`."""
|
||||
machine = (self._anisette_data or {}).get("X-Apple-I-MD-M")
|
||||
if machine is None:
|
||||
logger.warning("X-Apple-I-MD-M header not found! Returning fallback...")
|
||||
@@ -254,7 +254,7 @@ class RemoteAnisetteProvider(BaseAnisetteProvider, Serializable[RemoteAnisetteMa
|
||||
serial: str = "0",
|
||||
with_client_info: bool = False,
|
||||
) -> dict[str, str]:
|
||||
"""See `BaseAnisetteProvider.get_headers`_."""
|
||||
"""See :meth::meth:`BaseAnisetteProvider.get_headers`."""
|
||||
if self._closed:
|
||||
msg = "RemoteAnisetteProvider has been closed and cannot be used"
|
||||
raise RuntimeError(msg)
|
||||
@@ -270,7 +270,7 @@ class RemoteAnisetteProvider(BaseAnisetteProvider, Serializable[RemoteAnisetteMa
|
||||
|
||||
@override
|
||||
async def close(self) -> None:
|
||||
"""See `AnisetteProvider.close`."""
|
||||
"""See :meth:`AnisetteProvider.close`."""
|
||||
if self._closed:
|
||||
return # Already closed, make it idempotent
|
||||
|
||||
@@ -283,7 +283,7 @@ class RemoteAnisetteProvider(BaseAnisetteProvider, Serializable[RemoteAnisetteMa
|
||||
|
||||
|
||||
class LocalAnisetteProvider(BaseAnisetteProvider, Serializable[LocalAnisetteMapping]):
|
||||
"""Anisette provider. Generates headers without a remote server using the `anisette` library."""
|
||||
"""Local anisette provider using the `anisette` library."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -328,7 +328,7 @@ class LocalAnisetteProvider(BaseAnisetteProvider, Serializable[LocalAnisetteMapp
|
||||
|
||||
@override
|
||||
def to_json(self, dst: str | Path | None = None, /) -> LocalAnisetteMapping:
|
||||
"""See `BaseAnisetteProvider.serialize`."""
|
||||
"""See :meth:`BaseAnisetteProvider.serialize`."""
|
||||
with BytesIO() as buf:
|
||||
self._ani.save_provisioning(buf)
|
||||
prov_data = base64.b64encode(buf.getvalue()).decode("utf-8")
|
||||
@@ -349,7 +349,7 @@ class LocalAnisetteProvider(BaseAnisetteProvider, Serializable[LocalAnisetteMapp
|
||||
*,
|
||||
libs_path: str | Path | None = None,
|
||||
) -> LocalAnisetteProvider:
|
||||
"""See `BaseAnisetteProvider.deserialize`."""
|
||||
"""See :meth:`BaseAnisetteProvider.deserialize`."""
|
||||
val = read_data_json(val)
|
||||
|
||||
assert val["type"] == "aniLocal"
|
||||
@@ -366,7 +366,7 @@ class LocalAnisetteProvider(BaseAnisetteProvider, Serializable[LocalAnisetteMapp
|
||||
serial: str = "0",
|
||||
with_client_info: bool = False,
|
||||
) -> dict[str, str]:
|
||||
"""See `BaseAnisetteProvider.get_headers`_."""
|
||||
"""See :meth:`BaseAnisetteProvider.get_headers`."""
|
||||
self._ani_data = self._ani.get_data()
|
||||
|
||||
return await super().get_headers(user_id, device_id, serial, with_client_info)
|
||||
@@ -374,7 +374,7 @@ class LocalAnisetteProvider(BaseAnisetteProvider, Serializable[LocalAnisetteMapp
|
||||
@property
|
||||
@override
|
||||
def otp(self) -> str:
|
||||
"""See `BaseAnisetteProvider.otp`_."""
|
||||
"""See :meth:`BaseAnisetteProvider.otp`."""
|
||||
machine = (self._ani_data or {}).get("X-Apple-I-MD")
|
||||
if machine is None:
|
||||
logger.warning("X-Apple-I-MD header not found! Returning fallback...")
|
||||
@@ -383,7 +383,7 @@ class LocalAnisetteProvider(BaseAnisetteProvider, Serializable[LocalAnisetteMapp
|
||||
@property
|
||||
@override
|
||||
def machine(self) -> str:
|
||||
"""See `BaseAnisetteProvider.machine`_."""
|
||||
"""See :meth:`BaseAnisetteProvider.machine`."""
|
||||
machine = (self._ani_data or {}).get("X-Apple-I-MD-M")
|
||||
if machine is None:
|
||||
logger.warning("X-Apple-I-MD-M header not found! Returning fallback...")
|
||||
@@ -391,4 +391,4 @@ class LocalAnisetteProvider(BaseAnisetteProvider, Serializable[LocalAnisetteMapp
|
||||
|
||||
@override
|
||||
async def close(self) -> None:
|
||||
"""See `BaseAnisetteProvider.close`_."""
|
||||
"""See :meth:`BaseAnisetteProvider.close`."""
|
||||
|
||||
@@ -52,14 +52,18 @@ LocationReportMapping = Union[LocationReportEncryptedMapping, LocationReportDecr
|
||||
|
||||
|
||||
class LocationReport(HasHashedPublicKey, Serializable[LocationReportMapping]):
|
||||
"""Location report corresponding to a certain `HasHashedPublicKey`."""
|
||||
"""Location report corresponding to a certain :meth:`HasHashedPublicKey`."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
payload: bytes,
|
||||
hashed_adv_key: bytes,
|
||||
) -> None:
|
||||
"""Initialize a `KeyReport`. You should probably use `KeyReport.from_payload` instead."""
|
||||
"""
|
||||
Initialize a :class:`LocationReport`.
|
||||
|
||||
You should probably use :meth:`LocationReport.from_payload` instead.
|
||||
"""
|
||||
self._payload: bytes = payload
|
||||
self._hashed_adv_key: bytes = hashed_adv_key
|
||||
|
||||
@@ -68,7 +72,7 @@ class LocationReport(HasHashedPublicKey, Serializable[LocationReportMapping]):
|
||||
@property
|
||||
@override
|
||||
def hashed_adv_key_bytes(self) -> bytes:
|
||||
"""See `HasHashedPublicKey.hashed_adv_key_bytes`."""
|
||||
"""See :meth:`HasHashedPublicKey.hashed_adv_key_bytes`."""
|
||||
return self._hashed_adv_key
|
||||
|
||||
@property
|
||||
@@ -96,7 +100,7 @@ class LocationReport(HasHashedPublicKey, Serializable[LocationReportMapping]):
|
||||
return key.hashed_adv_key_bytes == self._hashed_adv_key
|
||||
|
||||
def decrypt(self, key: KeyPair) -> None:
|
||||
"""Decrypt the report using its corresponding `KeyPair`."""
|
||||
"""Decrypt the report using its corresponding :meth:`KeyPair`."""
|
||||
if not self.can_decrypt(key):
|
||||
msg = "Cannot decrypt with this key!"
|
||||
raise ValueError(msg)
|
||||
@@ -136,7 +140,7 @@ class LocationReport(HasHashedPublicKey, Serializable[LocationReportMapping]):
|
||||
|
||||
@property
|
||||
def timestamp(self) -> datetime:
|
||||
"""The `datetime` when this report was recorded by a device."""
|
||||
"""The :meth:`datetime` when this report was recorded by a device."""
|
||||
timestamp_int = int.from_bytes(self._payload[0:4], "big") + (60 * 60 * 24 * 11323)
|
||||
return datetime.fromtimestamp(timestamp_int, tz=timezone.utc).astimezone()
|
||||
|
||||
@@ -302,9 +306,9 @@ class LocationReport(HasHashedPublicKey, Serializable[LocationReportMapping]):
|
||||
|
||||
def __lt__(self, other: LocationReport) -> bool:
|
||||
"""
|
||||
Compare against another `KeyReport`.
|
||||
Compare against another :meth:`KeyReport`.
|
||||
|
||||
A `KeyReport` is said to be "less than" another `KeyReport` iff its recorded
|
||||
A :meth:`KeyReport` is said to be "less than" another :meth:`KeyReport` iff its recorded
|
||||
timestamp is strictly less than the other report.
|
||||
"""
|
||||
if isinstance(other, LocationReport):
|
||||
@@ -369,12 +373,12 @@ class LocationReportsFetcher:
|
||||
"""
|
||||
Fetch location reports for a certain device.
|
||||
|
||||
When ``device`` is a single :class:`.HasHashedPublicKey`, this method will return
|
||||
When `device` is a single :class:`HasHashedPublicKey`, this method will return
|
||||
a list of location reports corresponding to that key.
|
||||
When ``device`` is a :class:`.RollingKeyPairSource`, it will return a list of
|
||||
When `device` is a :class:`RollingKeyPairSource`, it will return a list of
|
||||
location reports corresponding to that source.
|
||||
When ``device`` is a sequence of :class:`.HasHashedPublicKey`s or RollingKeyPairSource's,
|
||||
it will return a dictionary with the :class:`.HasHashedPublicKey` or `.RollingKeyPairSource`
|
||||
When `device` is a sequence of :class:`HasHashedPublicKey`s or RollingKeyPairSource's,
|
||||
it will return a dictionary with the provided object
|
||||
as key, and a list of location reports as value.
|
||||
"""
|
||||
key_devs: dict[HasHashedPublicKey, HasHashedPublicKey | RollingKeyPairSource] = {}
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing_extensions import override
|
||||
|
||||
|
||||
class LoginState(Enum):
|
||||
"""Enum of possible login states. Used for `AppleAccount`'s internal state machine."""
|
||||
"""Enum of possible login states. Used for :meth:`AppleAccount`'s internal state machine."""
|
||||
|
||||
LOGGED_OUT = 0
|
||||
REQUIRE_2FA = 1
|
||||
@@ -15,9 +15,9 @@ class LoginState(Enum):
|
||||
|
||||
def __lt__(self, other: "LoginState") -> bool:
|
||||
"""
|
||||
Compare against another `LoginState`.
|
||||
Compare against another :meth:`LoginState`.
|
||||
|
||||
A `LoginState` is said to be "less than" another `LoginState` iff it is in
|
||||
A :meth:`LoginState` is said to be "less than" another :meth:`LoginState` iff it is in
|
||||
an "earlier" stage of the login process, going from LOGGED_OUT to LOGGED_IN.
|
||||
"""
|
||||
if isinstance(other, LoginState):
|
||||
|
||||
@@ -63,13 +63,13 @@ class AsyncSecondFactorMethod(BaseSecondFactorMethod, ABC):
|
||||
@override
|
||||
@abstractmethod
|
||||
async def request(self) -> None:
|
||||
"""See `BaseSecondFactorMethod.request`."""
|
||||
"""See :meth:`BaseSecondFactorMethod.request`."""
|
||||
raise NotImplementedError
|
||||
|
||||
@override
|
||||
@abstractmethod
|
||||
async def submit(self, code: str) -> LoginState:
|
||||
"""See `BaseSecondFactorMethod.submit`."""
|
||||
"""See :meth:`BaseSecondFactorMethod.submit`."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -93,13 +93,13 @@ class SyncSecondFactorMethod(BaseSecondFactorMethod, ABC):
|
||||
@override
|
||||
@abstractmethod
|
||||
def request(self) -> None:
|
||||
"""See `BaseSecondFactorMethod.request`."""
|
||||
"""See :meth:`BaseSecondFactorMethod.request`."""
|
||||
raise NotImplementedError
|
||||
|
||||
@override
|
||||
@abstractmethod
|
||||
def submit(self, code: str) -> LoginState:
|
||||
"""See `BaseSecondFactorMethod.submit`."""
|
||||
"""See :meth:`BaseSecondFactorMethod.submit`."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ class TrustedDeviceSecondFactorMethod(BaseSecondFactorMethod, ABC):
|
||||
|
||||
|
||||
class AsyncSmsSecondFactor(AsyncSecondFactorMethod, SmsSecondFactorMethod):
|
||||
"""An async implementation of `SmsSecondFactorMethod`."""
|
||||
"""An async implementation of :meth:`SmsSecondFactorMethod`."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -139,7 +139,7 @@ class AsyncSmsSecondFactor(AsyncSecondFactorMethod, SmsSecondFactorMethod):
|
||||
"""
|
||||
Initialize the second factor method.
|
||||
|
||||
Should not be done manually; use `AsyncAppleAccount.get_2fa_methods` instead.
|
||||
Should not be done manually; use :meth:`AsyncAppleAccount.get_2fa_methods` instead.
|
||||
"""
|
||||
super().__init__(account)
|
||||
|
||||
@@ -174,7 +174,7 @@ class AsyncSmsSecondFactor(AsyncSecondFactorMethod, SmsSecondFactorMethod):
|
||||
|
||||
|
||||
class SyncSmsSecondFactor(SyncSecondFactorMethod, SmsSecondFactorMethod):
|
||||
"""A sync implementation of `SmsSecondFactorMethod`."""
|
||||
"""A sync implementation of :meth:`SmsSecondFactorMethod`."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -182,7 +182,7 @@ class SyncSmsSecondFactor(SyncSecondFactorMethod, SmsSecondFactorMethod):
|
||||
number_id: int,
|
||||
phone_number: str,
|
||||
) -> None:
|
||||
"""See `AsyncSmsSecondFactor.__init__`."""
|
||||
"""See :meth:`AsyncSmsSecondFactor.__init__`."""
|
||||
super().__init__(account)
|
||||
|
||||
self._phone_number_id: int = number_id
|
||||
@@ -191,28 +191,28 @@ class SyncSmsSecondFactor(SyncSecondFactorMethod, SmsSecondFactorMethod):
|
||||
@property
|
||||
@override
|
||||
def phone_number_id(self) -> int:
|
||||
"""See `AsyncSmsSecondFactor.phone_number_id`."""
|
||||
"""See :meth:`AsyncSmsSecondFactor.phone_number_id`."""
|
||||
return self._phone_number_id
|
||||
|
||||
@property
|
||||
@override
|
||||
def phone_number(self) -> str:
|
||||
"""See `AsyncSmsSecondFactor.phone_number`."""
|
||||
"""See :meth:`AsyncSmsSecondFactor.phone_number`."""
|
||||
return self._phone_number
|
||||
|
||||
@override
|
||||
def request(self) -> None:
|
||||
"""See `AsyncSmsSecondFactor.request`."""
|
||||
"""See :meth:`AsyncSmsSecondFactor.request`."""
|
||||
return self.account.sms_2fa_request(self._phone_number_id)
|
||||
|
||||
@override
|
||||
def submit(self, code: str) -> LoginState:
|
||||
"""See `AsyncSmsSecondFactor.submit`."""
|
||||
"""See :meth:`AsyncSmsSecondFactor.submit`."""
|
||||
return self.account.sms_2fa_submit(self._phone_number_id, code)
|
||||
|
||||
|
||||
class AsyncTrustedDeviceSecondFactor(AsyncSecondFactorMethod, TrustedDeviceSecondFactorMethod):
|
||||
"""An async implementation of `TrustedDeviceSecondFactorMethod`."""
|
||||
"""An async implementation of :meth:`TrustedDeviceSecondFactorMethod`."""
|
||||
|
||||
@override
|
||||
async def request(self) -> None:
|
||||
@@ -224,14 +224,14 @@ class AsyncTrustedDeviceSecondFactor(AsyncSecondFactorMethod, TrustedDeviceSecon
|
||||
|
||||
|
||||
class SyncTrustedDeviceSecondFactor(SyncSecondFactorMethod, TrustedDeviceSecondFactorMethod):
|
||||
"""A sync implementation of `TrustedDeviceSecondFactorMethod`."""
|
||||
"""A sync implementation of :meth:`TrustedDeviceSecondFactorMethod`."""
|
||||
|
||||
@override
|
||||
def request(self) -> None:
|
||||
"""See `AsyncTrustedDeviceSecondFactor.request`."""
|
||||
"""See :meth:`AsyncTrustedDeviceSecondFactor.request`."""
|
||||
return self.account.td_2fa_request()
|
||||
|
||||
@override
|
||||
def submit(self, code: str) -> LoginState:
|
||||
"""See `AsyncTrustedDeviceSecondFactor.submit`."""
|
||||
"""See :meth:`AsyncTrustedDeviceSecondFactor.submit`."""
|
||||
return self.account.td_2fa_submit(code)
|
||||
|
||||
@@ -211,7 +211,7 @@ class SeparatedOfflineFindingDevice(OfflineFindingDevice, HasPublicKey):
|
||||
detected_at: datetime,
|
||||
additional_data: dict[Any, Any] | None = None,
|
||||
) -> None:
|
||||
"""Initialize a `SeparatedOfflineFindingDevice`."""
|
||||
"""Initialize a :meth:`SeparatedOfflineFindingDevice`."""
|
||||
super().__init__(mac_bytes, status, detected_at, additional_data)
|
||||
|
||||
self._public_key: bytes = public_key
|
||||
@@ -225,7 +225,7 @@ class SeparatedOfflineFindingDevice(OfflineFindingDevice, HasPublicKey):
|
||||
@property
|
||||
@override
|
||||
def adv_key_bytes(self) -> bytes:
|
||||
"""See `HasPublicKey.adv_key_bytes`."""
|
||||
"""See :meth:`HasPublicKey.adv_key_bytes`."""
|
||||
return self._public_key
|
||||
|
||||
@override
|
||||
@@ -300,7 +300,7 @@ _DEVICE_TYPES = {
|
||||
|
||||
|
||||
class OfflineFindingScanner:
|
||||
"""BLE scanner that searches for `OfflineFindingDevice`s."""
|
||||
"""BLE scanner that searches for :meth:`OfflineFindingDevice`s."""
|
||||
|
||||
_scan_ctrl_lock = asyncio.Lock()
|
||||
|
||||
@@ -311,7 +311,7 @@ class OfflineFindingScanner:
|
||||
Initialize an instance of the Scanner using an event loop.
|
||||
|
||||
You most likely do not want to use this yourself;
|
||||
check out `OfflineFindingScanner.create` instead.
|
||||
check out :meth:`OfflineFindingScanner.create` instead.
|
||||
"""
|
||||
self._scanner: BleakScanner = BleakScanner(self._scan_callback, cb={"use_bdaddr": True})
|
||||
|
||||
@@ -377,10 +377,10 @@ class OfflineFindingScanner:
|
||||
extend_timeout: bool = False,
|
||||
) -> AsyncGenerator[OfflineFindingDevice, None]:
|
||||
"""
|
||||
Scan for `OfflineFindingDevice`s for up to `timeout` seconds.
|
||||
Scan for :meth:`OfflineFindingDevice`s for up to :meth:`timeout` seconds.
|
||||
|
||||
If `extend_timeout` is set, the timer will be extended
|
||||
by `timeout` seconds every time a new device is discovered.
|
||||
If :meth:`extend_timeout` is set, the timer will be extended
|
||||
by :meth:`timeout` seconds every time a new device is discovered.
|
||||
"""
|
||||
await self._start_scan()
|
||||
|
||||
|
||||
@@ -19,9 +19,9 @@ class Closable(ABC):
|
||||
|
||||
def __init__(self, loop: asyncio.AbstractEventLoop | None = None) -> None:
|
||||
"""
|
||||
Initialize the ``Closable``.
|
||||
Initialize the :class:`Closable`.
|
||||
|
||||
If an event loop is given, the ``Closable`` will attempt to close itself
|
||||
If an event loop is given, the :class:`Closable` will attempt to close itself
|
||||
using the loop when it is garbage collected.
|
||||
"""
|
||||
self._loop: asyncio.AbstractEventLoop | None = loop
|
||||
@@ -57,7 +57,7 @@ class Serializable(Generic[_T], ABC):
|
||||
If an argument is provided, the output will also be written to that file.
|
||||
|
||||
The output of this method is guaranteed to be JSON-serializable, and passing
|
||||
the return value of this function as an argument to `Serializable.from_json`
|
||||
the return value of this function as an argument to :meth:`Serializable.from_json`
|
||||
will always result in an exact copy of the internal state as it was when exported.
|
||||
|
||||
You are encouraged to save and load object states to and from disk whenever possible,
|
||||
@@ -69,11 +69,11 @@ class Serializable(Generic[_T], ABC):
|
||||
@abstractmethod
|
||||
def from_json(cls, val: str | Path | _T, /) -> Self:
|
||||
"""
|
||||
Restore state from a previous `Closable.to_json` export.
|
||||
Restore state from a previous :meth:`Closable.to_json` export.
|
||||
|
||||
If given a str or Path, it must point to a json file from `Serializable.to_json`.
|
||||
If given a str or Path, it must point to a json file from :meth:`Serializable.to_json`.
|
||||
Otherwise, it should be the Mapping itself.
|
||||
|
||||
See `Serializable.to_json` for more information.
|
||||
See :meth:`Serializable.to_json` for more information.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -7,10 +7,10 @@ from collections.abc import Mapping
|
||||
from pathlib import Path
|
||||
from typing import TypeVar, cast
|
||||
|
||||
T = TypeVar("T", bound=Mapping)
|
||||
_T = TypeVar("_T", bound=Mapping)
|
||||
|
||||
|
||||
def save_and_return_json(data: T, dst: str | Path | None) -> T:
|
||||
def save_and_return_json(data: _T, dst: str | Path | None) -> _T:
|
||||
"""Save and return a JSON-serializable data structure."""
|
||||
if dst is None:
|
||||
return data
|
||||
@@ -23,12 +23,12 @@ def save_and_return_json(data: T, dst: str | Path | None) -> T:
|
||||
return data
|
||||
|
||||
|
||||
def read_data_json(val: str | Path | T) -> T:
|
||||
def read_data_json(val: str | Path | _T) -> _T:
|
||||
"""Read JSON data from a file if a path is passed, or return the argument itself."""
|
||||
if isinstance(val, str):
|
||||
val = Path(val)
|
||||
|
||||
if isinstance(val, Path):
|
||||
val = cast("T", json.loads(val.read_text()))
|
||||
val = cast("_T", json.loads(val.read_text()))
|
||||
|
||||
return val
|
||||
|
||||
@@ -33,7 +33,7 @@ class _HttpRequestOptions(_RequestOptions, total=False):
|
||||
|
||||
|
||||
class HttpResponse:
|
||||
"""Response of a request made by `HttpSession`."""
|
||||
"""Response of a request made by :meth:`HttpSession`."""
|
||||
|
||||
def __init__(self, status_code: int, content: bytes) -> None:
|
||||
"""Initialize the response."""
|
||||
@@ -115,7 +115,7 @@ class HttpSession(Closable):
|
||||
"""
|
||||
Make an HTTP request.
|
||||
|
||||
Keyword arguments will directly be passed to `aiohttp.ClientSession.request`.
|
||||
Keyword arguments will directly be passed to :meth:`aiohttp.ClientSession.request`.
|
||||
"""
|
||||
session = await self._get_session()
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
from collections.abc import Coroutine
|
||||
from typing import TypeVar, Union
|
||||
|
||||
T = TypeVar("T")
|
||||
_T = TypeVar("_T")
|
||||
|
||||
# Cannot use `|` operator (PEP 604) in python 3.9,
|
||||
# even with __future__ import since it is evaluated directly
|
||||
MaybeCoro = Union[T, Coroutine[None, None, T]]
|
||||
MaybeCoro = Union[_T, Coroutine[None, None, _T]]
|
||||
|
||||
@@ -28,7 +28,13 @@ dev = [
|
||||
"packaging>=25.0,<26.0",
|
||||
]
|
||||
test = ["pytest>=8.3.2,<9.0.0"]
|
||||
docs = ["sphinx>=8.2.3,<8.3.0", "sphinx-autoapi==3.6.0"]
|
||||
docs = [
|
||||
"furo>=2025.7.19",
|
||||
"myst-parser>=4.0.1",
|
||||
"sphinx>=8.2.3,<8.3.0",
|
||||
"sphinx-autoapi==3.6.0",
|
||||
"sphinx-book-theme>=1.1.4",
|
||||
]
|
||||
|
||||
[tool.pyright]
|
||||
venvPath = "."
|
||||
|
||||
15
shell.nix
15
shell.nix
@@ -1,18 +1,21 @@
|
||||
{ pkgs ? import <nixpkgs> {} }:
|
||||
{
|
||||
pkgs ? import <nixpkgs> { },
|
||||
}:
|
||||
|
||||
let
|
||||
unstable = import (fetchTarball https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz) { };
|
||||
unstable = import (fetchTarball "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz") { };
|
||||
in
|
||||
pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
python312
|
||||
unstable.uv
|
||||
gh
|
||||
graphviz
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
if [[ -d .venv/ ]]; then
|
||||
source .venv/bin/activate
|
||||
fi
|
||||
if [[ -d .venv/ ]]; then
|
||||
source .venv/bin/activate
|
||||
fi
|
||||
'';
|
||||
}
|
||||
}
|
||||
|
||||
130
uv.lock
generated
130
uv.lock
generated
@@ -1,5 +1,5 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
revision = 2
|
||||
requires-python = ">=3.9, <3.14"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.12'",
|
||||
@@ -7,6 +7,18 @@ resolution-markers = [
|
||||
"python_full_version < '3.11'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "accessible-pygments"
|
||||
version = "0.0.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pygments", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiohappyeyeballs"
|
||||
version = "2.6.1"
|
||||
@@ -577,8 +589,11 @@ dev = [
|
||||
{ name = "tomli" },
|
||||
]
|
||||
docs = [
|
||||
{ name = "furo", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "myst-parser", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "sphinx", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "sphinx-autoapi", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "sphinx-book-theme", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
test = [
|
||||
{ name = "pytest" },
|
||||
@@ -604,8 +619,11 @@ dev = [
|
||||
{ name = "tomli", specifier = ">=2.0.1,<3.0.0" },
|
||||
]
|
||||
docs = [
|
||||
{ name = "furo", marker = "python_full_version >= '3.11'", specifier = ">=2025.7.19" },
|
||||
{ name = "myst-parser", marker = "python_full_version >= '3.11'", specifier = ">=4.0.1" },
|
||||
{ name = "sphinx", marker = "python_full_version >= '3.11'", specifier = ">=8.2.3,<8.3.0" },
|
||||
{ name = "sphinx-autoapi", marker = "python_full_version >= '3.11'", specifier = "==3.6.0" },
|
||||
{ name = "sphinx-book-theme", marker = "python_full_version >= '3.11'", specifier = ">=1.1.4" },
|
||||
]
|
||||
test = [{ name = "pytest", specifier = ">=8.3.2,<9.0.0" }]
|
||||
|
||||
@@ -734,6 +752,22 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/5c/a3d95dc1ec6cdeb032d789b552ecc76effa3557ea9186e1566df6aac18df/fs-2.4.16-py2.py3-none-any.whl", hash = "sha256:660064febbccda264ae0b6bace80a8d1be9e089e0a5eb2427b7d517f9a91545c", size = 135261, upload-time = "2022-05-02T09:25:52.363Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "furo"
|
||||
version = "2025.7.19"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "accessible-pygments", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "beautifulsoup4", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "pygments", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "sphinx", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "sphinx-basic-ng", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/69/312cd100fa45ddaea5a588334d2defa331ff427bcb61f5fe2ae61bdc3762/furo-2025.7.19.tar.gz", hash = "sha256:4164b2cafcf4023a59bb3c594e935e2516f6b9d35e9a5ea83d8f6b43808fe91f", size = 1662054, upload-time = "2025-07-19T10:52:09.754Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/34/2b07b72bee02a63241d654f5d8af87a2de977c59638eec41ca356ab915cd/furo-2025.7.19-py3-none-any.whl", hash = "sha256:bdea869822dfd2b494ea84c0973937e35d1575af088b6721a29c7f7878adc9e3", size = 342175, upload-time = "2025-07-19T10:52:02.399Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.6.12"
|
||||
@@ -782,6 +816,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "3.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.2"
|
||||
@@ -850,6 +896,27 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload-time = "2024-10-18T15:21:52.974Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdit-py-plugins"
|
||||
version = "0.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542, upload-time = "2024-09-09T20:27:49.564Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316, upload-time = "2024-09-09T20:27:48.397Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multidict"
|
||||
version = "6.6.3"
|
||||
@@ -970,6 +1037,23 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/30/9aec301e9772b098c1f5c0ca0279237c9766d94b97802e9888010c64b0ed/multidict-6.6.3-py3-none-any.whl", hash = "sha256:8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a", size = 12313, upload-time = "2025-06-30T15:53:45.437Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "myst-parser"
|
||||
version = "4.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "docutils", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "jinja2", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "markdown-it-py", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "mdit-py-plugins", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "pyyaml", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "sphinx", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985, upload-time = "2025-02-12T10:53:03.833Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d", size = 84579, upload-time = "2025-02-12T10:53:02.078Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nodeenv"
|
||||
version = "1.9.1"
|
||||
@@ -1150,6 +1234,25 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydata-sphinx-theme"
|
||||
version = "0.15.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "accessible-pygments", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "babel", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "beautifulsoup4", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "docutils", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "packaging", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "pygments", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "sphinx", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/67/ea/3ab478cccacc2e8ef69892c42c44ae547bae089f356c4b47caf61730958d/pydata_sphinx_theme-0.15.4.tar.gz", hash = "sha256:7762ec0ac59df3acecf49fd2f889e1b4565dbce8b88b2e29ee06fdd90645a06d", size = 2400673, upload-time = "2024-06-25T19:28:45.041Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/d3/c622950d87a2ffd1654208733b5bd1c5645930014abed8f4c0d74863988b/pydata_sphinx_theme-0.15.4-py3-none-any.whl", hash = "sha256:2136ad0e9500d0949f96167e63f3e298620040aea8f9c74621959eda5d4cf8e6", size = 4640157, upload-time = "2024-06-25T19:28:42.383Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyelftools"
|
||||
version = "0.32"
|
||||
@@ -1434,6 +1537,31 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/58/17/0eda9dc80fcaf257222b506844207e71b5d59567c41bbdcca2a72da119b9/sphinx_autoapi-3.6.0-py3-none-any.whl", hash = "sha256:f3b66714493cab140b0e896d33ce7137654a16ac1edb6563edcbd47bf975f711", size = 35281, upload-time = "2025-02-18T01:50:52.789Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sphinx-basic-ng"
|
||||
version = "1.0.0b2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "sphinx", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736, upload-time = "2023-07-08T18:40:54.166Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/dd/018ce05c532a22007ac58d4f45232514cd9d6dd0ee1dc374e309db830983/sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b", size = 22496, upload-time = "2023-07-08T18:40:52.659Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sphinx-book-theme"
|
||||
version = "1.1.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydata-sphinx-theme", marker = "python_full_version >= '3.11'" },
|
||||
{ name = "sphinx", marker = "python_full_version >= '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/45/19/d002ed96bdc7738c15847c730e1e88282d738263deac705d5713b4d8fa94/sphinx_book_theme-1.1.4.tar.gz", hash = "sha256:73efe28af871d0a89bd05856d300e61edce0d5b2fbb7984e84454be0fedfe9ed", size = 439188, upload-time = "2025-02-20T16:32:32.581Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/51/9e/c41d68be04eef5b6202b468e0f90faf0c469f3a03353f2a218fd78279710/sphinx_book_theme-1.1.4-py3-none-any.whl", hash = "sha256:843b3f5c8684640f4a2d01abd298beb66452d1b2394cd9ef5be5ebd5640ea0e1", size = 433952, upload-time = "2025-02-20T16:32:31.009Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sphinxcontrib-applehelp"
|
||||
version = "2.0.0"
|
||||
|
||||
Reference in New Issue
Block a user