mirror of
https://github.com/malmeloo/FindMy.py.git
synced 2026-04-17 23:53:57 +02:00
@@ -24,6 +24,8 @@ extensions = [
|
|||||||
"sphinx.ext.autodoc",
|
"sphinx.ext.autodoc",
|
||||||
"sphinx.ext.inheritance_diagram",
|
"sphinx.ext.inheritance_diagram",
|
||||||
"autoapi.extension",
|
"autoapi.extension",
|
||||||
|
"sphinx_togglebutton",
|
||||||
|
"sphinx_design",
|
||||||
]
|
]
|
||||||
|
|
||||||
templates_path = ["_templates"]
|
templates_path = ["_templates"]
|
||||||
|
|||||||
10
docs/getstarted/00-install.md
Normal file
10
docs/getstarted/00-install.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Installation
|
||||||
|
|
||||||
|
FindMy.py is available in the standard PyPi repositories. You can install it using the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -U findmy
|
||||||
|
```
|
||||||
|
|
||||||
|
We highly recommend using a [virtual environment](https://docs.python.org/3/library/venv.html) for your project
|
||||||
|
if you want to use FindMy.py. This reduces the chance of dependency conflicts.
|
||||||
@@ -1,7 +1,122 @@
|
|||||||
# Logging in
|
# Logging in
|
||||||
|
|
||||||
Some useful features of this library require an active login session with Apple in order to work correctly.
|
Most 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.
|
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.
|
This page will guide you through the steps needed to log into an Apple account using FindMy.py.
|
||||||
|
|
||||||
|
## Step 0: Account Requirements
|
||||||
|
|
||||||
|
FindMy.py requires an **active** Apple Account which has had a device attached to it **at least once**.
|
||||||
|
It is OK if there are currently no devices signed into the account, as long as a device has signed into
|
||||||
|
it at least once in the past. Note that this does not have to be a _real_ device: a hackintosh using e.g.
|
||||||
|
[Docker-OSX](https://github.com/sickcodes/Docker-OSX) may also work for you if the VM is configured correctly.
|
||||||
|
We do not and will not provide support regarding setting this up.
|
||||||
|
|
||||||
|
Additionally, if you want to track your AirTags, iDevices or other FindMy-compatible 3rd party devices,
|
||||||
|
the account used for FindMy.py does _not_ have to be the same one as the one that the devices are attached to.
|
||||||
|
Given the right decryption keys, any Apple account can query the location history of any FindMy device.
|
||||||
|
However, if you want to track such an official device, you currently must have access to a machine that is
|
||||||
|
running a compatible version of MacOS in order to extract the decryption keys (see later sections).
|
||||||
|
|
||||||
|
## Step 1: Creating an AppleAccount instance
|
||||||
|
|
||||||
|
The first time we want to sign in, we must manually construct an instance of the [AppleAccount](#findmy.AppleAccount)
|
||||||
|
class. Creating such a class requires specifying an [Anisette](../technical/15-Anisette.md) provider. Anisette
|
||||||
|
data is usually generated on-device, and identifies our virtual device when we make a request to Apple's servers.
|
||||||
|
|
||||||
|
There are two different Anisette providers included in FindMy.py: [LocalAnisetteProvider](#findmy.LocalAnisetteProvider)
|
||||||
|
and [RemoteAnisetteProvider](#findmy.RemoteAnisetteProvider). The local provider is much easier to use,
|
||||||
|
so we will be utilizing it in this example.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from findmy import AppleAccount, LocalAnisetteProvider
|
||||||
|
|
||||||
|
ani = LocalAnisetteProvider(libs_path="ani_libs.bin")
|
||||||
|
account = AppleAccount(ani)
|
||||||
|
```
|
||||||
|
|
||||||
|
Note the `libs_path` argument: the local Anisette provider needs to use some proprietary libraries
|
||||||
|
from Apple, which will be stored in this file. They will be automatically downloaded if the file is missing.
|
||||||
|
While the argument is technically optional, it is highly recommended to provide it; otherwise, the library
|
||||||
|
will need to re-download the bundle every time. The size of the bundle is approximately 2,1 MB.
|
||||||
|
|
||||||
|
## Step 2: Logging in
|
||||||
|
|
||||||
|
Logging into an Apple Account is an interactive process: depending on the circumstances, 2FA may or may
|
||||||
|
not be required, and there are multiple different methods to perform 2FA authentication. FindMy.py supports
|
||||||
|
both SMS and Trusted Device challenges to pass the 2FA check, but you must handle the sign-in flow manually in your application.
|
||||||
|
|
||||||
|
```{attention}
|
||||||
|
FindMy.py currently does not support passkey authentication: [#159](https://github.com/malmeloo/FindMy.py/issues/159).
|
||||||
|
If you use a passkey to secure your Apple Account, you must disable it to use FindMy.py. This is because enabling
|
||||||
|
passkeys for your account will disable other 2FA mechanisms.
|
||||||
|
```
|
||||||
|
|
||||||
|
To start the authentication process, provide your email and password as follows:
|
||||||
|
|
||||||
|
```python
|
||||||
|
state = account.login(email, password)
|
||||||
|
```
|
||||||
|
|
||||||
|
The `state` variable will now contain a [LoginState](#findmy.LoginState). If `value == LoginState.LOGGED_IN`, you're
|
||||||
|
good! Continue to the next step. If `value == LoginState.REQUIRE_2FA`, we need to pass a 2FA challenge first.
|
||||||
|
Read on to learn how to do this.
|
||||||
|
|
||||||
|
In order to pass the 2FA challenge, we first need to find out which challenges Apple provides to us. We can use either
|
||||||
|
one of these challenges to continue the login flow.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from findmy import LoginState, TrustedDeviceSecondFactorMethod, SmsSecondFactorMethod
|
||||||
|
|
||||||
|
if state == LoginState.REQUIRE_2FA: # Account requires 2FA
|
||||||
|
methods = account.get_2fa_methods()
|
||||||
|
|
||||||
|
for i, method in enumerate(methods):
|
||||||
|
if isinstance(method, TrustedDeviceSecondFactorMethod):
|
||||||
|
print(f"{i} - Trusted Device")
|
||||||
|
elif isinstance(method, SmsSecondFactorMethod):
|
||||||
|
print(f"{i} - SMS ({method.phone_number})")
|
||||||
|
|
||||||
|
# example output:
|
||||||
|
# 0 - Trusted Device
|
||||||
|
# 1 - SMS (+31 •• ••••••55)
|
||||||
|
# 2 - SMS (+31 •• ••••••32)
|
||||||
|
```
|
||||||
|
|
||||||
|
Depending on your account configuration, you will either get more or fewer 2FA challenge options.
|
||||||
|
In order to pass one of these challenges, we will first call its `request()` method to request a code
|
||||||
|
(on a Trusted Device or via SMS), and then use the `submit()` method to submit the code and pass the challenge.
|
||||||
|
|
||||||
|
```python
|
||||||
|
ind = int(input("Method? > "))
|
||||||
|
|
||||||
|
method = methods[ind]
|
||||||
|
method.request()
|
||||||
|
code = input("Code? > ")
|
||||||
|
|
||||||
|
method.submit(code)
|
||||||
|
```
|
||||||
|
|
||||||
|
If all went well, you should now be logged in!
|
||||||
|
|
||||||
|
## Step 3: Saving / restoring the session
|
||||||
|
|
||||||
|
Before we continue to fetching device locations, I first want to talk about properly closing and restoring sessions.
|
||||||
|
Apple Account sessions are precious, and you shall not create more of them than necessary. Each time we go through the
|
||||||
|
steps outlined above, a new 'device' is added to your account, and you will need to go through the 2FA flow again.
|
||||||
|
This is inefficient and simply unnecessary.
|
||||||
|
|
||||||
|
Therefore, once you are done, it is good practice to save the current state of the account to a file, as well as close
|
||||||
|
any resources that the instance may be holding onto:
|
||||||
|
|
||||||
|
```python
|
||||||
|
acc.to_json("account.json")
|
||||||
|
|
||||||
|
acc.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, if you want to pick up the session again later:
|
||||||
|
|
||||||
|
```python
|
||||||
|
acc = AppleAccount.from_json("account.json", anisette_libs_path="ani_libs.bin")
|
||||||
|
```
|
||||||
|
|||||||
259
docs/getstarted/02-fetching.md
Normal file
259
docs/getstarted/02-fetching.md
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
# Fetching device locations
|
||||||
|
|
||||||
|
```{note}
|
||||||
|
The steps below assume that you have already obtained an `AppleAccount` instance with a login session attached.
|
||||||
|
If you don't have this yet, follow the instructions [here](01-account.md) to obtain one.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 1: Obtaining device information
|
||||||
|
|
||||||
|
In order to fetch location reports for your device, FindMy.py requires the keys that are used to encrypt the location
|
||||||
|
reports that are uploaded by other Apple devices. Depending on the device you are using, this process can differ somewhat.
|
||||||
|
|
||||||
|
```{tip}
|
||||||
|
This step can be quite annoying, but don't fret! You only have to do this once for each device you want to track.
|
||||||
|
Can't figure it out? Join the [Discord server](http://discord.gg/EF6UCG2TF6) and we'll try to help!
|
||||||
|
```
|
||||||
|
|
||||||
|
```````{tab-set}
|
||||||
|
:sync-group: device-type
|
||||||
|
|
||||||
|
``````{tab-item} Official device
|
||||||
|
:sync: official-device
|
||||||
|
|
||||||
|
If you want to track an official FindMy device (AirTag, iPhone/iPad/Mac, 3rd party 'works with FindMy'), you currently
|
||||||
|
need access to a device running MacOS. This can be either a real device or a Hackintosh, however, make sure that you are
|
||||||
|
signed into your Apple account and that the FindMy app is able to track your device. This is a one-time process, so you
|
||||||
|
can also ask a friend to borrow their Mac.
|
||||||
|
|
||||||
|
Note that not all versions of MacOS are currently supported. Please see the menus below for more details.
|
||||||
|
|
||||||
|
`````{tab-set}
|
||||||
|
|
||||||
|
````{tab-item} MacOS <= 14
|
||||||
|
FindMy.py includes a built-in utility that will dump the accessories from your Mac. Note that it will pop up
|
||||||
|
an interactive password prompt to unlock the keychain; therefore, this utility does **not** work over SSH.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m findmy decrypt --out-dir devices/
|
||||||
|
```
|
||||||
|
|
||||||
|
The above command will write one JSON file for each accessory found on your system to the `devices` directory.
|
||||||
|
These files are ready to be used with FindMy.py!
|
||||||
|
|
||||||
|
````
|
||||||
|
|
||||||
|
````{tab-item} MacOS 15
|
||||||
|
MacOS 15 may or may not include additional protection for the BeaconStoreKey. You should first try to follow
|
||||||
|
the instructions for MacOS 14. If these do not work for you, read on.
|
||||||
|
|
||||||
|
If the instructions for MacOS 14 do not work for you, the BeaconStoreKey is likely protected. We will need to
|
||||||
|
use an additional utility to decrypt a set of 'plist' files. Go and follow the instructions at @pajowu's
|
||||||
|
[beaconstorekey-extractor](https://github.com/pajowu/beaconstorekey-extractor), then return here.
|
||||||
|
|
||||||
|
Welcome back! **Did you remember to re-enable System Integrity Protection? If not, go do that now!**
|
||||||
|
|
||||||
|
If all went well, you should now have one or multiple decrypted plist files. Hooray!
|
||||||
|
That was the most difficult part. These plist files are not directly compatible with FindMy.py,
|
||||||
|
so we'll need to convert them first.
|
||||||
|
Save this [plist_to_json](https://github.com/malmeloo/FindMy.py/blob/main/examples/plist_to_json.py)
|
||||||
|
script somewhere on your computer and run it as follows:
|
||||||
|
|
||||||
|
```python
|
||||||
|
python3 plist_to_json.py path/to/original_file.plist device.json
|
||||||
|
```
|
||||||
|
|
||||||
|
This will convert a single plist file into a FindMy.py-compatible JSON file and save it to `device.json`.
|
||||||
|
Repeat this step for any other plist files you want to convert.
|
||||||
|
|
||||||
|
```{note}
|
||||||
|
The first time you try to fetch the location of your device, FindMy.py might appear to hang for a bit.
|
||||||
|
This is because the beaconstorekey-extractor tool does not export key alignment data, so FindMy.py needs
|
||||||
|
to query a wide range of possible values to find the right alignment to use. The older your tag is, the
|
||||||
|
longer it will take to do this process.
|
||||||
|
|
||||||
|
If you are physically close to the tag, you can speed this up significantly by using the
|
||||||
|
[Tag Scanner](https://github.com/malmeloo/FindMy.py/blob/main/examples/scanner.py). This will attempt
|
||||||
|
to discover your tag via Bluetooth and update its alignment based on the values that it is currently broadcasting.
|
||||||
|
Make sure to give it your device JSON file as argument! Otherwise, the scanner does not know which tag
|
||||||
|
to look for.
|
||||||
|
```
|
||||||
|
|
||||||
|
````
|
||||||
|
|
||||||
|
````{tab-item} MacOS 26
|
||||||
|
MacOS 26 appears to protect the BeaconStoreKey needed to decrypt the plist records that contain accessory data.
|
||||||
|
Unlike with MacOS 15, disabling SIP does not appear to fix it.
|
||||||
|
|
||||||
|
If you figure out a way to dump the plist encryption key, please share your findings
|
||||||
|
[here](https://github.com/malmeloo/FindMy.py/issues/177).
|
||||||
|
````
|
||||||
|
|
||||||
|
````{tab-item} I don't have a Mac :(
|
||||||
|
Unfortunately, FindMy.py currently only supports dumping accessory information from a Mac.
|
||||||
|
Device encryption keys are stored in your account's keychain, which is only accessible on Apple hardware.
|
||||||
|
iOS / iPadOS is too limited and does not allow us to access the necessary device secrets.
|
||||||
|
|
||||||
|
A method to join the encrypted keychain circle from non-MacOS hardware has recently been found,
|
||||||
|
but it takes a lot of time and effort to implement. We are currently considering what the best
|
||||||
|
way would be to implement this, however, we are not currently actively working on making this happen.
|
||||||
|
You can follow development on this feature and voice your support in
|
||||||
|
[this](https://github.com/malmeloo/FindMy.py/issues/173) GitHub issue.
|
||||||
|
````
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
``````
|
||||||
|
|
||||||
|
``````{tab-item} Custom device
|
||||||
|
:sync: custom-device
|
||||||
|
|
||||||
|
If you built your own FindMy tag (using e.g. [OpenHaystack](https://https://github.com/seemoo-lab/openhaystack),
|
||||||
|
[macless-haystack](https://github.com/dchristl/macless-haystack), or [one](https://github.com/pix/heystack-nrf5x)
|
||||||
|
of the [many](https://github.com/hybridgroup/go-haystack) other [available](https://github.com/dakhnod/FakeTag)
|
||||||
|
projects), it will most likely be broadcasting a static key. In this case, grab the private key that you generated
|
||||||
|
and create a [KeyPair](#findmy.KeyPair) object as follows:
|
||||||
|
|
||||||
|
````python
|
||||||
|
# PRIVATE key in base64 format
|
||||||
|
device = KeyPair.from_b64(...)
|
||||||
|
````
|
||||||
|
|
||||||
|
`````{admonition} Don't have a private key yet?
|
||||||
|
:class: tip dropdown
|
||||||
|
|
||||||
|
If you are setting up your DIY tag and have not generated a private key yet, you can use FindMy.py to do it!
|
||||||
|
|
||||||
|
````python
|
||||||
|
device = KeyPair.new()
|
||||||
|
print(device.private_key_b64)
|
||||||
|
# a6C9bgy4H/bpZ7vGtVBdO3/UyNjan2/3a7UW4w==
|
||||||
|
````
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
``````
|
||||||
|
|
||||||
|
```````
|
||||||
|
|
||||||
|
## Step 2: Testing your device JSON file (optional)
|
||||||
|
|
||||||
|
At this point, you should be able to fetch location reports for your accessory. FindMy.py includes extensive
|
||||||
|
example scripts to help you test this.
|
||||||
|
|
||||||
|
`````{tab-set}
|
||||||
|
:sync-group: device-type
|
||||||
|
|
||||||
|
````{tab-item} Official device
|
||||||
|
:sync: official-device
|
||||||
|
|
||||||
|
Clone the FindMy.py repository somewhere and enter the `examples/` directory.
|
||||||
|
Then run the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 airtag.py path_to_device.json
|
||||||
|
```
|
||||||
|
|
||||||
|
The script will ask for your account credentials. If all went well, it will output a location report as follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
Last known location:
|
||||||
|
- LocationReport(hashed_adv_key=..., timestamp=..., lat=..., lon=...)
|
||||||
|
```
|
||||||
|
|
||||||
|
````
|
||||||
|
|
||||||
|
````{tab-item} Custom device
|
||||||
|
:sync: custom-device
|
||||||
|
|
||||||
|
Clone the FindMy.py repository somewhere and enter the `examples/` directory.
|
||||||
|
Then run the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 fetch_reports.py <private_key_base64>
|
||||||
|
```
|
||||||
|
|
||||||
|
The script will ask for your account credentials. If all went well, it will output a location report as follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
Last known location:
|
||||||
|
- LocationReport(hashed_adv_key=..., timestamp=..., lat=..., lon=...)
|
||||||
|
```
|
||||||
|
|
||||||
|
````
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
## Step 3: Fetching location reports
|
||||||
|
|
||||||
|
To fetch location report for a device, you can use the [fetch_location](#findmy.AppleAccount.fetch_location) method
|
||||||
|
on your [AppleAccount](#findmy.AppleAccount) instance. This method will return either a [LocationReport](#findmy.LocationReport)
|
||||||
|
if a location is found, or `None` if no location was found.
|
||||||
|
|
||||||
|
```python
|
||||||
|
location = account.fetch_location(device)
|
||||||
|
print(location)
|
||||||
|
|
||||||
|
# LocationReport(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to query locations for multiple devices, you can also pass in a list. FindMy.py will then optimize its
|
||||||
|
request payloads to get the locations in as few queries to Apple servers as possible. In this case, the method will
|
||||||
|
return a dictionary with the given devices as keys, and the fetch result as value.
|
||||||
|
|
||||||
|
```python
|
||||||
|
locations = account.fetch_location([device1, device2])
|
||||||
|
print(locations)
|
||||||
|
|
||||||
|
# {device1: LocationReport(...), device2: None}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also save location reports to JSON if you want to store them:
|
||||||
|
|
||||||
|
```python
|
||||||
|
location.to_json("report.json")
|
||||||
|
```
|
||||||
|
|
||||||
|
````{caution}
|
||||||
|
The JSON representation of a location report includes the device's encryption key at that time.
|
||||||
|
**Sharing this file with someone else will allow them to query location reports for your device.**
|
||||||
|
You can avoid including the key by setting the `include_key` parameter to `False`, however,
|
||||||
|
this will save the report in its encrypted format, which means you will have to manually decrypt it again.
|
||||||
|
|
||||||
|
```python
|
||||||
|
enc_report_json = report.to_json(include_key=False)
|
||||||
|
report = LocationReport.from_json(enc_report_json)
|
||||||
|
|
||||||
|
print(report.is_decrypted)
|
||||||
|
# False
|
||||||
|
|
||||||
|
print(report.latitude)
|
||||||
|
# RuntimeError: Latitude is unavailable while the report is encrypted.
|
||||||
|
|
||||||
|
report.decrypt(key) # key is the `KeyPair` of the device at that time
|
||||||
|
|
||||||
|
print(report.is_decrypted)
|
||||||
|
# True
|
||||||
|
```
|
||||||
|
|
||||||
|
````
|
||||||
|
|
||||||
|
## Step 4: Saving accessory state to disk
|
||||||
|
|
||||||
|
After fetching, FindMy.py may have made changes to the accessory's internal state.
|
||||||
|
Saving these changes to the accessory's JSON representation ensures that the process of fetching
|
||||||
|
the device's location will be as fast and efficient as possible.
|
||||||
|
|
||||||
|
The device's state can be exported to JSON as follows:
|
||||||
|
|
||||||
|
```python
|
||||||
|
device.to_json("airtag.json")
|
||||||
|
```
|
||||||
|
|
||||||
|
```{tip}
|
||||||
|
As you may have noticed, many objects in FindMy.py can be (de)serialized to and from JSON.
|
||||||
|
Classes such as [AppleAccount](#findmy.AppleAccount), [LocationReport](#findmy.LocationReport),
|
||||||
|
[KeyPair](#findmy.KeyPair) and [FindMyAccessory](#findmy.FindMyAccessory) all subclass
|
||||||
|
[Serializable](#findmy.util.abc.Serializable). Whenever a class in FindMy.py subclasses `Serializable`,
|
||||||
|
you can save and load its state using the `to_json` and `from_json` methods.
|
||||||
|
```
|
||||||
3
docs/technical/15-Anisette.md
Normal file
3
docs/technical/15-Anisette.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Anisette
|
||||||
|
|
||||||
|
TODO
|
||||||
@@ -36,11 +36,10 @@ def main(airtag_path: Path) -> int:
|
|||||||
airtag = FindMyAccessory.from_json(airtag_path)
|
airtag = FindMyAccessory.from_json(airtag_path)
|
||||||
|
|
||||||
# Step 1: log into an Apple account
|
# Step 1: log into an Apple account
|
||||||
print("Logging into account")
|
|
||||||
acc = get_account_sync(STORE_PATH, ANISETTE_SERVER, ANISETTE_LIBS_PATH)
|
acc = get_account_sync(STORE_PATH, ANISETTE_SERVER, ANISETTE_LIBS_PATH)
|
||||||
|
print(f"Logged in as: {acc.account_name} ({acc.first_name} {acc.last_name})")
|
||||||
|
|
||||||
# step 2: fetch reports!
|
# step 2: fetch reports!
|
||||||
print("Fetching location")
|
|
||||||
location = acc.fetch_location(airtag)
|
location = acc.fetch_location(airtag)
|
||||||
|
|
||||||
# step 3: print 'em
|
# step 3: print 'em
|
||||||
@@ -306,10 +306,10 @@ class LocationReport(HasHashedPublicKey, util.abc.Serializable[LocationReportMap
|
|||||||
|
|
||||||
def __lt__(self, other: LocationReport) -> bool:
|
def __lt__(self, other: LocationReport) -> bool:
|
||||||
"""
|
"""
|
||||||
Compare against another :meth:`KeyReport`.
|
Compare against another :meth:`LocationReport`.
|
||||||
|
|
||||||
A :meth:`KeyReport` is said to be "less than" another :meth:`KeyReport` iff its recorded
|
A :meth:`LocationReport` is said to be "less than" another :meth:`LocationReport` iff
|
||||||
timestamp is strictly less than the other report.
|
its recorded timestamp is strictly less than the other report.
|
||||||
"""
|
"""
|
||||||
if isinstance(other, LocationReport):
|
if isinstance(other, LocationReport):
|
||||||
return self.timestamp < other.timestamp
|
return self.timestamp < other.timestamp
|
||||||
@@ -318,7 +318,7 @@ class LocationReport(HasHashedPublicKey, util.abc.Serializable[LocationReportMap
|
|||||||
@override
|
@override
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
"""Human-readable string representation of the location report."""
|
"""Human-readable string representation of the location report."""
|
||||||
msg = f"KeyReport(hashed_adv_key={self.hashed_adv_key_b64}, timestamp={self.timestamp}"
|
msg = f"LocationReport(hashed_adv_key={self.hashed_adv_key_b64}, timestamp={self.timestamp}"
|
||||||
if self.is_decrypted:
|
if self.is_decrypted:
|
||||||
msg += f", lat={self.latitude}, lon={self.longitude}"
|
msg += f", lat={self.latitude}, lon={self.longitude}"
|
||||||
msg += ")"
|
msg += ")"
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ docs = [
|
|||||||
"sphinx>=8.2.3,<8.3.0; python_full_version >= '3.11'",
|
"sphinx>=8.2.3,<8.3.0; python_full_version >= '3.11'",
|
||||||
"sphinx-autoapi==3.6.0",
|
"sphinx-autoapi==3.6.0",
|
||||||
"sphinx-book-theme>=1.1.4",
|
"sphinx-book-theme>=1.1.4",
|
||||||
|
"sphinx-design>=0.6.1",
|
||||||
|
"sphinx-togglebutton>=0.3.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pyright]
|
[tool.pyright]
|
||||||
|
|||||||
44
uv.lock
generated
44
uv.lock
generated
@@ -608,6 +608,8 @@ docs = [
|
|||||||
{ name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
{ name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||||
{ name = "sphinx-autoapi" },
|
{ name = "sphinx-autoapi" },
|
||||||
{ name = "sphinx-book-theme" },
|
{ name = "sphinx-book-theme" },
|
||||||
|
{ name = "sphinx-design" },
|
||||||
|
{ name = "sphinx-togglebutton" },
|
||||||
]
|
]
|
||||||
test = [
|
test = [
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
@@ -638,6 +640,8 @@ docs = [
|
|||||||
{ 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", specifier = "==3.6.0" },
|
{ name = "sphinx-autoapi", specifier = "==3.6.0" },
|
||||||
{ name = "sphinx-book-theme", specifier = ">=1.1.4" },
|
{ name = "sphinx-book-theme", specifier = ">=1.1.4" },
|
||||||
|
{ name = "sphinx-design", specifier = ">=0.6.1" },
|
||||||
|
{ name = "sphinx-togglebutton", specifier = ">=0.3.2" },
|
||||||
]
|
]
|
||||||
test = [{ name = "pytest", specifier = ">=8.3.2,<9.0.0" }]
|
test = [{ name = "pytest", specifier = ">=8.3.2,<9.0.0" }]
|
||||||
|
|
||||||
@@ -1670,6 +1674,37 @@ 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" },
|
{ 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 = "sphinx-design"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||||
|
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
|
||||||
|
{ name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/2b/69/b34e0cb5336f09c6866d53b4a19d76c227cdec1bbc7ac4de63ca7d58c9c7/sphinx_design-0.6.1.tar.gz", hash = "sha256:b44eea3719386d04d765c1a8257caca2b3e6f8421d7b3a5e742c0fd45f84e632", size = 2193689, upload-time = "2024-08-02T13:48:44.277Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/43/65c0acbd8cc6f50195a3a1fc195c404988b15c67090e73c7a41a9f57d6bd/sphinx_design-0.6.1-py3-none-any.whl", hash = "sha256:b11f37db1a802a183d61b159d9a202314d4d2fe29c163437001324fe2f19549c", size = 2215338, upload-time = "2024-08-02T13:48:42.106Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sphinx-togglebutton"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "docutils" },
|
||||||
|
{ name = "setuptools" },
|
||||||
|
{ name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||||
|
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
|
||||||
|
{ name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||||
|
{ name = "wheel" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f0/df/d151dfbbe588116e450ca7e898750cb218dca6b2e557ced8de6f9bd7242b/sphinx-togglebutton-0.3.2.tar.gz", hash = "sha256:ab0c8b366427b01e4c89802d5d078472c427fa6e9d12d521c34fa0442559dc7a", size = 8324, upload-time = "2022-07-15T12:08:50.286Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/18/267ce39f29d26cdc7177231428ba823fe5ca94db8c56d1bed69033b364c8/sphinx_togglebutton-0.3.2-py3-none-any.whl", hash = "sha256:9647ba7874b7d1e2d43413d8497153a85edc6ac95a3fea9a75ef9c1e08aaae2b", size = 8249, upload-time = "2022-07-15T12:08:48.8Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sphinxcontrib-applehelp"
|
name = "sphinxcontrib-applehelp"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@@ -1835,6 +1870,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279, upload-time = "2025-08-13T14:24:05.111Z" },
|
{ url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279, upload-time = "2025-08-13T14:24:05.111Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wheel"
|
||||||
|
version = "0.45.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545, upload-time = "2024-11-23T00:18:23.513Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494, upload-time = "2024-11-23T00:18:21.207Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winrt-runtime"
|
name = "winrt-runtime"
|
||||||
version = "3.2.1"
|
version = "3.2.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user