mirror of
https://github.com/malmeloo/FindMy.py.git
synced 2026-04-25 18:25:37 +02:00
Merge pull request #7 from malmeloo/feat/better-docs
Better documentation
This commit is contained in:
5
.github/workflows/docs.yml
vendored
5
.github/workflows/docs.yml
vendored
@@ -22,6 +22,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Install graphviz
|
||||||
|
run: sudo apt-get install -y graphviz
|
||||||
|
|
||||||
- name: Build documentation
|
- name: Build documentation
|
||||||
run: |
|
run: |
|
||||||
cd docs
|
cd docs
|
||||||
@@ -33,7 +36,7 @@ jobs:
|
|||||||
- name: Upload Pages artifact
|
- name: Upload Pages artifact
|
||||||
uses: actions/upload-pages-artifact@v3
|
uses: actions/upload-pages-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: 'docs/_build/html/'
|
path: "docs/_build/html/"
|
||||||
|
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
id: deployment
|
||||||
|
|||||||
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 -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
project = "FindMy.py"
|
project = "FindMy.py"
|
||||||
copyright = "2024, Mike Almeloo"
|
copyright = "2024, Mike Almeloo"
|
||||||
author = "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 ---------------------------------------------------
|
# -- General configuration ---------------------------------------------------
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#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"]
|
templates_path = ["_templates"]
|
||||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||||
|
|
||||||
# -- AutoAPI Options ---------------------------------------------------------
|
# -- AutoAPI Options ---------------------------------------------------------
|
||||||
autoapi_dirs = ["../findmy/"]
|
autoapi_dirs = ["../findmy/"]
|
||||||
|
autoapi_root = "reference/"
|
||||||
|
autoapi_add_toctree_entry = False
|
||||||
|
autoapi_keep_files = True
|
||||||
autoapi_options = [
|
autoapi_options = [
|
||||||
"members",
|
"members",
|
||||||
"undoc-members",
|
"undoc-members",
|
||||||
"show-inheritance",
|
"show-inheritance",
|
||||||
|
"show-inheritance-diagram",
|
||||||
"show-module-summary",
|
"show-module-summary",
|
||||||
"special-members",
|
"special-members",
|
||||||
"imported-members",
|
"imported-members",
|
||||||
@@ -34,5 +47,5 @@ autoapi_options = [
|
|||||||
# -- Options for HTML output -------------------------------------------------
|
# -- Options for HTML output -------------------------------------------------
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#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"]
|
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):
|
class RollingKeyPairSource(ABC):
|
||||||
"""A class that generates rolling `KeyPair`s."""
|
"""A class that generates rolling :meth:`KeyPair`s."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|||||||
@@ -21,5 +21,5 @@ class InvalidStateError(RuntimeError):
|
|||||||
"""
|
"""
|
||||||
Raised when a method is used that is in conflict with the internal account state.
|
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
|
@property
|
||||||
@override
|
@override
|
||||||
def hashed_adv_key_bytes(self) -> bytes:
|
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()
|
return hashlib.sha256(self.adv_key_bytes).digest()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -136,7 +136,7 @@ class KeyPair(HasPublicKey, Serializable[KeyPairMapping]):
|
|||||||
key_type: KeyType = KeyType.UNKNOWN,
|
key_type: KeyType = KeyType.UNKNOWN,
|
||||||
name: str | None = None,
|
name: str | None = 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)
|
priv_int = crypto.bytes_to_int(private_key)
|
||||||
self._priv_key = ec.derive_private_key(
|
self._priv_key = ec.derive_private_key(
|
||||||
priv_int,
|
priv_int,
|
||||||
@@ -162,15 +162,15 @@ class KeyPair(HasPublicKey, Serializable[KeyPairMapping]):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def new(cls) -> KeyPair:
|
def new(cls) -> KeyPair:
|
||||||
"""Generate a new random `KeyPair`."""
|
"""Generate a new random :meth:`KeyPair`."""
|
||||||
return cls(secrets.token_bytes(28))
|
return cls(secrets.token_bytes(28))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_b64(cls, key_b64: str) -> KeyPair:
|
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))
|
return cls(base64.b64decode(key_b64))
|
||||||
|
|
||||||
@@ -185,7 +185,7 @@ class KeyPair(HasPublicKey, Serializable[KeyPairMapping]):
|
|||||||
"""
|
"""
|
||||||
Return the private key as a base64-encoded string.
|
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")
|
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})'
|
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."""
|
"""KeyPair generator."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@@ -244,17 +244,17 @@ class KeyGenerator(ABC, Generic[K]):
|
|||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def __next__(self) -> K:
|
def __next__(self) -> _K:
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def __getitem__(self, val: int) -> K: ...
|
def __getitem__(self, val: int) -> _K: ...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def __getitem__(self, val: slice) -> Generator[K, None, None]: ...
|
def __getitem__(self, val: slice) -> Generator[_K, None, None]: ...
|
||||||
|
|
||||||
@abstractmethod
|
@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
|
return NotImplemented
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ def get_key() -> bytes:
|
|||||||
|
|
||||||
def decrypt_plist(encrypted: str | Path | bytes | IO[bytes], key: bytes) -> dict:
|
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.
|
:param encrypted: If bytes or IO, the encrypted plist data.
|
||||||
If str or Path, the path to the encrypted plist file, which is
|
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.
|
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
|
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.
|
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
|
raise NotImplementedError
|
||||||
|
|
||||||
@@ -218,7 +218,7 @@ class BaseAppleAccount(Closable, Serializable[AccountStateMapping], ABC):
|
|||||||
"""
|
"""
|
||||||
Request a 2FA code to be sent to a trusted device.
|
Request a 2FA code to be sent to a trusted device.
|
||||||
|
|
||||||
Consider using `BaseSecondFactorMethod.request` instead.
|
Consider using :meth:`BaseSecondFactorMethod.request` instead.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ class BaseAppleAccount(Closable, Serializable[AccountStateMapping], ABC):
|
|||||||
"""
|
"""
|
||||||
Submit a 2FA code that was sent to a trusted device.
|
Submit a 2FA code that was sent to a trusted device.
|
||||||
|
|
||||||
Consider using `BaseSecondFactorMethod.submit` instead.
|
Consider using :meth:`BaseSecondFactorMethod.submit` instead.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@@ -270,9 +270,9 @@ class BaseAppleAccount(Closable, Serializable[AccountStateMapping], ABC):
|
|||||||
list[LocationReport] | dict[HasHashedPublicKey | RollingKeyPairSource, list[LocationReport]]
|
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
|
raise NotImplementedError
|
||||||
|
|
||||||
@@ -311,9 +311,9 @@ class BaseAppleAccount(Closable, Serializable[AccountStateMapping], ABC):
|
|||||||
list[LocationReport] | dict[HasHashedPublicKey | RollingKeyPairSource, list[LocationReport]]
|
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
|
raise NotImplementedError
|
||||||
|
|
||||||
@@ -326,13 +326,13 @@ class BaseAppleAccount(Closable, Serializable[AccountStateMapping], ABC):
|
|||||||
"""
|
"""
|
||||||
Retrieve a complete dictionary of Anisette headers.
|
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
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class AsyncAppleAccount(BaseAppleAccount):
|
class AsyncAppleAccount(BaseAppleAccount):
|
||||||
"""An async implementation of `BaseAppleAccount`."""
|
"""An async implementation of :meth:`BaseAppleAccount`."""
|
||||||
|
|
||||||
# auth endpoints
|
# auth endpoints
|
||||||
_ENDPOINT_GSA = "https://gsa.apple.com/grandslam/GsService2"
|
_ENDPOINT_GSA = "https://gsa.apple.com/grandslam/GsService2"
|
||||||
@@ -357,7 +357,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
|||||||
"""
|
"""
|
||||||
Initialize the apple account.
|
Initialize the apple account.
|
||||||
|
|
||||||
:param anisette: An instance of `AsyncAnisetteProvider`.
|
:param anisette: An instance of :meth:`AsyncAnisetteProvider`.
|
||||||
"""
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
@@ -401,7 +401,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
|||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
def login_state(self) -> LoginState:
|
def login_state(self) -> LoginState:
|
||||||
"""See `BaseAppleAccount.login_state`."""
|
"""See :meth:`BaseAppleAccount.login_state`."""
|
||||||
return self._login_state
|
return self._login_state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -412,7 +412,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
|||||||
)
|
)
|
||||||
@override
|
@override
|
||||||
def account_name(self) -> str | None:
|
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
|
return self._account_info["account_name"] if self._account_info else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -423,7 +423,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
|||||||
)
|
)
|
||||||
@override
|
@override
|
||||||
def first_name(self) -> str | None:
|
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
|
return self._account_info["first_name"] if self._account_info else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -434,7 +434,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
|||||||
)
|
)
|
||||||
@override
|
@override
|
||||||
def last_name(self) -> str | None:
|
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
|
return self._account_info["last_name"] if self._account_info else None
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -501,7 +501,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
|||||||
@require_login_state(LoginState.LOGGED_OUT)
|
@require_login_state(LoginState.LOGGED_OUT)
|
||||||
@override
|
@override
|
||||||
async def login(self, username: str, password: str) -> LoginState:
|
async def login(self, username: str, password: str) -> LoginState:
|
||||||
"""See `BaseAppleAccount.login`."""
|
"""See :meth:`BaseAppleAccount.login`."""
|
||||||
# LOGGED_OUT -> (REQUIRE_2FA or AUTHENTICATED)
|
# LOGGED_OUT -> (REQUIRE_2FA or AUTHENTICATED)
|
||||||
new_state = await self._gsa_authenticate(username, password)
|
new_state = await self._gsa_authenticate(username, password)
|
||||||
if new_state == LoginState.REQUIRE_2FA: # pass control back to handle 2FA
|
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)
|
@require_login_state(LoginState.REQUIRE_2FA)
|
||||||
@override
|
@override
|
||||||
async def get_2fa_methods(self) -> Sequence[AsyncSecondFactorMethod]:
|
async def get_2fa_methods(self) -> Sequence[AsyncSecondFactorMethod]:
|
||||||
"""See `BaseAppleAccount.get_2fa_methods`."""
|
"""See :meth:`BaseAppleAccount.get_2fa_methods`."""
|
||||||
methods: list[AsyncSecondFactorMethod] = []
|
methods: list[AsyncSecondFactorMethod] = []
|
||||||
|
|
||||||
if self._account_info is None:
|
if self._account_info is None:
|
||||||
@@ -542,7 +542,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
|||||||
@require_login_state(LoginState.REQUIRE_2FA)
|
@require_login_state(LoginState.REQUIRE_2FA)
|
||||||
@override
|
@override
|
||||||
async def sms_2fa_request(self, phone_number_id: int) -> None:
|
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"}
|
data = {"phoneNumber": {"id": phone_number_id}, "mode": "sms"}
|
||||||
|
|
||||||
await self._sms_2fa_request(
|
await self._sms_2fa_request(
|
||||||
@@ -554,7 +554,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
|||||||
@require_login_state(LoginState.REQUIRE_2FA)
|
@require_login_state(LoginState.REQUIRE_2FA)
|
||||||
@override
|
@override
|
||||||
async def sms_2fa_submit(self, phone_number_id: int, code: str) -> LoginState:
|
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 = {
|
data = {
|
||||||
"phoneNumber": {"id": phone_number_id},
|
"phoneNumber": {"id": phone_number_id},
|
||||||
"securityCode": {"code": str(code)},
|
"securityCode": {"code": str(code)},
|
||||||
@@ -579,7 +579,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
|||||||
@require_login_state(LoginState.REQUIRE_2FA)
|
@require_login_state(LoginState.REQUIRE_2FA)
|
||||||
@override
|
@override
|
||||||
async def td_2fa_request(self) -> None:
|
async def td_2fa_request(self) -> None:
|
||||||
"""See `BaseAppleAccount.td_2fa_request`."""
|
"""See :meth:`BaseAppleAccount.td_2fa_request`."""
|
||||||
headers = {
|
headers = {
|
||||||
"Content-Type": "text/x-xml-plist",
|
"Content-Type": "text/x-xml-plist",
|
||||||
"Accept": "text/x-xml-plist",
|
"Accept": "text/x-xml-plist",
|
||||||
@@ -593,7 +593,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
|||||||
@require_login_state(LoginState.REQUIRE_2FA)
|
@require_login_state(LoginState.REQUIRE_2FA)
|
||||||
@override
|
@override
|
||||||
async def td_2fa_submit(self, code: str) -> LoginState:
|
async def td_2fa_submit(self, code: str) -> LoginState:
|
||||||
"""See `BaseAppleAccount.td_2fa_submit`."""
|
"""See :meth:`BaseAppleAccount.td_2fa_submit`."""
|
||||||
headers = {
|
headers = {
|
||||||
"security-code": code,
|
"security-code": code,
|
||||||
"Content-Type": "text/x-xml-plist",
|
"Content-Type": "text/x-xml-plist",
|
||||||
@@ -717,7 +717,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
|||||||
) -> (
|
) -> (
|
||||||
list[LocationReport] | dict[HasHashedPublicKey | RollingKeyPairSource, list[LocationReport]]
|
list[LocationReport] | dict[HasHashedPublicKey | RollingKeyPairSource, list[LocationReport]]
|
||||||
):
|
):
|
||||||
"""See `BaseAppleAccount.fetch_reports`."""
|
"""See :meth:`BaseAppleAccount.fetch_reports`."""
|
||||||
date_to = date_to or datetime.now().astimezone()
|
date_to = date_to or datetime.now().astimezone()
|
||||||
|
|
||||||
return await self._reports.fetch_reports(
|
return await self._reports.fetch_reports(
|
||||||
@@ -758,7 +758,7 @@ class AsyncAppleAccount(BaseAppleAccount):
|
|||||||
) -> (
|
) -> (
|
||||||
list[LocationReport] | dict[HasHashedPublicKey | RollingKeyPairSource, list[LocationReport]]
|
list[LocationReport] | dict[HasHashedPublicKey | RollingKeyPairSource, list[LocationReport]]
|
||||||
):
|
):
|
||||||
"""See `BaseAppleAccount.fetch_last_reports`."""
|
"""See :meth:`BaseAppleAccount.fetch_last_reports`."""
|
||||||
end = datetime.now(tz=timezone.utc)
|
end = datetime.now(tz=timezone.utc)
|
||||||
start = end - timedelta(hours=hours)
|
start = end - timedelta(hours=hours)
|
||||||
|
|
||||||
@@ -971,15 +971,15 @@ class AsyncAppleAccount(BaseAppleAccount):
|
|||||||
with_client_info: bool = False,
|
with_client_info: bool = False,
|
||||||
serial: str = "0",
|
serial: str = "0",
|
||||||
) -> dict[str, str]:
|
) -> 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)
|
return await self._anisette.get_headers(self._uid, self._devid, serial, with_client_info)
|
||||||
|
|
||||||
|
|
||||||
class AppleAccount(BaseAppleAccount):
|
class AppleAccount(BaseAppleAccount):
|
||||||
"""
|
"""
|
||||||
A sync implementation of `BaseappleAccount`.
|
A sync implementation of :meth:`BaseappleAccount`.
|
||||||
|
|
||||||
Uses `AsyncappleAccount` internally.
|
Uses :meth:`AsyncappleAccount` internally.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -988,7 +988,7 @@ class AppleAccount(BaseAppleAccount):
|
|||||||
*,
|
*,
|
||||||
state_info: AccountStateMapping | None = None,
|
state_info: AccountStateMapping | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""See `AsyncAppleAccount.__init__`."""
|
"""See :meth:`AsyncAppleAccount.__init__`."""
|
||||||
self._asyncacc = AsyncAppleAccount(anisette=anisette, state_info=state_info)
|
self._asyncacc = AsyncAppleAccount(anisette=anisette, state_info=state_info)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -1001,31 +1001,31 @@ class AppleAccount(BaseAppleAccount):
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
async def close(self) -> None:
|
async def close(self) -> None:
|
||||||
"""See `AsyncAppleAccount.close`."""
|
"""See :meth:`AsyncAppleAccount.close`."""
|
||||||
await self._asyncacc.close()
|
await self._asyncacc.close()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
def login_state(self) -> LoginState:
|
def login_state(self) -> LoginState:
|
||||||
"""See `AsyncAppleAccount.login_state`."""
|
"""See :meth:`AsyncAppleAccount.login_state`."""
|
||||||
return self._asyncacc.login_state
|
return self._asyncacc.login_state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
def account_name(self) -> str | None:
|
def account_name(self) -> str | None:
|
||||||
"""See `AsyncAppleAccount.login_state`."""
|
"""See :meth:`AsyncAppleAccount.login_state`."""
|
||||||
return self._asyncacc.account_name
|
return self._asyncacc.account_name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
def first_name(self) -> str | None:
|
def first_name(self) -> str | None:
|
||||||
"""See `AsyncAppleAccount.first_name`."""
|
"""See :meth:`AsyncAppleAccount.first_name`."""
|
||||||
return self._asyncacc.first_name
|
return self._asyncacc.first_name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
def last_name(self) -> str | None:
|
def last_name(self) -> str | None:
|
||||||
"""See `AsyncAppleAccount.last_name`."""
|
"""See :meth:`AsyncAppleAccount.last_name`."""
|
||||||
return self._asyncacc.last_name
|
return self._asyncacc.last_name
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -1051,13 +1051,13 @@ class AppleAccount(BaseAppleAccount):
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
def login(self, username: str, password: str) -> LoginState:
|
def login(self, username: str, password: str) -> LoginState:
|
||||||
"""See `AsyncAppleAccount.login`."""
|
"""See :meth:`AsyncAppleAccount.login`."""
|
||||||
coro = self._asyncacc.login(username, password)
|
coro = self._asyncacc.login(username, password)
|
||||||
return self._evt_loop.run_until_complete(coro)
|
return self._evt_loop.run_until_complete(coro)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def get_2fa_methods(self) -> Sequence[SyncSecondFactorMethod]:
|
def get_2fa_methods(self) -> Sequence[SyncSecondFactorMethod]:
|
||||||
"""See `AsyncAppleAccount.get_2fa_methods`."""
|
"""See :meth:`AsyncAppleAccount.get_2fa_methods`."""
|
||||||
coro = self._asyncacc.get_2fa_methods()
|
coro = self._asyncacc.get_2fa_methods()
|
||||||
methods = self._evt_loop.run_until_complete(coro)
|
methods = self._evt_loop.run_until_complete(coro)
|
||||||
|
|
||||||
@@ -1078,25 +1078,25 @@ class AppleAccount(BaseAppleAccount):
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
def sms_2fa_request(self, phone_number_id: int) -> None:
|
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)
|
coro = self._asyncacc.sms_2fa_request(phone_number_id)
|
||||||
return self._evt_loop.run_until_complete(coro)
|
return self._evt_loop.run_until_complete(coro)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def sms_2fa_submit(self, phone_number_id: int, code: str) -> LoginState:
|
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)
|
coro = self._asyncacc.sms_2fa_submit(phone_number_id, code)
|
||||||
return self._evt_loop.run_until_complete(coro)
|
return self._evt_loop.run_until_complete(coro)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def td_2fa_request(self) -> None:
|
def td_2fa_request(self) -> None:
|
||||||
"""See `AsyncAppleAccount.td_2fa_request`."""
|
"""See :meth:`AsyncAppleAccount.td_2fa_request`."""
|
||||||
coro = self._asyncacc.td_2fa_request()
|
coro = self._asyncacc.td_2fa_request()
|
||||||
return self._evt_loop.run_until_complete(coro)
|
return self._evt_loop.run_until_complete(coro)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def td_2fa_submit(self, code: str) -> LoginState:
|
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)
|
coro = self._asyncacc.td_2fa_submit(code)
|
||||||
return self._evt_loop.run_until_complete(coro)
|
return self._evt_loop.run_until_complete(coro)
|
||||||
|
|
||||||
@@ -1135,7 +1135,7 @@ class AppleAccount(BaseAppleAccount):
|
|||||||
) -> (
|
) -> (
|
||||||
list[LocationReport] | dict[HasHashedPublicKey | RollingKeyPairSource, list[LocationReport]]
|
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)
|
coro = self._asyncacc.fetch_reports(keys, date_from, date_to)
|
||||||
return self._evt_loop.run_until_complete(coro)
|
return self._evt_loop.run_until_complete(coro)
|
||||||
|
|
||||||
@@ -1170,7 +1170,7 @@ class AppleAccount(BaseAppleAccount):
|
|||||||
) -> (
|
) -> (
|
||||||
list[LocationReport] | dict[HasHashedPublicKey | RollingKeyPairSource, list[LocationReport]]
|
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)
|
coro = self._asyncacc.fetch_last_reports(keys, hours)
|
||||||
return self._evt_loop.run_until_complete(coro)
|
return self._evt_loop.run_until_complete(coro)
|
||||||
|
|
||||||
@@ -1180,6 +1180,6 @@ class AppleAccount(BaseAppleAccount):
|
|||||||
with_client_info: bool = False,
|
with_client_info: bool = False,
|
||||||
serial: str = "0",
|
serial: str = "0",
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
"""See `AsyncAppleAccount.get_anisette_headers`."""
|
"""See :meth:`AsyncAppleAccount.get_anisette_headers`."""
|
||||||
coro = self._asyncacc.get_anisette_headers(with_client_info, serial)
|
coro = self._asyncacc.get_anisette_headers(with_client_info, serial)
|
||||||
return self._evt_loop.run_until_complete(coro)
|
return self._evt_loop.run_until_complete(coro)
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class BaseAnisetteProvider(Closable, Serializable, ABC):
|
|||||||
"""
|
"""
|
||||||
Generate a complete dictionary of Anisette headers.
|
Generate a complete dictionary of Anisette headers.
|
||||||
|
|
||||||
Consider using `BaseAppleAccount.get_anisette_headers` instead.
|
Consider using :meth:`BaseAppleAccount.get_anisette_headers` instead.
|
||||||
"""
|
"""
|
||||||
headers = {
|
headers = {
|
||||||
# Current Time
|
# Current Time
|
||||||
@@ -207,7 +207,7 @@ class RemoteAnisetteProvider(BaseAnisetteProvider, Serializable[RemoteAnisetteMa
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
def to_json(self, dst: str | Path | None = None, /) -> RemoteAnisetteMapping:
|
def to_json(self, dst: str | Path | None = None, /) -> RemoteAnisetteMapping:
|
||||||
"""See `BaseAnisetteProvider.serialize`."""
|
"""See :meth:`BaseAnisetteProvider.serialize`."""
|
||||||
return save_and_return_json(
|
return save_and_return_json(
|
||||||
{
|
{
|
||||||
"type": "aniRemote",
|
"type": "aniRemote",
|
||||||
@@ -219,7 +219,7 @@ class RemoteAnisetteProvider(BaseAnisetteProvider, Serializable[RemoteAnisetteMa
|
|||||||
@classmethod
|
@classmethod
|
||||||
@override
|
@override
|
||||||
def from_json(cls, val: str | Path | RemoteAnisetteMapping) -> RemoteAnisetteProvider:
|
def from_json(cls, val: str | Path | RemoteAnisetteMapping) -> RemoteAnisetteProvider:
|
||||||
"""See `BaseAnisetteProvider.deserialize`."""
|
"""See :meth:`BaseAnisetteProvider.deserialize`."""
|
||||||
val = read_data_json(val)
|
val = read_data_json(val)
|
||||||
|
|
||||||
assert val["type"] == "aniRemote"
|
assert val["type"] == "aniRemote"
|
||||||
@@ -231,7 +231,7 @@ class RemoteAnisetteProvider(BaseAnisetteProvider, Serializable[RemoteAnisetteMa
|
|||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
def otp(self) -> str:
|
def otp(self) -> str:
|
||||||
"""See `BaseAnisetteProvider.otp`_."""
|
"""See :meth:`BaseAnisetteProvider.otp`."""
|
||||||
otp = (self._anisette_data or {}).get("X-Apple-I-MD")
|
otp = (self._anisette_data or {}).get("X-Apple-I-MD")
|
||||||
if otp is None:
|
if otp is None:
|
||||||
logger.warning("X-Apple-I-MD header not found! Returning fallback...")
|
logger.warning("X-Apple-I-MD header not found! Returning fallback...")
|
||||||
@@ -240,7 +240,7 @@ class RemoteAnisetteProvider(BaseAnisetteProvider, Serializable[RemoteAnisetteMa
|
|||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
def machine(self) -> str:
|
def machine(self) -> str:
|
||||||
"""See `BaseAnisetteProvider.machine`_."""
|
"""See :meth:`BaseAnisetteProvider.machine`."""
|
||||||
machine = (self._anisette_data or {}).get("X-Apple-I-MD-M")
|
machine = (self._anisette_data or {}).get("X-Apple-I-MD-M")
|
||||||
if machine is None:
|
if machine is None:
|
||||||
logger.warning("X-Apple-I-MD-M header not found! Returning fallback...")
|
logger.warning("X-Apple-I-MD-M header not found! Returning fallback...")
|
||||||
@@ -254,7 +254,7 @@ class RemoteAnisetteProvider(BaseAnisetteProvider, Serializable[RemoteAnisetteMa
|
|||||||
serial: str = "0",
|
serial: str = "0",
|
||||||
with_client_info: bool = False,
|
with_client_info: bool = False,
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
"""See `BaseAnisetteProvider.get_headers`_."""
|
"""See :meth::meth:`BaseAnisetteProvider.get_headers`."""
|
||||||
if self._closed:
|
if self._closed:
|
||||||
msg = "RemoteAnisetteProvider has been closed and cannot be used"
|
msg = "RemoteAnisetteProvider has been closed and cannot be used"
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
@@ -270,7 +270,7 @@ class RemoteAnisetteProvider(BaseAnisetteProvider, Serializable[RemoteAnisetteMa
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
async def close(self) -> None:
|
async def close(self) -> None:
|
||||||
"""See `AnisetteProvider.close`."""
|
"""See :meth:`AnisetteProvider.close`."""
|
||||||
if self._closed:
|
if self._closed:
|
||||||
return # Already closed, make it idempotent
|
return # Already closed, make it idempotent
|
||||||
|
|
||||||
@@ -283,7 +283,7 @@ class RemoteAnisetteProvider(BaseAnisetteProvider, Serializable[RemoteAnisetteMa
|
|||||||
|
|
||||||
|
|
||||||
class LocalAnisetteProvider(BaseAnisetteProvider, Serializable[LocalAnisetteMapping]):
|
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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -328,7 +328,7 @@ class LocalAnisetteProvider(BaseAnisetteProvider, Serializable[LocalAnisetteMapp
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
def to_json(self, dst: str | Path | None = None, /) -> LocalAnisetteMapping:
|
def to_json(self, dst: str | Path | None = None, /) -> LocalAnisetteMapping:
|
||||||
"""See `BaseAnisetteProvider.serialize`."""
|
"""See :meth:`BaseAnisetteProvider.serialize`."""
|
||||||
with BytesIO() as buf:
|
with BytesIO() as buf:
|
||||||
self._ani.save_provisioning(buf)
|
self._ani.save_provisioning(buf)
|
||||||
prov_data = base64.b64encode(buf.getvalue()).decode("utf-8")
|
prov_data = base64.b64encode(buf.getvalue()).decode("utf-8")
|
||||||
@@ -349,7 +349,7 @@ class LocalAnisetteProvider(BaseAnisetteProvider, Serializable[LocalAnisetteMapp
|
|||||||
*,
|
*,
|
||||||
libs_path: str | Path | None = None,
|
libs_path: str | Path | None = None,
|
||||||
) -> LocalAnisetteProvider:
|
) -> LocalAnisetteProvider:
|
||||||
"""See `BaseAnisetteProvider.deserialize`."""
|
"""See :meth:`BaseAnisetteProvider.deserialize`."""
|
||||||
val = read_data_json(val)
|
val = read_data_json(val)
|
||||||
|
|
||||||
assert val["type"] == "aniLocal"
|
assert val["type"] == "aniLocal"
|
||||||
@@ -366,7 +366,7 @@ class LocalAnisetteProvider(BaseAnisetteProvider, Serializable[LocalAnisetteMapp
|
|||||||
serial: str = "0",
|
serial: str = "0",
|
||||||
with_client_info: bool = False,
|
with_client_info: bool = False,
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
"""See `BaseAnisetteProvider.get_headers`_."""
|
"""See :meth:`BaseAnisetteProvider.get_headers`."""
|
||||||
self._ani_data = self._ani.get_data()
|
self._ani_data = self._ani.get_data()
|
||||||
|
|
||||||
return await super().get_headers(user_id, device_id, serial, with_client_info)
|
return await super().get_headers(user_id, device_id, serial, with_client_info)
|
||||||
@@ -374,7 +374,7 @@ class LocalAnisetteProvider(BaseAnisetteProvider, Serializable[LocalAnisetteMapp
|
|||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
def otp(self) -> str:
|
def otp(self) -> str:
|
||||||
"""See `BaseAnisetteProvider.otp`_."""
|
"""See :meth:`BaseAnisetteProvider.otp`."""
|
||||||
machine = (self._ani_data or {}).get("X-Apple-I-MD")
|
machine = (self._ani_data or {}).get("X-Apple-I-MD")
|
||||||
if machine is None:
|
if machine is None:
|
||||||
logger.warning("X-Apple-I-MD header not found! Returning fallback...")
|
logger.warning("X-Apple-I-MD header not found! Returning fallback...")
|
||||||
@@ -383,7 +383,7 @@ class LocalAnisetteProvider(BaseAnisetteProvider, Serializable[LocalAnisetteMapp
|
|||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
def machine(self) -> str:
|
def machine(self) -> str:
|
||||||
"""See `BaseAnisetteProvider.machine`_."""
|
"""See :meth:`BaseAnisetteProvider.machine`."""
|
||||||
machine = (self._ani_data or {}).get("X-Apple-I-MD-M")
|
machine = (self._ani_data or {}).get("X-Apple-I-MD-M")
|
||||||
if machine is None:
|
if machine is None:
|
||||||
logger.warning("X-Apple-I-MD-M header not found! Returning fallback...")
|
logger.warning("X-Apple-I-MD-M header not found! Returning fallback...")
|
||||||
@@ -391,4 +391,4 @@ class LocalAnisetteProvider(BaseAnisetteProvider, Serializable[LocalAnisetteMapp
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
async def close(self) -> None:
|
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]):
|
class LocationReport(HasHashedPublicKey, Serializable[LocationReportMapping]):
|
||||||
"""Location report corresponding to a certain `HasHashedPublicKey`."""
|
"""Location report corresponding to a certain :meth:`HasHashedPublicKey`."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
payload: bytes,
|
payload: bytes,
|
||||||
hashed_adv_key: bytes,
|
hashed_adv_key: bytes,
|
||||||
) -> None:
|
) -> 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._payload: bytes = payload
|
||||||
self._hashed_adv_key: bytes = hashed_adv_key
|
self._hashed_adv_key: bytes = hashed_adv_key
|
||||||
|
|
||||||
@@ -68,7 +72,7 @@ class LocationReport(HasHashedPublicKey, Serializable[LocationReportMapping]):
|
|||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
def hashed_adv_key_bytes(self) -> bytes:
|
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
|
return self._hashed_adv_key
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -96,7 +100,7 @@ class LocationReport(HasHashedPublicKey, Serializable[LocationReportMapping]):
|
|||||||
return key.hashed_adv_key_bytes == self._hashed_adv_key
|
return key.hashed_adv_key_bytes == self._hashed_adv_key
|
||||||
|
|
||||||
def decrypt(self, key: KeyPair) -> None:
|
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):
|
if not self.can_decrypt(key):
|
||||||
msg = "Cannot decrypt with this key!"
|
msg = "Cannot decrypt with this key!"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
@@ -136,7 +140,7 @@ class LocationReport(HasHashedPublicKey, Serializable[LocationReportMapping]):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def timestamp(self) -> datetime:
|
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)
|
timestamp_int = int.from_bytes(self._payload[0:4], "big") + (60 * 60 * 24 * 11323)
|
||||||
return datetime.fromtimestamp(timestamp_int, tz=timezone.utc).astimezone()
|
return datetime.fromtimestamp(timestamp_int, tz=timezone.utc).astimezone()
|
||||||
|
|
||||||
@@ -302,9 +306,9 @@ class LocationReport(HasHashedPublicKey, Serializable[LocationReportMapping]):
|
|||||||
|
|
||||||
def __lt__(self, other: LocationReport) -> bool:
|
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.
|
timestamp is strictly less than the other report.
|
||||||
"""
|
"""
|
||||||
if isinstance(other, LocationReport):
|
if isinstance(other, LocationReport):
|
||||||
@@ -369,12 +373,12 @@ class LocationReportsFetcher:
|
|||||||
"""
|
"""
|
||||||
Fetch location reports for a certain device.
|
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.
|
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.
|
location reports corresponding to that source.
|
||||||
When ``device`` is a sequence of :class:`.HasHashedPublicKey`s or RollingKeyPairSource's,
|
When `device` is a sequence of :class:`HasHashedPublicKey`s or RollingKeyPairSource's,
|
||||||
it will return a dictionary with the :class:`.HasHashedPublicKey` or `.RollingKeyPairSource`
|
it will return a dictionary with the provided object
|
||||||
as key, and a list of location reports as value.
|
as key, and a list of location reports as value.
|
||||||
"""
|
"""
|
||||||
key_devs: dict[HasHashedPublicKey, HasHashedPublicKey | RollingKeyPairSource] = {}
|
key_devs: dict[HasHashedPublicKey, HasHashedPublicKey | RollingKeyPairSource] = {}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from typing_extensions import override
|
|||||||
|
|
||||||
|
|
||||||
class LoginState(Enum):
|
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
|
LOGGED_OUT = 0
|
||||||
REQUIRE_2FA = 1
|
REQUIRE_2FA = 1
|
||||||
@@ -15,9 +15,9 @@ class LoginState(Enum):
|
|||||||
|
|
||||||
def __lt__(self, other: "LoginState") -> bool:
|
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.
|
an "earlier" stage of the login process, going from LOGGED_OUT to LOGGED_IN.
|
||||||
"""
|
"""
|
||||||
if isinstance(other, LoginState):
|
if isinstance(other, LoginState):
|
||||||
|
|||||||
@@ -63,13 +63,13 @@ class AsyncSecondFactorMethod(BaseSecondFactorMethod, ABC):
|
|||||||
@override
|
@override
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def request(self) -> None:
|
async def request(self) -> None:
|
||||||
"""See `BaseSecondFactorMethod.request`."""
|
"""See :meth:`BaseSecondFactorMethod.request`."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def submit(self, code: str) -> LoginState:
|
async def submit(self, code: str) -> LoginState:
|
||||||
"""See `BaseSecondFactorMethod.submit`."""
|
"""See :meth:`BaseSecondFactorMethod.submit`."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
@@ -93,13 +93,13 @@ class SyncSecondFactorMethod(BaseSecondFactorMethod, ABC):
|
|||||||
@override
|
@override
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def request(self) -> None:
|
def request(self) -> None:
|
||||||
"""See `BaseSecondFactorMethod.request`."""
|
"""See :meth:`BaseSecondFactorMethod.request`."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def submit(self, code: str) -> LoginState:
|
def submit(self, code: str) -> LoginState:
|
||||||
"""See `BaseSecondFactorMethod.submit`."""
|
"""See :meth:`BaseSecondFactorMethod.submit`."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ class TrustedDeviceSecondFactorMethod(BaseSecondFactorMethod, ABC):
|
|||||||
|
|
||||||
|
|
||||||
class AsyncSmsSecondFactor(AsyncSecondFactorMethod, SmsSecondFactorMethod):
|
class AsyncSmsSecondFactor(AsyncSecondFactorMethod, SmsSecondFactorMethod):
|
||||||
"""An async implementation of `SmsSecondFactorMethod`."""
|
"""An async implementation of :meth:`SmsSecondFactorMethod`."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -139,7 +139,7 @@ class AsyncSmsSecondFactor(AsyncSecondFactorMethod, SmsSecondFactorMethod):
|
|||||||
"""
|
"""
|
||||||
Initialize the second factor method.
|
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)
|
super().__init__(account)
|
||||||
|
|
||||||
@@ -174,7 +174,7 @@ class AsyncSmsSecondFactor(AsyncSecondFactorMethod, SmsSecondFactorMethod):
|
|||||||
|
|
||||||
|
|
||||||
class SyncSmsSecondFactor(SyncSecondFactorMethod, SmsSecondFactorMethod):
|
class SyncSmsSecondFactor(SyncSecondFactorMethod, SmsSecondFactorMethod):
|
||||||
"""A sync implementation of `SmsSecondFactorMethod`."""
|
"""A sync implementation of :meth:`SmsSecondFactorMethod`."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -182,7 +182,7 @@ class SyncSmsSecondFactor(SyncSecondFactorMethod, SmsSecondFactorMethod):
|
|||||||
number_id: int,
|
number_id: int,
|
||||||
phone_number: str,
|
phone_number: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""See `AsyncSmsSecondFactor.__init__`."""
|
"""See :meth:`AsyncSmsSecondFactor.__init__`."""
|
||||||
super().__init__(account)
|
super().__init__(account)
|
||||||
|
|
||||||
self._phone_number_id: int = number_id
|
self._phone_number_id: int = number_id
|
||||||
@@ -191,28 +191,28 @@ class SyncSmsSecondFactor(SyncSecondFactorMethod, SmsSecondFactorMethod):
|
|||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
def phone_number_id(self) -> int:
|
def phone_number_id(self) -> int:
|
||||||
"""See `AsyncSmsSecondFactor.phone_number_id`."""
|
"""See :meth:`AsyncSmsSecondFactor.phone_number_id`."""
|
||||||
return self._phone_number_id
|
return self._phone_number_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
def phone_number(self) -> str:
|
def phone_number(self) -> str:
|
||||||
"""See `AsyncSmsSecondFactor.phone_number`."""
|
"""See :meth:`AsyncSmsSecondFactor.phone_number`."""
|
||||||
return self._phone_number
|
return self._phone_number
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def request(self) -> None:
|
def request(self) -> None:
|
||||||
"""See `AsyncSmsSecondFactor.request`."""
|
"""See :meth:`AsyncSmsSecondFactor.request`."""
|
||||||
return self.account.sms_2fa_request(self._phone_number_id)
|
return self.account.sms_2fa_request(self._phone_number_id)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def submit(self, code: str) -> LoginState:
|
def submit(self, code: str) -> LoginState:
|
||||||
"""See `AsyncSmsSecondFactor.submit`."""
|
"""See :meth:`AsyncSmsSecondFactor.submit`."""
|
||||||
return self.account.sms_2fa_submit(self._phone_number_id, code)
|
return self.account.sms_2fa_submit(self._phone_number_id, code)
|
||||||
|
|
||||||
|
|
||||||
class AsyncTrustedDeviceSecondFactor(AsyncSecondFactorMethod, TrustedDeviceSecondFactorMethod):
|
class AsyncTrustedDeviceSecondFactor(AsyncSecondFactorMethod, TrustedDeviceSecondFactorMethod):
|
||||||
"""An async implementation of `TrustedDeviceSecondFactorMethod`."""
|
"""An async implementation of :meth:`TrustedDeviceSecondFactorMethod`."""
|
||||||
|
|
||||||
@override
|
@override
|
||||||
async def request(self) -> None:
|
async def request(self) -> None:
|
||||||
@@ -224,14 +224,14 @@ class AsyncTrustedDeviceSecondFactor(AsyncSecondFactorMethod, TrustedDeviceSecon
|
|||||||
|
|
||||||
|
|
||||||
class SyncTrustedDeviceSecondFactor(SyncSecondFactorMethod, TrustedDeviceSecondFactorMethod):
|
class SyncTrustedDeviceSecondFactor(SyncSecondFactorMethod, TrustedDeviceSecondFactorMethod):
|
||||||
"""A sync implementation of `TrustedDeviceSecondFactorMethod`."""
|
"""A sync implementation of :meth:`TrustedDeviceSecondFactorMethod`."""
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def request(self) -> None:
|
def request(self) -> None:
|
||||||
"""See `AsyncTrustedDeviceSecondFactor.request`."""
|
"""See :meth:`AsyncTrustedDeviceSecondFactor.request`."""
|
||||||
return self.account.td_2fa_request()
|
return self.account.td_2fa_request()
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def submit(self, code: str) -> LoginState:
|
def submit(self, code: str) -> LoginState:
|
||||||
"""See `AsyncTrustedDeviceSecondFactor.submit`."""
|
"""See :meth:`AsyncTrustedDeviceSecondFactor.submit`."""
|
||||||
return self.account.td_2fa_submit(code)
|
return self.account.td_2fa_submit(code)
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ class SeparatedOfflineFindingDevice(OfflineFindingDevice, HasPublicKey):
|
|||||||
detected_at: datetime,
|
detected_at: datetime,
|
||||||
additional_data: dict[Any, Any] | None = None,
|
additional_data: dict[Any, Any] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a `SeparatedOfflineFindingDevice`."""
|
"""Initialize a :meth:`SeparatedOfflineFindingDevice`."""
|
||||||
super().__init__(mac_bytes, status, detected_at, additional_data)
|
super().__init__(mac_bytes, status, detected_at, additional_data)
|
||||||
|
|
||||||
self._public_key: bytes = public_key
|
self._public_key: bytes = public_key
|
||||||
@@ -225,7 +225,7 @@ class SeparatedOfflineFindingDevice(OfflineFindingDevice, HasPublicKey):
|
|||||||
@property
|
@property
|
||||||
@override
|
@override
|
||||||
def adv_key_bytes(self) -> bytes:
|
def adv_key_bytes(self) -> bytes:
|
||||||
"""See `HasPublicKey.adv_key_bytes`."""
|
"""See :meth:`HasPublicKey.adv_key_bytes`."""
|
||||||
return self._public_key
|
return self._public_key
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -300,7 +300,7 @@ _DEVICE_TYPES = {
|
|||||||
|
|
||||||
|
|
||||||
class OfflineFindingScanner:
|
class OfflineFindingScanner:
|
||||||
"""BLE scanner that searches for `OfflineFindingDevice`s."""
|
"""BLE scanner that searches for :meth:`OfflineFindingDevice`s."""
|
||||||
|
|
||||||
_scan_ctrl_lock = asyncio.Lock()
|
_scan_ctrl_lock = asyncio.Lock()
|
||||||
|
|
||||||
@@ -311,7 +311,7 @@ class OfflineFindingScanner:
|
|||||||
Initialize an instance of the Scanner using an event loop.
|
Initialize an instance of the Scanner using an event loop.
|
||||||
|
|
||||||
You most likely do not want to use this yourself;
|
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})
|
self._scanner: BleakScanner = BleakScanner(self._scan_callback, cb={"use_bdaddr": True})
|
||||||
|
|
||||||
@@ -377,10 +377,10 @@ class OfflineFindingScanner:
|
|||||||
extend_timeout: bool = False,
|
extend_timeout: bool = False,
|
||||||
) -> AsyncGenerator[OfflineFindingDevice, None]:
|
) -> 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
|
If :meth:`extend_timeout` is set, the timer will be extended
|
||||||
by `timeout` seconds every time a new device is discovered.
|
by :meth:`timeout` seconds every time a new device is discovered.
|
||||||
"""
|
"""
|
||||||
await self._start_scan()
|
await self._start_scan()
|
||||||
|
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ class Closable(ABC):
|
|||||||
|
|
||||||
def __init__(self, loop: asyncio.AbstractEventLoop | None = None) -> None:
|
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.
|
using the loop when it is garbage collected.
|
||||||
"""
|
"""
|
||||||
self._loop: asyncio.AbstractEventLoop | None = loop
|
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.
|
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 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.
|
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,
|
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
|
@abstractmethod
|
||||||
def from_json(cls, val: str | Path | _T, /) -> Self:
|
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.
|
Otherwise, it should be the Mapping itself.
|
||||||
|
|
||||||
See `Serializable.to_json` for more information.
|
See :meth:`Serializable.to_json` for more information.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ from collections.abc import Mapping
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TypeVar, cast
|
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."""
|
"""Save and return a JSON-serializable data structure."""
|
||||||
if dst is None:
|
if dst is None:
|
||||||
return data
|
return data
|
||||||
@@ -23,12 +23,12 @@ def save_and_return_json(data: T, dst: str | Path | None) -> T:
|
|||||||
return data
|
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."""
|
"""Read JSON data from a file if a path is passed, or return the argument itself."""
|
||||||
if isinstance(val, str):
|
if isinstance(val, str):
|
||||||
val = Path(val)
|
val = Path(val)
|
||||||
|
|
||||||
if isinstance(val, Path):
|
if isinstance(val, Path):
|
||||||
val = cast("T", json.loads(val.read_text()))
|
val = cast("_T", json.loads(val.read_text()))
|
||||||
|
|
||||||
return val
|
return val
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class _HttpRequestOptions(_RequestOptions, total=False):
|
|||||||
|
|
||||||
|
|
||||||
class HttpResponse:
|
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:
|
def __init__(self, status_code: int, content: bytes) -> None:
|
||||||
"""Initialize the response."""
|
"""Initialize the response."""
|
||||||
@@ -115,7 +115,7 @@ class HttpSession(Closable):
|
|||||||
"""
|
"""
|
||||||
Make an HTTP request.
|
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()
|
session = await self._get_session()
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
from collections.abc import Coroutine
|
from collections.abc import Coroutine
|
||||||
from typing import TypeVar, Union
|
from typing import TypeVar, Union
|
||||||
|
|
||||||
T = TypeVar("T")
|
_T = TypeVar("_T")
|
||||||
|
|
||||||
# Cannot use `|` operator (PEP 604) in python 3.9,
|
# Cannot use `|` operator (PEP 604) in python 3.9,
|
||||||
# even with __future__ import since it is evaluated directly
|
# 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",
|
"packaging>=25.0,<26.0",
|
||||||
]
|
]
|
||||||
test = ["pytest>=8.3.2,<9.0.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]
|
[tool.pyright]
|
||||||
venvPath = "."
|
venvPath = "."
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
{ pkgs ? import <nixpkgs> {} }:
|
{
|
||||||
|
pkgs ? import <nixpkgs> { },
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
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
|
in
|
||||||
pkgs.mkShell {
|
pkgs.mkShell {
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
python312
|
python312
|
||||||
unstable.uv
|
unstable.uv
|
||||||
gh
|
gh
|
||||||
|
graphviz
|
||||||
];
|
];
|
||||||
|
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
|
|||||||
130
uv.lock
generated
130
uv.lock
generated
@@ -1,5 +1,5 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 3
|
revision = 2
|
||||||
requires-python = ">=3.9, <3.14"
|
requires-python = ">=3.9, <3.14"
|
||||||
resolution-markers = [
|
resolution-markers = [
|
||||||
"python_full_version >= '3.12'",
|
"python_full_version >= '3.12'",
|
||||||
@@ -7,6 +7,18 @@ resolution-markers = [
|
|||||||
"python_full_version < '3.11'",
|
"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]]
|
[[package]]
|
||||||
name = "aiohappyeyeballs"
|
name = "aiohappyeyeballs"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
@@ -577,8 +589,11 @@ dev = [
|
|||||||
{ name = "tomli" },
|
{ name = "tomli" },
|
||||||
]
|
]
|
||||||
docs = [
|
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", marker = "python_full_version >= '3.11'" },
|
||||||
{ name = "sphinx-autoapi", 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 = [
|
test = [
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
@@ -604,8 +619,11 @@ dev = [
|
|||||||
{ name = "tomli", specifier = ">=2.0.1,<3.0.0" },
|
{ name = "tomli", specifier = ">=2.0.1,<3.0.0" },
|
||||||
]
|
]
|
||||||
docs = [
|
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", 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-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" }]
|
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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "identify"
|
name = "identify"
|
||||||
version = "2.6.12"
|
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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "markupsafe"
|
name = "markupsafe"
|
||||||
version = "3.0.2"
|
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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "multidict"
|
name = "multidict"
|
||||||
version = "6.6.3"
|
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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "nodeenv"
|
name = "nodeenv"
|
||||||
version = "1.9.1"
|
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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "pyelftools"
|
name = "pyelftools"
|
||||||
version = "0.32"
|
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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "sphinxcontrib-applehelp"
|
name = "sphinxcontrib-applehelp"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user