This commit is contained in:
2026-03-21 08:22:24 -04:00
parent 47aeebd86f
commit c5a563f047
5 changed files with 229 additions and 28 deletions

Binary file not shown.

View File

@@ -1,13 +1,35 @@
import asyncio import asyncio
import json from dotenv import load_dotenv
import os import os
import json
import logging
from pyicloud import PyiCloudService from pyicloud import PyiCloudService
from pyicloud.exceptions import PyiCloud2FARequiredException from pyicloud.exceptions import PyiCloud2FARequiredException
class JsonFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
payload = {
"ts": self.formatTime(record, "%Y-%m-%dT%H:%M:%S%z"),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
}
if record.exc_info:
payload["exc_info"] = self.formatException(record.exc_info)
return json.dumps(payload, ensure_ascii=True)
handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
root_logger = logging.getLogger()
root_logger.handlers = [handler]
root_logger.setLevel(logging.INFO)
logger = logging.getLogger("ios-api")
class FindMyMonitor: class FindMyMonitor:
def __init__(self, username, password, queue: asyncio.Queue, token_file="icloud_token.txt"): def __init__(self, queue: asyncio.Queue, token_file="icloud_token.txt"):
self.username = username self.username = os.getenv("APPLE_ID")
self.password = password self.password = os.getenv("APPLE_PW")
self.token_file = token_file self.token_file = token_file
self.queue = queue self.queue = queue
self.api = None self.api = None
@@ -72,6 +94,7 @@ class FindMyMonitor:
self.start() self.start()
while self.running: while self.running:
logger.info("Starting iCloud FMF loop")
try: try:
lat, lon, ts = await self.get_location() lat, lon, ts = await self.get_location()
print(f"[{ts}] Location: {lat}, {lon}") print(f"[{ts}] Location: {lat}, {lon}")

View File

@@ -5,7 +5,9 @@ requires-python = ">=3.14"
dependencies = [ dependencies = [
"fastapi==0.135.1", "fastapi==0.135.1",
"pydantic==2.12.5", "pydantic==2.12.5",
"pyicloud>=2.4.1",
"pymobiledevice3==9.0.0", "pymobiledevice3==9.0.0",
"python-dotenv>=1.2.2",
"python-socketio==5.16.1", "python-socketio==5.16.1",
"typing==3.10.0.0", "typing==3.10.0.0",
"uvicorn==0.41.0", "uvicorn==0.41.0",

View File

@@ -157,12 +157,14 @@ class LocationSimulationState:
self.queue_data: Dict = {} self.queue_data: Dict = {}
self.queue_status: Optional[asyncio.Event] = asyncio.Event() self.queue_status: Optional[asyncio.Event] = asyncio.Event()
self.queue_state: str = "STOPPED" self.queue_state: str = "STOPPED"
self.test_mode: bool = False self.test_mode: bool = True
self.simulation_task: Optional[asyncio.Task] = None self.simulation_task: Optional[asyncio.Task] = None
self.sio: socketio.AsyncServer = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*") self.sio: socketio.AsyncServer = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*")
self.tunnel: Optional[RemoteServiceDiscoveryService] = None self.tunnel: Optional[RemoteServiceDiscoveryService] = None
self.fmf_queue: asyncio.Queue = asyncio.Queue self.fmf_queue: asyncio.Queue = asyncio.Queue
self.fmf_location: Optional[iCloudLocationData] = None self.fmf_location: Optional[iCloudLocationData] = None
self.icloud_monitor = FindMyMonitor(self.fmf_queue)
self.icloud_monitor_task = None
class TunneldRunnerSio: class TunneldRunnerSio:
@@ -205,6 +207,7 @@ class TunneldRunnerSio:
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
self._tunneld_core.start() self._tunneld_core.start()
await start_icloud_monitor()
yield yield
logger.info("Closing tunneld tasks...") logger.info("Closing tunneld tasks...")
await empty_simulation_queue() await empty_simulation_queue()
@@ -384,16 +387,17 @@ class TunneldRunnerSio:
async def start_icloud_monitor() async def start_icloud_monitor():
"""Start Apple iCloud Find My Monitor to retreive actual reported device location""" """Start Apple iCloud Find My Monitor to retreive actual reported device location"""
monitor = FindMyMonitor(apple_id, apple_pw, self.context.fmf_queue) self.context.icloud_monitor_task = asyncio.create_task(self.context.icloud_monitor.run_monitor(interval=30))
monitor_task = asyncio.create_task(monitor.run_monitor(interval=30))
while True: while True:
updated_location = await self.context.fmf_queue.get() updated_location = await self.context.fmf_queue.get()
if self.context.fmf_location !== updated_location: if self.context.fmf_location != updated_location:
self.context.fmf_location = update_location self.context.fmf_location = update_location
self.context.sio.emit("fmf_update", updated_location, namespace="/",) self.context.sio.emit("fmf_update", updated_location, namespace="/",)
async def end_icloud_monitor():
self.context.icloud_monitor.end()
async def pause_simulation_queue(): async def pause_simulation_queue():
"""Pauses asyncio.Queue playback""" """Pauses asyncio.Queue playback"""
@@ -444,7 +448,7 @@ class TunneldRunnerSio:
async def end_simulation_queue() -> bool: async def end_simulation_queue() -> bool:
"""Ends asyncio.Queue playback and closes tunnel""" """Ends asyncio.Queue playback and closes tunnel"""
logger.info("End location simulation request from %s", sid) logger.info("End location simulation request")
try: try:
if self.context.test_mode: if self.context.test_mode:
q = self.context.queue q = self.context.queue
@@ -503,6 +507,7 @@ class TunneldRunnerSio:
"test_mode": self.context.test_mode, "test_mode": self.context.test_mode,
"simulation_task": self.context.simulation_task.get_name() if self.context.simulation_task else None, "simulation_task": self.context.simulation_task.get_name() if self.context.simulation_task else None,
"tunnel": self.context.tunnel.service.address[0] if self.context.tunnel else None, "tunnel": self.context.tunnel.service.address[0] if self.context.tunnel else None,
"fmf_location": self.context.fmf_location,
} }
return data return data
@@ -636,6 +641,7 @@ class TunneldRunnerSio:
async def app_add_location(data: SimulationRequestData) -> fastapi.Response: async def app_add_location(data: SimulationRequestData) -> fastapi.Response:
""" Add a location to the simulation queue""" """ Add a location to the simulation queue"""
logger.info("Request to add new location to queue") logger.info("Request to add new location to queue")
loc_id = str(uuid.uuid4()) loc_id = str(uuid.uuid4())
latitude = data.get("latitude") if isinstance(data, dict) else getattr(data, "latitude", None) latitude = data.get("latitude") if isinstance(data, dict) else getattr(data, "latitude", None)
longitude = data.get("longitude") if isinstance(data, dict) else getattr(data, "longitude", None) longitude = data.get("longitude") if isinstance(data, dict) else getattr(data, "longitude", None)
@@ -644,28 +650,27 @@ class TunneldRunnerSio:
if latitude is not None and longitude is not None: if latitude is not None and longitude is not None:
logger.info("Adding location %s (%s, %s) with %s delay to the queue", loc_id, latitude, longitude, logger.info("Adding location %s (%s, %s) with %s delay to the queue", loc_id, latitude, longitude,
delay) delay)
await self.context.queue.put((loc_id, latitude, longitude, delay)) accrued_delay = 0
if delay == 0: if self.context.queue_data:
start_time = datetime.now(timezone.utc).isoformat() accrued_delay = sum(item.get('delay', 0) for item in self.context.queue_data.values())
else:
now_time = datetime.now(timezone.utc) now_time = datetime.now(timezone.utc)
new_time = now_time + timedelta(seconds=delay) new_time = now_time + timedelta(seconds=accrued_delay) + timedelta(seconds=delay)
start_time = new_time.isoformat() start_time = new_time.isoformat()
location_item = { location_item = {
loc_id: {
"loc_id": loc_id, "loc_id": loc_id,
"latitude": latitude, "latitude": latitude,
"longitude": longitude, "longitude": longitude,
"delay": delay, "delay": delay,
"start": start_time "start": start_time
} }
}
self.context.queue_list.append(location_item)
resp = { resp = {
"status": "added", "status": "added",
"message": f"Location {loc_id} added to the queue", "message": f"Location {loc_id} added to the queue",
"item": location_item "item": location_item
} }
await self.context.queue.put(loc_id)
add_item(loc_id, location_item)
logger.info("Location %s added to the queue", loc_id)
else: else:
resp = {"status": "error", "message": "Invalid location data"} resp = {"status": "error", "message": "Invalid location data"}
return generate_http_response(resp) return generate_http_response(resp)
@@ -1040,8 +1045,9 @@ class LocationSimulationTestQueue(LocationSimulationBase):
async def play_queue(self, disable_sleep: bool = False, timing_randomness_range: int = 0) -> None: async def play_queue(self, disable_sleep: bool = False, timing_randomness_range: int = 0) -> None:
while True: while True:
while self.context.queue_state == "PAUSED": if self.context.queue_state == "PAUSED":
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
continue
if self.context.queue_state == "SHUTDOWN": if self.context.queue_state == "SHUTDOWN":
break break
loc_id = await self.context.queue.get() loc_id = await self.context.queue.get()
@@ -1051,6 +1057,7 @@ class LocationSimulationTestQueue(LocationSimulationBase):
latitude = location_item.get("latitude") latitude = location_item.get("latitude")
longitude = location_item.get("longitude") longitude = location_item.get("longitude")
delay = location_item.get("delay") delay = location_item.get("delay")
delay = 0 if delay is None else delay
start_time = location_item.get("start_time") start_time = location_item.get("start_time")
if self.context.set_location_enabled: if self.context.set_location_enabled:
if delay > 0 and not disable_sleep: if delay > 0 and not disable_sleep:
@@ -1075,9 +1082,7 @@ class LocationSimulationTestQueue(LocationSimulationBase):
await self.set(latitude, longitude) await self.set(latitude, longitude)
self.context.latitude = latitude self.context.latitude = latitude
self.context.longitude = longitude self.context.longitude = longitude
self.context.loc_id = loc_id
await self.context.sio.emit( await self.context.sio.emit(
"simulation_status", "simulation_status",
{ {
"status": self.context.simulation_active, "status": self.context.simulation_active,

171
uv.lock generated
View File

@@ -95,7 +95,9 @@ source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "fastapi" }, { name = "fastapi" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "pyicloud" },
{ name = "pymobiledevice3" }, { name = "pymobiledevice3" },
{ name = "python-dotenv" },
{ name = "python-socketio" }, { name = "python-socketio" },
{ name = "typing" }, { name = "typing" },
{ name = "uvicorn" }, { name = "uvicorn" },
@@ -105,7 +107,9 @@ dependencies = [
requires-dist = [ requires-dist = [
{ name = "fastapi", specifier = "==0.135.1" }, { name = "fastapi", specifier = "==0.135.1" },
{ name = "pydantic", specifier = "==2.12.5" }, { name = "pydantic", specifier = "==2.12.5" },
{ name = "pyicloud", specifier = ">=2.4.1" },
{ name = "pymobiledevice3", specifier = "==9.0.0" }, { name = "pymobiledevice3", specifier = "==9.0.0" },
{ name = "python-dotenv", specifier = ">=1.2.2" },
{ name = "python-socketio", specifier = "==5.16.1" }, { name = "python-socketio", specifier = "==5.16.1" },
{ name = "typing", specifier = "==3.10.0.0" }, { name = "typing", specifier = "==3.10.0.0" },
{ name = "uvicorn", specifier = "==0.41.0" }, { name = "uvicorn", specifier = "==0.41.0" },
@@ -412,6 +416,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/72/42e900510195b23a56bde950d26a51f8b723846bfcaa0286e90287f0422b/fastapi-0.135.1-py3-none-any.whl", hash = "sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e", size = 116999, upload-time = "2026-03-01T18:18:30.831Z" }, { url = "https://files.pythonhosted.org/packages/e4/72/42e900510195b23a56bde950d26a51f8b723846bfcaa0286e90287f0422b/fastapi-0.135.1-py3-none-any.whl", hash = "sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e", size = 116999, upload-time = "2026-03-01T18:18:30.831Z" },
] ]
[[package]]
name = "fido2"
version = "2.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e7/3c/c65377e48c144afca6b02c69f10c0fe936db556096a4e2c9798e2aa72db6/fido2-2.1.1.tar.gz", hash = "sha256:f1379f845870cc7fc64c7f07323c3ce41e8c96c37054e79e0acd5630b3fec5ac", size = 4455940, upload-time = "2026-01-19T11:08:34.683Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e2/ab/d0fa89cc4b982800dd88daa799612f11642bf9393851715d9eaeba3cfcac/fido2-2.1.1-py3-none-any.whl", hash = "sha256:f85c16c8084abf6530b6c6ec3a0cf8575943321842e06916686943a8b784182c", size = 226945, upload-time = "2026-01-19T11:08:29.675Z" },
]
[[package]] [[package]]
name = "gpxpy" name = "gpxpy"
version = "1.6.2" version = "1.6.2"
@@ -541,6 +557,39 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" },
] ]
[[package]]
name = "jaraco-classes"
version = "3.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "more-itertools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" },
]
[[package]]
name = "jaraco-context"
version = "6.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" },
]
[[package]]
name = "jaraco-functools"
version = "4.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "more-itertools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" },
]
[[package]] [[package]]
name = "jedi" name = "jedi"
version = "0.19.2" version = "0.19.2"
@@ -553,6 +602,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" },
] ]
[[package]]
name = "jeepney"
version = "0.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" },
]
[[package]] [[package]]
name = "jinxed" name = "jinxed"
version = "1.3.0" version = "1.3.0"
@@ -565,6 +623,36 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/27/e3/0e0014d6ab159d48189e92044ace13b1e1fe9aa3024ba9f4e8cf172aa7c2/jinxed-1.3.0-py2.py3-none-any.whl", hash = "sha256:b993189f39dc2d7504d802152671535b06d380b26d78070559551cbf92df4fc5", size = 33085, upload-time = "2024-07-31T22:39:17.426Z" }, { url = "https://files.pythonhosted.org/packages/27/e3/0e0014d6ab159d48189e92044ace13b1e1fe9aa3024ba9f4e8cf172aa7c2/jinxed-1.3.0-py2.py3-none-any.whl", hash = "sha256:b993189f39dc2d7504d802152671535b06d380b26d78070559551cbf92df4fc5", size = 33085, upload-time = "2024-07-31T22:39:17.426Z" },
] ]
[[package]]
name = "keyring"
version = "25.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jaraco-classes" },
{ name = "jaraco-context" },
{ name = "jaraco-functools" },
{ name = "jeepney", marker = "sys_platform == 'linux'" },
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
{ name = "secretstorage", marker = "sys_platform == 'linux'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" },
]
[[package]]
name = "keyrings-alt"
version = "5.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jaraco-classes" },
{ name = "jaraco-context" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5c/7b/e3bf53326e0753bee11813337b1391179582ba5c6851b13e0d9502d15a50/keyrings_alt-5.0.2.tar.gz", hash = "sha256:8f097ebe9dc8b185106502b8cdb066c926d2180e13b4689fd4771a3eab7d69fb", size = 29229, upload-time = "2024-08-14T01:09:28.12Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/0d/9c59313ab43d0858a9a665e80763bd830dc78d5f379afc3815e123c486c2/keyrings.alt-5.0.2-py3-none-any.whl", hash = "sha256:6be74693192f3f37bbb752bfac9b86e6177076b17d2ac12a390f1d6abff8ac7c", size = 17930, upload-time = "2024-08-14T01:09:26.785Z" },
]
[[package]] [[package]]
name = "la-panic" name = "la-panic"
version = "0.5.0" version = "0.5.0"
@@ -631,6 +719,15 @@ 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" }, { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
] ]
[[package]]
name = "more-itertools"
version = "10.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" },
]
[[package]] [[package]]
name = "opack2" name = "opack2"
version = "0.0.1" version = "0.0.1"
@@ -897,6 +994,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/96/17/58ee1f079114ea360601ccd331fd73032701abfe9278495e3dec0c35ef3f/pygnuutils-0.1.1-py3-none-any.whl", hash = "sha256:3b690540cc13f2c763250ee5cc647e9c81055d1002b1bcf7ac07ea6d259a21c5", size = 46351, upload-time = "2023-05-12T12:56:58.211Z" }, { url = "https://files.pythonhosted.org/packages/96/17/58ee1f079114ea360601ccd331fd73032701abfe9278495e3dec0c35ef3f/pygnuutils-0.1.1-py3-none-any.whl", hash = "sha256:3b690540cc13f2c763250ee5cc647e9c81055d1002b1bcf7ac07ea6d259a21c5", size = 46351, upload-time = "2023-05-12T12:56:58.211Z" },
] ]
[[package]]
name = "pyicloud"
version = "2.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "click" },
{ name = "fido2" },
{ name = "keyring" },
{ name = "keyrings-alt" },
{ name = "requests" },
{ name = "srp" },
{ name = "tzlocal" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0c/f0/48aeab9d92690b6a7cf31200159c369441cdae1ac9d93d6bf69c096bf001/pyicloud-2.4.1.tar.gz", hash = "sha256:9c13bc46e08cabd87c4d4418133b5a303f30c3ef0478b6d1b13aa74c2e5334f6", size = 141404, upload-time = "2026-02-21T17:08:18.109Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/36/d2/875b873d54d3bc7dad37b50fd255b357f8adaa105af5bb2b02443cafc783/pyicloud-2.4.1-py3-none-any.whl", hash = "sha256:a767ada7cc2961428f8c2d0ce327102ae7666e3835610945409247fcf9d85e68", size = 66974, upload-time = "2026-02-21T17:08:16.196Z" },
]
[[package]] [[package]]
name = "pyimg4" name = "pyimg4"
version = "0.8.8" version = "0.8.8"
@@ -1009,6 +1125,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
] ]
[[package]]
name = "python-dotenv"
version = "1.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
]
[[package]] [[package]]
name = "python-engineio" name = "python-engineio"
version = "4.13.1" version = "4.13.1"
@@ -1071,6 +1196,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
] ]
[[package]]
name = "pywin32-ctypes"
version = "0.2.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" },
]
[[package]] [[package]]
name = "qh3" name = "qh3"
version = "1.6.0" version = "1.6.0"
@@ -1168,6 +1302,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/b6/049c75d399ccf6e25abea0652b85bf7e7e101e0300aa9c1d284ad7061c0b/runs-1.3.0-py3-none-any.whl", hash = "sha256:e71a551cfa8da9ef882cac1d5a108bda78c9edee5b8d87e37c1003da5b6a7bed", size = 6406, upload-time = "2026-02-03T15:59:59.96Z" }, { url = "https://files.pythonhosted.org/packages/4f/b6/049c75d399ccf6e25abea0652b85bf7e7e101e0300aa9c1d284ad7061c0b/runs-1.3.0-py3-none-any.whl", hash = "sha256:e71a551cfa8da9ef882cac1d5a108bda78c9edee5b8d87e37c1003da5b6a7bed", size = 6406, upload-time = "2026-02-03T15:59:59.96Z" },
] ]
[[package]]
name = "secretstorage"
version = "3.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "jeepney" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" },
]
[[package]] [[package]]
name = "setuptools" name = "setuptools"
version = "82.0.0" version = "82.0.0"
@@ -1207,6 +1354,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
] ]
[[package]]
name = "srp"
version = "1.0.22"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8d/fb/9210875dd162d3977580407b1c5ce6e779e770b8197a0de76819144a9755/srp-1.0.22.tar.gz", hash = "sha256:f330d0ec7387e2ac8577487b164963155d4a031bca6e2024f1b0930eb92baa5d", size = 22472, upload-time = "2024-11-01T21:52:54.006Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/89/75/5352c3ebd26e7d119042ae8de07354435a19c77fa2b44058fa97a1416783/srp-1.0.22-py3-none-any.whl", hash = "sha256:35aa8af053285a35683eb37182dcb2e46dbd85c7075d28e139f200d6bf16ea43", size = 25347, upload-time = "2024-11-01T21:52:53.021Z" },
]
[[package]] [[package]]
name = "srptools" name = "srptools"
version = "1.0.1" version = "1.0.1"
@@ -1353,6 +1512,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
] ]
[[package]]
name = "tzlocal"
version = "5.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" },
]
[[package]] [[package]]
name = "urllib3" name = "urllib3"
version = "2.6.3" version = "2.6.3"