extensive changes
This commit is contained in:
10
.idea/back-end.iml
generated
10
.idea/back-end.iml
generated
@@ -1,5 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module version="4">
|
||||
<module external.system.id="java-source" type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="jdk" jdkName="uv (pymd3_vue_location_sim)" jdkType="Python SDK" />
|
||||
</component>
|
||||
<component name="PyDocumentationSettings">
|
||||
<option name="format" value="PLAIN" />
|
||||
<option name="myDocStringFormat" value="Plain" />
|
||||
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/back-end.iml" filepath="$PROJECT_DIR$/.idea/back-end.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.14
|
||||
Binary file not shown.
Binary file not shown.
24
cookies/willbrunogmailcom.cookiejar
Normal file
24
cookies/willbrunogmailcom.cookiejar
Normal file
@@ -0,0 +1,24 @@
|
||||
#LWP-Cookies-2.0
|
||||
Set-Cookie3: dslang=US-EN; path="/"; domain=.apple.com; path_spec; secure; discard; HttpOnly=None; version=0
|
||||
Set-Cookie3: site=USA; path="/"; domain=.apple.com; path_spec; secure; discard; HttpOnly=None; version=0
|
||||
Set-Cookie3: acn01="vWB+8XG/BRIWimhzMz5URIMyy8lepYRnuuCxqAANNsVFbcxf"; path="/"; domain=.apple.com; path_spec; secure; expires="2027-03-27 09:37:26Z"; HttpOnly=None; version=0
|
||||
Set-Cookie3: aasp=90537484E4B81989243BAABCFA1DE44F53876FB5F9C433FD2C4687BD0D409642748545163AD333F3CDBBDDC6128BF84382082E6C0A4E27921C8792F0883DABFBFE8534D699F84266A14F842D6502C6902F2F00DC126EBD3E7D5FF7C490D9FE6740C97EFA884B52031C2E5BB2469A785CBDD236AD7014E383; path="/"; domain=.idmsa.apple.com; path_spec; secure; discard; HttpOnly=None; version=0
|
||||
Set-Cookie3: DES580750186337023c50d1415a6e6ca44a2="HSARMTKNSRVXWFlajR2ecD1662phQjqU9vXxnL49ZjypuVYYXHDpA3wTiX6Mf2J4WDlIhZj52z81aDOuz+VC80bVhV41TSNN4ggoPjW8WnsQrjniTQYkgJycPQNnzhkK4hfe2AMrr/bhrJJm8sHHc+Oh1HUckN6T7T4c1bmf2Qg9tRwsdRDNyMMyFH/Ml/cQlWKj39/YHlY=SRVX"; path="/"; domain=.idmsa.apple.com; path_spec; secure; expires="2026-04-21 03:09:12Z"; HttpOnly=None; version=0
|
||||
Set-Cookie3: X-APPLE-UNIQUE-CLIENT-ID="\"BA==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; discard; version=0
|
||||
Set-Cookie3: X-APPLE-WEBAUTH-LOGIN="\"v=1:t=BA==BST_IAAAAAAABLwIAAAAAGnGXmMRDmdzLmljbG91ZC5hdXRovQCH_epzZatm6b76myo9LfRsbXIfkwxREvo7U3efqnjYgORbMDpBEJyshI1bhC-Ww30ipqYT87rp7mJxkcXRw4HYHvxJCs1rC9M6JWbFvrmUTu4RuVuaVYSNHRYAz8-DS6bagfD6XUHY-5Xmqf2fnmeGqZfNag~~\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; discard; HttpOnly=None; version=0
|
||||
Set-Cookie3: X-APPLE-WEBAUTH-VALIDATE="\"v=1:t=BA==BST_IAAAAAAABLwIAAAAAGnGXmMRDmdzLmljbG91ZC5hdXRovQCH_epzZatm6b76myo9LfRsbXIfkwxREvo7U3efqnjYgORbMDpBEJyshI1bhC-Ww30ipqYT87rp7mJxkcXRw4HYHvxJCs1rC9M6JWbFvrmUTllbuUPSPqBVc1raHL34Cl5lu7SfciATbqjQE4KgZchJLIo6PQ~~\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; discard; version=0
|
||||
Set-Cookie3: X-APPLE-WEBAUTH-USER="\"v=1:s=1:d=157320350\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-04-26 10:39:35Z"; HttpOnly=None; version=0
|
||||
Set-Cookie3: X_APPLE_WEB_KB-FHMLYL_TPMN_3A8D3KIPPI0C_EC="\"v=1:t=BA==BST_IAAAAAAABLwIAAAAAGm_OLQRDmdzLmljbG91ZC5hdXRovQD38nYoxQenHW9WggeFKkoDa8I8zeKoOshv6I4dsZQalR2itry1r6kUZe9d_BZan1W-oKlImTrYi_-Vt5Q4YEJWJITWeqN8QChxvbTXB0o8sQ-wAIzBL1J5sQIRBqMadrtP5U0wslkRg0u0AguK20CM4TGoGg~~\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-21 00:32:52Z"; HttpOnly=None; version=0
|
||||
Set-Cookie3: X-APPLE-DS-WEB-SESSION-TOKEN="\"AQHO22bhY5UiUtuYBNJRupGy4tIGYR08ChFW77qgTTPDpPof4ZOPHhm2rGQ7OnoHchQlJLXG77T2BEoZxUO7ZknuRUUoK/j9/XTHYwv92CgFs/STC7oB1aMYmfddwNigbg9L5v40kX3Fmg6teX34bGYuOfqoHKTCuVxhllBNxZsKcPsKLGqRdZ4b3thHu+Y6+uVq5mAT9RnUbYAMD7hYP1Vr4aq+6CYi4+TdtO02SngpGcCRC5BR3PJh2udSG2YSGk5T+SBb4uBx94CzF2116QQWR0WjwSpqJ2K881lENtE3Kvf0pmM5mIw9fLdSgDi6kqP8iJHdni85QYpuTasMzd7ROWih6OqSV0U3O62X8ix+Xf3P1e/Fes7prNxjME+tcORKftTockp3c5U28DKcnZYYYuBRh+BNaAqVIAMg7AW9dNJUuTWdzVtUPhkLgvyTal625iiSh7Q6wsIISa03Cjtueli3NV++Z3R2jFvTQ/mC1lH9U//3ul+UPqblKCBy7SKGi/oph/PIroggTUwx/tvjQxCQf8RVkGg+8T0UE7rj6wDuaYu1i7twt7o1QsBlefEpVU9fYhCK3sq5aydhLRsb47wX42WhftBRTDenq7SwaHJZfxQ3T4NVULg8OsuCeuO1Gejnbylh9BI3Ws+I5k0ZNOaZtAWBlW8dsa5KrEdr+cRG0oV4EKPGTbt/mhvdFA/Kes/ksfI27DkKDmQ0MVj7nTTXQSkxeeQWEuP3T5LSNaYGDl+E6zqn5SsTt1RGxYMj3SHfSzxcLjNk7RoGcNle45L+V1wA5G8=\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-04-26 10:39:35Z"; HttpOnly=None; version=0
|
||||
Set-Cookie3: X-APPLE-WEBAUTH-HSA-TRUST="\"28a33818a1dce9a0eecde38e7c8fcc6f080b70bc9feb505599fb2855903a4792_HSARMTKNSRVXWFlajR2ecD1662phQjqU9vXxnL49ZjypuVYYXHDpA3wTiX6Mf2J4WDlIhZj52z81aDOuz+VC80bVhV41TSNN4ggoPjW8WnsQrjniTQYkgJycPQNnzhkK4hfe2AMrr/bhrJJm8sHHc+Oh1HUckN6T7T4c1bmf2Qg9tRwsdRDNyMMyFH/Ml/cQlWKj39/YHlY=SRVX\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-06-25 10:39:31Z"; HttpOnly=None; version=0
|
||||
Set-Cookie3: X-APPLE-WEBAUTH-PCS-Events="\"S2V5QXBwbDoBAAAA8QQnAADHJQyTmjkr1lRgHCHPONwUkefefsNrEGp6R9lQhFZ2hQ==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-04-26 10:39:35Z"; HttpOnly=None; version=0
|
||||
Set-Cookie3: X-APPLE-WEBAUTH-PCS-Documents="\"S2V5QXBwbDoBAAAAAgQnAAATRltQsTNPTggWr2+h+Ck7taXy1Nfd/0gTtx4/mgZMuQ==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-04-26 10:39:35Z"; HttpOnly=None; version=0
|
||||
Set-Cookie3: X-APPLE-WEBAUTH-PCS-Photos="\"S2V5QXBwbDoBAAAAAwQnAAC2DQKpt8kGxmIXjouUofkM/o9x9ZcMbMi8+kob3iqjtg==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-04-26 10:39:35Z"; HttpOnly=None; version=0
|
||||
Set-Cookie3: X-APPLE-WEBAUTH-PCS-Cloudkit="\"S2V5QXBwbDoBAAAABAQnAAD32tR1O4Wfr112mDd798bvrPSnGJ9SGs1UAu8ZxpArag==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-04-26 10:39:35Z"; HttpOnly=None; version=0
|
||||
Set-Cookie3: X-APPLE-WEBAUTH-PCS-Safari="\"S2V5QXBwbDoBAAAAFgQnAAAPeaT9obfpF4JkzvnTqn9iKRIFWjCO4Ibdd3ETw14+Kw==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-04-26 10:39:35Z"; HttpOnly=None; version=0
|
||||
Set-Cookie3: X-APPLE-WEBAUTH-PCS-Mail="\"S2V5QXBwbDoBAAAABwQnAADn5vf2KKnwETMPFGonUQAJwl4zr2g3X+iUWawSMgWOEg==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-04-26 10:39:35Z"; HttpOnly=None; version=0
|
||||
Set-Cookie3: X-APPLE-WEBAUTH-PCS-Notes="\"S2V5QXBwbDoBAAAACQQnAAALAg7ubdhjkrtB/lFNTwHudI77mxFx9gsBauT9LBhB6g==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-04-26 10:39:35Z"; HttpOnly=None; version=0
|
||||
Set-Cookie3: X-APPLE-WEBAUTH-PCS-News="\"S2V5QXBwbDoBAAAACwQnAAA5X6JspeIixJp6c7Uy+R2YynkGzH97NPfDPAkrO7uZ8g==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-04-26 10:39:35Z"; HttpOnly=None; version=0
|
||||
Set-Cookie3: X-APPLE-WEBAUTH-PCS-Sharing="\"S2V5QXBwbDoBAAAADAQnAADz1ui+JqfV6Fy6AI0Z6DScPVidn3wzooDronhTKnsLmQ==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-04-26 10:39:35Z"; HttpOnly=None; version=0
|
||||
Set-Cookie3: X-APPLE-WEBAUTH-TOKEN="\"v=2:t=BA==BST_IAAAAAAABLwIAAAAAGnGcucRDmdzLmljbG91ZC5hdXRovQBTsTtaO6tyhJbzfEVId4DPvNxvrOsTyKuhGBini_Ov9HBK9EgpLZCsfHxc0R0TWp-8AWGCWsLJSRkerU_Wt4CQG-vY_elLY1vevhKMmIi2CdVVyTBenEo_PyeAkNds0z9SjSi5a5z5rBqJpSysM1UyXNTTcg~~\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-04-10 12:07:03Z"; HttpOnly=None; version=0
|
||||
Set-Cookie3: xr_3n2093n1a="BW/k2XvBcxLsX55iCOOGgMojIY0Q+/Ey61qJDHU0TQE="; path="/"; domain=p144-fmipweb.icloud.com; path_spec; secure; discard; HttpOnly=None; version=0
|
||||
1
cookies/willbrunogmailcom.session
Normal file
1
cookies/willbrunogmailcom.session
Normal file
@@ -0,0 +1 @@
|
||||
{"client_id": "a803c3ce-2586-11f1-a724-8f6777a1d2b5", "session_id": "90537484E4B81989243BAABCFA1DE44F53876FB5F9C433FD2C4687BD0D409642748545163AD333F3CDBBDDC6128BF84382082E6C0A4E27921C8792F0883DABFBFE8534D699F84266A14F842D6502C6902F2F00DC126EBD3E7D5FF7C490D9FE6740C97EFA884B52031C2E5BB2469A785CBDD236AD7014E383", "auth_attributes": "CpWqHR6Y8JEnXubiHUHm2K5yCpOQORPPkclAkiMbW2nsEkSK2oVF24FHI3QNNw4cfBQfJOSEdRvVtGKtbi/Zxzbma2uaF9uD1kAlxlin4oPpxzNH5xKC472p08YUBkvusVaZ4wPqv6Zv+MZefISD/f/5A3vIn2fTZbXgImoKKaTLlwSn5wOcaJYoJYAhWgak+YPWeGLAdm1oGuXvTkCZe3gYtizo8lu6d4SBEjQ18GJ0BAN8D+BRxOb9B1AhxnzUd25aKq4CXzkRmLLCQWX0AA02xVKHQTE=", "scnt": "AAAA-jkwNTM3NDg0RTRCODE5ODkyNDNCQUFCQ0ZBMURFNDRGNTM4NzZGQjVGOUM0MzNGRDJDNDY4N0JEMEQ0MDk2NDI3NDg1NDUxNjNBRDMzM0YzQ0RCQkREQzYxMjhCRjg0MzgyMDgyRTZDMEE0RTI3OTIxQzg3OTJGMDg4M0RBQkZCRkU4NTM0RDY5OUY4NDI2NkExNEY4NDJENjUwMkM2OTAyRjJGMDBEQzEyNkVCRDNFN0Q1RkY3QzQ5MEQ5RkU2NzQwQzk3RUZBODg0QjUyMDMxQzJFNUJCMjQ2OUE3ODVDQkREMjM2QUQ3MDE0RTM4M3wyAAABnS61l867bZUy_24a4kaFbwGNgVJFgWQLrkDm8d_dAuxiqTUQBdnQ9m4C0ia8AA02xTey6hfMfAIuPPlov20tqtmvTh-N23rQ3Q4jQeaNx6uMrtovmQ", "account_country": "USA", "session_token": "idWfvF23GAFDtpDv51Mnga3iu7fJCAdyDaw22SorSHz5JTRc/SottqqYD2KGG/56FLnY9lHAZnJUz8APx24/Y5Udfi45jsW6FmUZ1btGt4ihu0lGg5zIWAxY6B9AHVe4ROZ0mI90mvT5jajGL56Ke3fhL6ncIYP/LTQz2EJfwZtd02MgeQ5j1W0IfF8zmhlDyb9eAn1dC6X61hh7d/DZdLDFfZxxyXkBhemi6dNQKDwAUnYl7YKlShZDLUFNFG0k3m2yiLQANI+InL7fDHPEdyiNaGh9Lu5VLcq7hgNFqIqN6JFjUBAr/cauSeTHHmGPtH9Ry4VWMpl3Ukd+nppFylMCazMj3hvjXpSIkMR7B41pxxRA1FtjgYryeUkexcQXx8vrMYwrkVdtOB4p2xRfX7xXzMGH5VpwpFO75/nO10A4n+YEXg/JaPW31R8zLBvnZd6oL5ElCGIlXQadsPM0mzDdbvM2dXhMQaJ4dLQC9p4XhIrEkGhV7op5YeLWfNiDg3tBDLYn+WVEw7xWmEp6/NRZqFmctEDgJak9AwUV5ostJ4O69UxiaqkL8VgYysHBVcN0Tl6XwVHvh+o5LBL3C9fAsQ1ZIMNb088Sl8iRHbwOxa/UfiQ9RWiTfUhlGzNhd6nxOLIN5SaBV4Z3Xu4asRpP00Ue0igWBzKmn6nTk5lEMABMA+fiDVYdpuN0dNWbscZwLLozBsToayEgfDD/KdYCdqEV8y1mL97z7nSjB1VAfs5x3Skhq6AhKIe9p1Y9AbsOJP8vs+p+G+xHi/sKKzz8r3g27yNpP7emIO6lioYeuHTg341QSGJtPofrq9d+04J08dgnvxJ6dsfyGrYYGjIV9OYidbhMlXkXPofkMVA+1SCjVi1pQOPtlZ8ygzJK4+OiLK8NJJR5tcaMNSZ0l16RIoZw+IdEi2invyN/SXQqjVyie10sEbRarN8cVOJ58AANNsVYFrWa", "trust_eligible": "true", "grant_code": "cca13786ef08a405f94e575e9e8501ab7.0.mrzwz.y2Ef8JBOyp06v2uZhFL20g", "trust_token": "HSARMTKNSRVXWFlajR2ecD1662phQjqU9vXxnL49ZjypuVYYXHDpA3wTiX6Mf2J4WDlIhZj52z81aDOuz+VC80bVhV41TSNN4ggoPjW8WnsQrjniTQYkgJycPQNnzhkK4hfe2AMrr/bhrJJm8sHHc+Oh1HUckN6T7T4c1bmf2Qg9tRwsdRDNyMMyFH/Ml/cQlWKj39/YHlY=SRVX"}
|
||||
27
database.py
27
database.py
@@ -1,27 +0,0 @@
|
||||
from sqlalchemy import create_engine
|
||||
from dqlalchemy.orm import sessionmaker, declaritive_base
|
||||
|
||||
DATABASE_URL = "sqlite:///./locations.db"
|
||||
|
||||
engine = create_engine(
|
||||
DATABASE_URL, connect_engine("check_same_thread": false}
|
||||
)
|
||||
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
def get_db():
|
||||
"""Dependency for getting database session."""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def create_all_tables():
|
||||
"""Creates all tables defined with Base in the database."""
|
||||
# Note: import models before calling create_all_tables()
|
||||
Base.metadata_create_all(bind=engine)
|
||||
print("Database and tables created.")
|
||||
|
||||
58
db_models.py
58
db_models.py
@@ -1,58 +0,0 @@
|
||||
from sqlalchemy import Column, Integer, String, Float, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from .database import Base
|
||||
|
||||
|
||||
class Location(Base):
|
||||
"""
|
||||
SQLAlchemy model for the 'locations' table.
|
||||
"""
|
||||
__tablename__ = "locations"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, unique=False, index=True)
|
||||
address = Column(String, unique=False, index=True)
|
||||
latitude = Column(Float, unique=False, index=True)
|
||||
longitude = Column(Float, unique=False, index=True)
|
||||
is_favorite = Column(Boolean, unique=False, index=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Location(name='{self.name}', address='{self.address}', latitude='{self.latitude}', longitude='{self.longitude}', is_favorite='{self.is_favorite}')>"
|
||||
|
||||
class Route(Base):
|
||||
"""
|
||||
SQLAlchemy model for the 'routes' table.
|
||||
"""
|
||||
__tablename__ = "routes"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, index=True)
|
||||
|
||||
# Start and Endpoints (One-to-Many relationship)
|
||||
start_location_id = Column(Integer, ForeignKey('locations.id'))
|
||||
end_location_id = Column(Integer, ForeignKey('locations.id'))
|
||||
|
||||
start_location = relationship("Location", foreign_keys=[start_id])
|
||||
end_location = relationship("Location", foreign_keys=[end_id])
|
||||
|
||||
# Relationship to get waypoints ordered
|
||||
waypoints = relationship("Waypoint", order_by="Waypoint.order", back_populates="route")
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Route(name='{self.name}', start='{self.start_location.name}', end='{self.end_location.name}')>"
|
||||
|
||||
# Association Table for Many-to-Many relationsjip (Routes <-> Waypoints)
|
||||
class Waypoint(Base):
|
||||
"""
|
||||
SQLAlchemy model for the 'waypoints' table.
|
||||
"""
|
||||
__tablename__ = 'waypoints'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
route_id = Column(Integer, ForeignKey('routes.id'))
|
||||
location_id = Column(Integer, ForeignKey('locations.id'))
|
||||
order = Column(Integer, nullable=False)
|
||||
|
||||
route = relationship("Route", back_populates="waypoints")
|
||||
location = relationship("Location")
|
||||
111
icloud.py
111
icloud.py
@@ -1,111 +0,0 @@
|
||||
import asyncio
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
|
||||
from pyicloud import PyiCloudService
|
||||
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:
|
||||
def __init__(self, queue: asyncio.Queue, token_file="icloud_token.txt"):
|
||||
self.username = os.getenv("APPLE_ID")
|
||||
self.password = os.getenv("APPLE_PW")
|
||||
self.token_file = token_file
|
||||
self.queue = queue
|
||||
self.api = None
|
||||
self.device = None
|
||||
self.running = True
|
||||
|
||||
async def authenticate(self):
|
||||
"""Authenticates with iCloud, handling 2FA and token storage."""
|
||||
if os.path.exists(self.token_file):
|
||||
print("Loading stored session...")
|
||||
self.api = PyiCloudService(self.username, cookie_directory="./cookies")
|
||||
else:
|
||||
print("No stored session. Authenticating...")
|
||||
self.api = PyiCloudService(self.username, self.password, cookie_directory="./cookies")
|
||||
|
||||
if self.api.requires_2fa:
|
||||
print("Two-factor authentication required.")
|
||||
code = input("Enter the code you received: ")
|
||||
result = self.api.validate_2fa_code(code)
|
||||
print(f"Code validation result: {result}")
|
||||
if not result:
|
||||
print("Failed to verify 2FA code")
|
||||
return False
|
||||
|
||||
# Trust the session
|
||||
self.api.trust_session()
|
||||
|
||||
print("Successfully authenticated.")
|
||||
return True
|
||||
|
||||
async def get_location(self):
|
||||
"""Fetches the latest latitude and longitude."""
|
||||
if not self.api:
|
||||
await self.authenticate()
|
||||
|
||||
# Refresh API data
|
||||
self.api.refresh_client()
|
||||
|
||||
# Find the device (modify name to match your iPhone name in iCloud)
|
||||
if not self.device:
|
||||
# Assuming you have devices, pick the first or match by name
|
||||
self.device = self.api.devices[0]
|
||||
print(f"Monitoring device: {self.device.name()}")
|
||||
|
||||
location = self.device.location()
|
||||
if location:
|
||||
return location['latitude'], location['longitude'], location['timeStamp']
|
||||
return None
|
||||
|
||||
def start(self):
|
||||
self.running = True
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
|
||||
async def run_monitor(self, interval=60):
|
||||
"""Runs the monitor loop."""
|
||||
if not await self.authenticate():
|
||||
return
|
||||
|
||||
if not self.running:
|
||||
self.start()
|
||||
|
||||
while self.running:
|
||||
logger.info("Starting iCloud FMF loop")
|
||||
try:
|
||||
lat, lon, ts = await self.get_location()
|
||||
print(f"[{ts}] Location: {lat}, {lon}")
|
||||
# Add your logic to update database/API here
|
||||
await self.queue.put(lat, lng, ts)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
# Re-authenticate if session expired
|
||||
await self.authenticate()
|
||||
|
||||
await asyncio.sleep(interval)
|
||||
|
||||
|
||||
50
main.bk
50
main.bk
@@ -1,50 +0,0 @@
|
||||
from flask import Flask, request, jsonify
|
||||
from flask_cors import CORS
|
||||
|
||||
app = Flask(__name__, static_folder="../front-end/dist/assets", template_folder="../front-end/dist")
|
||||
CORS(app)
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return app.send_static_file('index.html')
|
||||
|
||||
# Essential for handling client-side routing (e.g., Vue Router history mode)
|
||||
@app.route('/<path:path>')
|
||||
def static_files(path):
|
||||
return app.send_static_file(path)
|
||||
|
||||
@app.route('/api/set', methods=['POST'])
|
||||
def handle_json_data():
|
||||
data = request.get_json()
|
||||
|
||||
if not data:
|
||||
return jsonify({'error': 'Missing JSON data'}), 400
|
||||
|
||||
if 'lat' not in data:
|
||||
return jsonify({'error': 'Missing lat in JSON data'}), 400
|
||||
|
||||
if 'lng' not in data:
|
||||
return jsonify({'error': 'Missing lng in JSON data'}), 400
|
||||
|
||||
# Process the data (e.g., save to a database)
|
||||
lat = data['lat']
|
||||
lng = data['lng']
|
||||
|
||||
# Return a JSON response
|
||||
return jsonify({
|
||||
'message': f'lat: {lat}, lng: {lng} received successfully!',
|
||||
'data': data
|
||||
}), 200
|
||||
|
||||
@app.route('/api/status', methods=['GET'])
|
||||
def get_data():
|
||||
return jsonify(message="Hello from Flask API!")
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(error):
|
||||
return jsonify({'error': 'Not found'}), 404
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||
46
main.py
46
main.py
@@ -1,48 +1,16 @@
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import sys
|
||||
import socketio
|
||||
import tempfile
|
||||
from contextlib import nullcontext
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Optional, TextIO
|
||||
|
||||
from typing import Annotated
|
||||
import typer
|
||||
from typer_injector import InjectingTyper
|
||||
|
||||
from pymobiledevice3.bonjour import DEFAULT_BONJOUR_TIMEOUT, browse_remotepairing_manual_pairing
|
||||
from pymobiledevice3.cli.cli_common import (
|
||||
RSDServiceProviderDep,
|
||||
async_command,
|
||||
print_json,
|
||||
prompt_device_list,
|
||||
sudo_required,
|
||||
user_requested_colored_output,
|
||||
)
|
||||
from pymobiledevice3.common import get_home_folder
|
||||
from pymobiledevice3.exceptions import NoDeviceConnectedError
|
||||
from pymobiledevice3.pair_records import PAIRING_RECORD_EXT, get_remote_pairing_record_filename
|
||||
from pymobiledevice3.remote.common import ConnectionType, TunnelProtocol
|
||||
from pymobiledevice3.remote.module_imports import MAX_IDLE_TIMEOUT, start_tunnel, verify_tunnel_imports
|
||||
from pymobiledevice3.remote.remote_service_discovery import RSD_PORT
|
||||
from pymobiledevice3.remote.tunnel_service import (
|
||||
RemotePairingManualPairingService,
|
||||
get_core_device_tunnel_services,
|
||||
get_remote_pairing_tunnel_services,
|
||||
)
|
||||
from pymobiledevice3.remote.utils import get_rsds
|
||||
from pymobiledevice3.remote.common import TunnelProtocol
|
||||
from pymobiledevice3.remote.module_imports import verify_tunnel_imports
|
||||
from pymobiledevice3.tunneld.api import TUNNELD_DEFAULT_ADDRESS
|
||||
from pymobiledevice3.utils import run_in_loop
|
||||
from server import TunneldRunnerSio, LocationSimulationState, logger
|
||||
|
||||
from src.pymd3_vue_location_sim.server import TunneldRunnerSio, LocationSimulationState
|
||||
from src.pymd3_vue_location_sim.json_formatter import logger
|
||||
|
||||
def main():
|
||||
cli_tunneld(host="0.0.0.0", port=8000)
|
||||
|
||||
cli_tunneld(host="0.0.0.0", port=49151)
|
||||
|
||||
def cli_tunneld(
|
||||
host: Annotated[str, typer.Option(help="Address to bind the tunneld server to.")] = TUNNELD_DEFAULT_ADDRESS[0],
|
||||
@@ -93,4 +61,4 @@ def cli_tunneld(
|
||||
|
||||
# 4. Entry point (always last)
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
@@ -3,12 +3,18 @@ name = "back-end"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
"click>=8.3.1",
|
||||
"daemonize>=2.5.0",
|
||||
"fastapi==0.135.1",
|
||||
"geopy==2.4.1",
|
||||
"pydantic==2.12.5",
|
||||
"pyicloud>=2.4.1",
|
||||
"pymobiledevice3==9.0.0",
|
||||
"python-dotenv>=1.2.2",
|
||||
"python-socketio==5.16.1",
|
||||
"sqlalchemy>=2.0.48",
|
||||
"sqlalchemy-orm>=1.2.10",
|
||||
"typer>=0.24.1",
|
||||
"typing==3.10.0.0",
|
||||
"uvicorn==0.41.0",
|
||||
]
|
||||
|
||||
1074
server_recover.py
1074
server_recover.py
File diff suppressed because it is too large
Load Diff
37
socktest.py
37
socktest.py
@@ -1,37 +0,0 @@
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
import socketio
|
||||
|
||||
app = FastAPI()
|
||||
sio = socketio.AsyncServer(cors_allowed_origins="*", async_mode="asgi")
|
||||
socket_app = socketio.ASGIApp(sio, app)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def main():
|
||||
return {"message": "Hello World"}
|
||||
|
||||
|
||||
@sio.on("connect")
|
||||
async def connect(sid, env):
|
||||
print("New Client Connected to This id :" + " " + str(sid))
|
||||
await sio.emit("message", f"Hello Client, Welcome to Socket.IO Server {sid}")
|
||||
|
||||
|
||||
@sio.event
|
||||
async def message(sid, data):
|
||||
print("Message from Client: " + str(data))
|
||||
return True
|
||||
|
||||
@sio.on("disconnect")
|
||||
async def disconnect(sid):
|
||||
print("Client Disconnected: " + " " + str(sid))
|
||||
|
||||
@sio.on('*')
|
||||
async def any_event(event, sid, data):
|
||||
print("Unregisered event received, event: " + str(event) + " sid: " + str(sid) + " data: " + str(data))
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
"socktest:socket_app", host="0.0.0.0", port=8000, lifespan="on", reload=True
|
||||
)
|
||||
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
0
src/pymd3_vue_location_sim/__init__.py
Normal file
0
src/pymd3_vue_location_sim/__init__.py
Normal file
44
src/pymd3_vue_location_sim/database.py
Normal file
44
src/pymd3_vue_location_sim/database.py
Normal file
@@ -0,0 +1,44 @@
|
||||
|
||||
from typing import List
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
|
||||
from .db_models import Base, Location, Route, Waypoint
|
||||
|
||||
|
||||
DATABASE_URL = "sqlite+aiosqlite:///.locations.db"
|
||||
engine = create_async_engine(DATABASE_URL, echo=True)
|
||||
async_session_local = async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
async def init_db() -> None:
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
async def create_location(name: str, address: str, latitude: float, longitude: float, is_favorite: bool = False):
|
||||
async with async_session_local() as session:
|
||||
new_location = Location(name=name, address=address, latitude=latitude, longitude=longitude, is_favorite=is_favorite)
|
||||
session.add(new_location)
|
||||
await session.commit()
|
||||
return new_location
|
||||
|
||||
async def get_locations():
|
||||
async with async_session_local() as session:
|
||||
result = await session.execute(select(Location))
|
||||
return result.scalars().all()
|
||||
|
||||
async def create_route(name: str, origin_id: int, destination_id: int, waypoints_data: List[dict]):
|
||||
async with async_session_local() as session:
|
||||
new_route = Route(name=name, origin_id=origin_id, destination_id=destination_id)
|
||||
for wp in waypoints_data:
|
||||
new_route.waypoints.append(Waypoint(order=wp['order'], description=wp['description']))
|
||||
session.add(new_route)
|
||||
await session.commit()
|
||||
return new_route
|
||||
|
||||
async def get_routes():
|
||||
async with async_session() as session:
|
||||
# Use joinedload for efficient loading of relationships
|
||||
from sqlalchemy.orm import joinedload
|
||||
result = await session.execute(
|
||||
select(Route).options(joinedload(Route.waypoints))
|
||||
)
|
||||
return result.scalars().unique().all()
|
||||
42
src/pymd3_vue_location_sim/db_models.py
Normal file
42
src/pymd3_vue_location_sim/db_models.py
Normal file
@@ -0,0 +1,42 @@
|
||||
|
||||
from sqlalchemy import ForeignKey, String
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||
from sqlalchemy.ext.asyncio import AsyncAttrs
|
||||
from typing import List
|
||||
|
||||
|
||||
class Base(AsyncAttrs, DeclarativeBase):
|
||||
pass
|
||||
|
||||
class Location(Base):
|
||||
__tablename__ = "locations"
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(100))
|
||||
address: Mapped[str] = mapped_column(String(255))
|
||||
latitude: Mapped[float]
|
||||
longitude: Mapped[float]
|
||||
is_favorite: Mapped[bool] = mapped_column(default=False)
|
||||
|
||||
routes: Mapped[List["Route"]] = relationship(back_populates="destination", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class Route(Base):
|
||||
__tablename__ = "routes"
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(100))
|
||||
origin_id: Mapped[int] = mapped_column(ForeignKey("locations.id"))
|
||||
destination_id: Mapped[int] = mapped_column(ForeignKey("locations.id"))
|
||||
destination: Mapped["Location"] = relationship(back_populates="routes")
|
||||
|
||||
waypoints: Mapped[List["Waypoint"]] = relationship(back_populates="route", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
|
||||
class Waypoint(Base):
|
||||
__tablename__ = "waypoint"
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
order: Mapped[int]
|
||||
description: Mapped[str]
|
||||
route_id: Mapped[int] = mapped_column(ForeignKey("route.id"))
|
||||
route: Mapped["Route"] = relationship(back_populates="waypoints")
|
||||
|
||||
334
src/pymd3_vue_location_sim/icloud_monitor.py
Normal file
334
src/pymd3_vue_location_sim/icloud_monitor.py
Normal file
@@ -0,0 +1,334 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
|
||||
import click
|
||||
from dotenv import load_dotenv
|
||||
from pyicloud import PyiCloudService
|
||||
from pyicloud.exceptions import (
|
||||
PyiCloud2FARequiredException,
|
||||
PyiCloud2SARequiredException,
|
||||
PyiCloudAuthRequiredException,
|
||||
PyiCloudFailedLoginException,
|
||||
PyiCloudNoStoredPasswordAvailableException,
|
||||
PyiCloudPasswordException,
|
||||
)
|
||||
|
||||
from .models import iCloudReturnData
|
||||
|
||||
load_dotenv()
|
||||
logger = logging.getLogger("ios-api")
|
||||
AUTH_EXCEPTIONS = (
|
||||
PyiCloudAuthRequiredException,
|
||||
PyiCloudFailedLoginException,
|
||||
PyiCloudPasswordException,
|
||||
PyiCloud2FARequiredException,
|
||||
PyiCloud2SARequiredException,
|
||||
PyiCloudNoStoredPasswordAvailableException,
|
||||
)
|
||||
|
||||
class FindMyMonitor:
|
||||
def __init__(
|
||||
self,
|
||||
queue: asyncio.Queue,
|
||||
token_file: str = "icloud_token.txt",
|
||||
sio=None,
|
||||
get_client_sids: Callable[[], list[str]] | None = None,
|
||||
code_timeout_seconds: int = 180,
|
||||
):
|
||||
self.username = os.getenv("APPLE_ID")
|
||||
self.password = os.getenv("APPLE_PW")
|
||||
self.token_file = token_file
|
||||
self.selected_device_id = os.getenv("SELECTED_DEVICE_ID")
|
||||
self.selected_device_name = os.getenv("SELECTED_DEVICE_NAME")
|
||||
self.selected_device = None
|
||||
self.queue = queue
|
||||
self.api = None
|
||||
self.device = None
|
||||
self.running = True
|
||||
self.sio = sio
|
||||
self.get_client_sids = get_client_sids
|
||||
self.code_timeout_seconds = code_timeout_seconds
|
||||
self.auth_init_timeout_seconds = int(
|
||||
os.getenv("ICLOUD_AUTH_INIT_TIMEOUT_SECONDS", "0")
|
||||
)
|
||||
self._logged_candidates = False
|
||||
self._no_location_streak = 0
|
||||
self._auth_error_streak = 0
|
||||
|
||||
async def _request_code_from_vue(self, prompt: str) -> str | None:
|
||||
if self.sio is None or self.get_client_sids is None:
|
||||
logger.warning("2FA request skipped: Socket.IO context not configured")
|
||||
return None
|
||||
sids = self.get_client_sids()
|
||||
if not sids:
|
||||
logger.warning("2FA request skipped: no connected Socket.IO clients")
|
||||
return None
|
||||
|
||||
payload = {"prompt": prompt, "digits": 6}
|
||||
logger.info("Emitting icloud_2fa_request to %s connected client(s)", len(sids))
|
||||
for sid in sids:
|
||||
try:
|
||||
# Ask one connected UI client for the code and wait for ACK response.
|
||||
response = await self.sio.call(
|
||||
"icloud_2fa_request",
|
||||
payload,
|
||||
to=sid,
|
||||
namespace="/",
|
||||
timeout=self.code_timeout_seconds,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Failed to retrieve 2FA code from sid=%s", sid)
|
||||
continue
|
||||
|
||||
code = response.get("code") if isinstance(response, dict) else response
|
||||
code_str = str(code).strip() if code is not None else ""
|
||||
if len(code_str) == 6 and code_str.isdigit():
|
||||
return code_str
|
||||
logger.warning("Invalid 2FA code payload from sid=%s", sid)
|
||||
return None
|
||||
|
||||
async def authenticate(self):
|
||||
"""Authenticates with iCloud, handling 2FA and token storage."""
|
||||
if not self.username:
|
||||
logger.warning("APPLE_ID is not configured; skipping iCloud monitor authentication")
|
||||
return False
|
||||
has_token = os.path.exists(self.token_file)
|
||||
if not has_token and not self.password:
|
||||
logger.warning(
|
||||
"No stored iCloud session and APPLE_PW is not configured; skipping iCloud monitor authentication"
|
||||
)
|
||||
return False
|
||||
|
||||
if os.path.exists(self.token_file):
|
||||
print("Loading stored session...")
|
||||
try:
|
||||
init_task = asyncio.to_thread(
|
||||
PyiCloudService, self.username, cookie_directory="./cookies"
|
||||
)
|
||||
if self.auth_init_timeout_seconds > 0:
|
||||
self.api = await asyncio.wait_for(
|
||||
init_task, timeout=self.auth_init_timeout_seconds
|
||||
)
|
||||
else:
|
||||
self.api = await init_task
|
||||
except Exception as e:
|
||||
logger.exception("Failed to initialize iCloud session from cookies: %s", e)
|
||||
return False
|
||||
else:
|
||||
print("No stored session. Authenticating...")
|
||||
try:
|
||||
init_task = asyncio.to_thread(
|
||||
PyiCloudService,
|
||||
self.username,
|
||||
self.password,
|
||||
cookie_directory="./cookies",
|
||||
)
|
||||
if self.auth_init_timeout_seconds > 0:
|
||||
self.api = await asyncio.wait_for(
|
||||
init_task, timeout=self.auth_init_timeout_seconds
|
||||
)
|
||||
else:
|
||||
self.api = await init_task
|
||||
except Exception as e:
|
||||
logger.exception("Failed to initialize iCloud session with credentials: %s", e)
|
||||
return False
|
||||
|
||||
if self.api.requires_2fa:
|
||||
print("Two-factor authentication required.")
|
||||
code = await self._request_code_from_vue("Enter the 6-digit Apple verification code")
|
||||
if code is None:
|
||||
if sys.stdin and sys.stdin.isatty():
|
||||
code = str(
|
||||
await asyncio.to_thread(
|
||||
click.prompt,
|
||||
"Please enter the 6-digit code sent to your trusted device",
|
||||
type=int,
|
||||
)
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"2FA required but no interactive terminal or Vue responder is available; deferring authentication"
|
||||
)
|
||||
return False
|
||||
# Verify the code
|
||||
result = await asyncio.to_thread(self.api.validate_2fa_code, code)
|
||||
print(f"2FA validation result: {result}")
|
||||
|
||||
if not result:
|
||||
print("Failed to verify 2FA code")
|
||||
return False
|
||||
|
||||
# Trust the session
|
||||
await asyncio.to_thread(self.api.trust_session)
|
||||
|
||||
if self.api.requires_2sa:
|
||||
import textwrap
|
||||
print(textwrap.dedent("""
|
||||
Two-step authentication required.
|
||||
Please select a device to receive a SMS verification code:
|
||||
"""))
|
||||
# List available devices for 2SA
|
||||
for i, device in enumerate(self.api.trusted_devices):
|
||||
print(
|
||||
f" {i + 1}: {device.get('deviceName', 'Unknown device')} ({device.get('phoneNumber', 'Unknown number')})")
|
||||
|
||||
# Prompt the user for their choice
|
||||
if not (sys.stdin and sys.stdin.isatty()):
|
||||
logger.warning(
|
||||
"2SA required but no interactive terminal is available; deferring authentication"
|
||||
)
|
||||
return False
|
||||
device_index = await asyncio.to_thread(click.prompt, "Please select a device number", type=int) - 1
|
||||
device = self.api.trusted_devices[device_index]
|
||||
if not await asyncio.to_thread(self.api.send_verification_code, device):
|
||||
print("Failed to send verification code")
|
||||
return False
|
||||
|
||||
# Prompt the user to enter the verification code they received
|
||||
code = await asyncio.to_thread(click.prompt, "Please enter verification code", type=int)
|
||||
if not await asyncio.to_thread(self.api.validate_verification_code, device, code):
|
||||
print("Failed to verify verification code")
|
||||
return False
|
||||
|
||||
print("Successfully authenticated.")
|
||||
return True
|
||||
|
||||
async def get_location(self):
|
||||
"""Fetches the latest latitude and longitude."""
|
||||
if not self.api:
|
||||
await self.authenticate()
|
||||
|
||||
# pyicloud 2.4.1 refresh lives on the FindMy manager, not PyiCloudService.
|
||||
devices_manager = await asyncio.to_thread(lambda: self.api.devices)
|
||||
await asyncio.to_thread(devices_manager.refresh, True)
|
||||
|
||||
# One-time diagnostics to help verify exact device selection among duplicates.
|
||||
if not self._logged_candidates:
|
||||
for d in devices_manager:
|
||||
status = d.status()
|
||||
logger.debug(
|
||||
"iCloud candidate id=%s name=%s model=%s deviceStatus=%s has_location=%s features=%s",
|
||||
d.data.get("id"),
|
||||
d.name,
|
||||
d.model_name,
|
||||
status.get("deviceStatus"),
|
||||
d.data.get("location") is not None,
|
||||
d.data.get("features"),
|
||||
)
|
||||
self._logged_candidates = True
|
||||
|
||||
# Select by ID first (exact), then by name, then first device if no selectors set.
|
||||
self.selected_device = None
|
||||
for device in devices_manager:
|
||||
if self.selected_device_id and device.data.get("id") == self.selected_device_id:
|
||||
self.selected_device = device
|
||||
break
|
||||
if self.selected_device is None:
|
||||
for device in devices_manager:
|
||||
if self.selected_device_name and device.name == self.selected_device_name:
|
||||
self.selected_device = device
|
||||
break
|
||||
if self.selected_device is None and not self.selected_device_id and not self.selected_device_name:
|
||||
for device in devices_manager:
|
||||
self.selected_device = device
|
||||
break
|
||||
|
||||
if self.selected_device:
|
||||
location = self.selected_device.location
|
||||
status = self.selected_device.status()
|
||||
logger.info(
|
||||
"iCloud device=%s location_available=%s status=%s",
|
||||
self.selected_device.name,
|
||||
self.selected_device.location_available,
|
||||
status,
|
||||
)
|
||||
if location:
|
||||
data = {
|
||||
"latitude": location['latitude'],
|
||||
"longitude": location['longitude'],
|
||||
"timeStamp": location['timeStamp'],
|
||||
"altitude": location['altitude'],
|
||||
"horizontalAccuracy": location['horizontalAccuracy'],
|
||||
"verticalAccuracy": location['verticalAccuracy'],
|
||||
"batteryLevel": status['batteryLevel'],
|
||||
"deviceDisplayName": status['deviceDisplayName'],
|
||||
"deviceStatus": status['deviceStatus'],
|
||||
"name": status['name']
|
||||
}
|
||||
response = iCloudReturnData(**data)
|
||||
return response
|
||||
logger.info("Location payload is None for device=%s", self.selected_device.name)
|
||||
return None
|
||||
|
||||
logger.warning(
|
||||
"No iCloud device matched SELECTED_DEVICE_ID='%s' or SELECTED_DEVICE_NAME='%s'.",
|
||||
self.selected_device_id,
|
||||
self.selected_device_name,
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def start(self):
|
||||
self.running = True
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
|
||||
def _no_location_backoff_seconds(self, base_interval: int) -> int:
|
||||
schedule = [15, 30, 60, 120, 300]
|
||||
idx = min(self._no_location_streak - 1, len(schedule) - 1)
|
||||
return max(base_interval, schedule[idx])
|
||||
|
||||
def _auth_backoff_seconds(self, base_interval: int) -> int:
|
||||
schedule = [15, 30, 60, 120, 300]
|
||||
idx = min(self._auth_error_streak - 1, len(schedule) - 1)
|
||||
return max(base_interval, schedule[idx])
|
||||
|
||||
async def run_monitor(self, interval=60):
|
||||
"""Runs the monitor loop."""
|
||||
if not await self.authenticate():
|
||||
return
|
||||
|
||||
if not self.running:
|
||||
self.start()
|
||||
|
||||
while self.running:
|
||||
sleep_seconds = interval
|
||||
try:
|
||||
device_data = await self.get_location()
|
||||
if device_data is not None:
|
||||
self._no_location_streak = 0
|
||||
self._auth_error_streak = 0
|
||||
print(f"{device_data.timeStamp} - Location: {device_data.latitude}, {device_data.longitude}")
|
||||
await self.queue.put(device_data)
|
||||
else:
|
||||
self._no_location_streak += 1
|
||||
sleep_seconds = self._no_location_backoff_seconds(interval)
|
||||
logger.info(
|
||||
"No iCloud location found for device id='%s' name='%s' streak=%s next_retry=%ss",
|
||||
self.selected_device_id,
|
||||
self.selected_device_name,
|
||||
self._no_location_streak,
|
||||
sleep_seconds,
|
||||
)
|
||||
except AUTH_EXCEPTIONS as e:
|
||||
self._auth_error_streak += 1
|
||||
sleep_seconds = self._auth_backoff_seconds(interval)
|
||||
logger.warning(
|
||||
"iCloud auth error (%s). Re-authenticating; streak=%s next_retry=%ss",
|
||||
type(e).__name__,
|
||||
self._auth_error_streak,
|
||||
sleep_seconds,
|
||||
)
|
||||
try:
|
||||
await self.authenticate()
|
||||
except Exception:
|
||||
logger.exception("iCloud re-authentication failed")
|
||||
except Exception as e:
|
||||
logger.exception("iCloud monitor loop error: %s", e)
|
||||
sleep_seconds = max(interval, 30)
|
||||
|
||||
await asyncio.sleep(sleep_seconds)
|
||||
18
src/pymd3_vue_location_sim/json_formatter.py
Normal file
18
src/pymd3_vue_location_sim/json_formatter.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import logging
|
||||
import json
|
||||
|
||||
handler = logging.StreamHandler()
|
||||
root_logger = logging.getLogger()
|
||||
logger = logging.getLogger("ios-api")
|
||||
|
||||
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)
|
||||
213
src/pymd3_vue_location_sim/locationsimulation.py
Normal file
213
src/pymd3_vue_location_sim/locationsimulation.py
Normal file
@@ -0,0 +1,213 @@
|
||||
from pymobiledevice3.services.dvt.instruments.location_simulation_base import (
|
||||
LocationSimulationBase,
|
||||
)
|
||||
from pymobiledevice3.services.dvt.instruments.location_simulation import (
|
||||
LocationSimulation,
|
||||
)
|
||||
|
||||
|
||||
class LocationSimulationQueue(LocationSimulation):
|
||||
def __init__(self, dvt, context: LocationSimulationState):
|
||||
super().__init__(dvt)
|
||||
self.context = context
|
||||
|
||||
async def play_queue(
|
||||
self, disable_sleep: bool = False, timing_randomness_range: int = 0
|
||||
) -> None:
|
||||
while True:
|
||||
if self.context.queue_state == "PAUSED":
|
||||
await asyncio.sleep(0.1)
|
||||
continue
|
||||
if self.context.queue_state == "SHUTDOWN":
|
||||
break
|
||||
loc_id = await self.context.queue.get()
|
||||
if loc_id is None:
|
||||
self.context.queue.task_done()
|
||||
break
|
||||
location_item = self.context.queue_data.get(loc_id)
|
||||
if location_item is None:
|
||||
logger.warning(
|
||||
"Simulation queue item missing for loc_id=%s; skipping stale entry",
|
||||
loc_id,
|
||||
)
|
||||
self.context.queue.task_done()
|
||||
continue
|
||||
new_latitude = location_item.get("latitude")
|
||||
new_longitude = location_item.get("longitude")
|
||||
new_delay = location_item.get("delay")
|
||||
new_delay = 0 if new_delay is None else new_delay
|
||||
new_start = location_item.get("start")
|
||||
|
||||
current_location_item = self.context.queue_data.get(self.context.loc_id)
|
||||
current_latitude = (
|
||||
current_location_item.get("latitude")
|
||||
if isinstance(current_location_item, dict)
|
||||
else self.context.latitude
|
||||
)
|
||||
current_longitude = (
|
||||
current_location_item.get("longitude")
|
||||
if isinstance(current_location_item, dict)
|
||||
else self.context.longitude
|
||||
)
|
||||
current_start = (
|
||||
current_location_item.get("start")
|
||||
if isinstance(current_location_item, dict)
|
||||
else None
|
||||
)
|
||||
|
||||
if self.context.set_location_enabled:
|
||||
if new_delay > 0 and not disable_sleep:
|
||||
countdown_delay = int(round(float(new_delay)))
|
||||
if timing_randomness_range > 0:
|
||||
new_delay = new_delay + random.uniform(
|
||||
-timing_randomness_range, timing_randomness_range
|
||||
)
|
||||
countdown_delay = int(round(float(new_delay)))
|
||||
for i in range(max(0, countdown_delay), 0, -1):
|
||||
self.context.next_move = i
|
||||
await self.context.sio.emit(
|
||||
"simulation_status",
|
||||
{
|
||||
"status": self.context.simulation_active,
|
||||
"loc_id": self.context.loc_id,
|
||||
"latitude": current_latitude,
|
||||
"longitude": current_longitude,
|
||||
"start": current_start,
|
||||
"next_move": i,
|
||||
},
|
||||
namespace="/",
|
||||
)
|
||||
await asyncio.sleep(1)
|
||||
self.context.queue_data[loc_id]["start"] = self.context.queue_data[self.context.loc_id]["end"] = datetime.now(timezone.utc).isoformat()
|
||||
await self.set(new_latitude, new_longitude)
|
||||
self.context.loc_id = loc_id
|
||||
self.context.latitude = new_latitude
|
||||
self.context.longitude = new_longitude
|
||||
await self.context.sio.emit(
|
||||
"simulation_status",
|
||||
{
|
||||
"status": self.context.simulation_active,
|
||||
"loc_id": self.context.loc_id,
|
||||
"latitude": self.context.latitude,
|
||||
"longitude": self.context.longitude,
|
||||
"start": new_start,
|
||||
"next_move": None,
|
||||
},
|
||||
namespace="/",
|
||||
)
|
||||
logger.info(
|
||||
"Set simulated location to %s, %s after %ss delay",
|
||||
new_latitude,
|
||||
new_longitude,
|
||||
new_delay,
|
||||
)
|
||||
self.context.queue.task_done()
|
||||
|
||||
|
||||
class LocationSimulationTestQueue(LocationSimulationBase):
|
||||
def __init__(self, context: LocationSimulationState):
|
||||
super().__init__()
|
||||
self.context = context
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self):
|
||||
return self
|
||||
|
||||
async def set(self, latitude: float, longitude: float) -> None:
|
||||
await asyncio.sleep(0.1)
|
||||
logger.info("Simulated location set to %s, %s", latitude, longitude)
|
||||
|
||||
async def clear(self) -> None:
|
||||
q = self.context.queue
|
||||
self.context.set_location_enabled = False
|
||||
self.context.queue_state = "SHUTDOWN"
|
||||
while not q.empty():
|
||||
try:
|
||||
item = q.get_nowait()
|
||||
q.task_done()
|
||||
logger.info("Discarding item from queue: %s", item)
|
||||
except asyncio.QueueEmpty:
|
||||
break
|
||||
await q.put(None)
|
||||
|
||||
if self.context.simulation_task is not None and not self.context.simulation_task.done():
|
||||
try:
|
||||
await asyncio.wait_for(self.context.simulation_task, timeout=5)
|
||||
except TimeoutError:
|
||||
self.context.simulation_task.cancel()
|
||||
with suppress(asyncio.CancelledError):
|
||||
await self.context.simulation_task
|
||||
self.context.simulation_active = False
|
||||
self.context.queue_state = "SHUTDOWN"
|
||||
|
||||
async def play_queue(
|
||||
self, disable_sleep: bool = False, timing_randomness_range: int = 0
|
||||
) -> None:
|
||||
while True:
|
||||
if self.context.queue_state == "PAUSED":
|
||||
await asyncio.sleep(0.1)
|
||||
continue
|
||||
if self.context.queue_state == "SHUTDOWN":
|
||||
break
|
||||
loc_id = await self.context.queue.get()
|
||||
if loc_id is None:
|
||||
self.context.queue.task_done()
|
||||
break
|
||||
location_item = self.context.queue_data.get(loc_id)
|
||||
if location_item is None:
|
||||
logger.warning(
|
||||
"Test simulation queue item missing for loc_id=%s; skipping stale entry",
|
||||
loc_id,
|
||||
)
|
||||
self.context.queue.task_done()
|
||||
continue
|
||||
latitude = location_item.get("latitude")
|
||||
longitude = location_item.get("longitude")
|
||||
delay = location_item.get("delay")
|
||||
delay = 0 if delay is None else delay
|
||||
start_time = location_item.get("start")
|
||||
if self.context.set_location_enabled:
|
||||
if delay > 0 and not disable_sleep:
|
||||
countdown_delay = int(round(float(delay)))
|
||||
if timing_randomness_range > 0:
|
||||
delay = delay + random.uniform(
|
||||
-timing_randomness_range, timing_randomness_range
|
||||
)
|
||||
countdown_delay = int(round(float(delay)))
|
||||
for i in range(max(0, countdown_delay), 0, -1):
|
||||
self.context.next_move = i
|
||||
await self.context.sio.emit(
|
||||
"simulation_status",
|
||||
{
|
||||
"status": self.context.simulation_active,
|
||||
"loc_id": self.context.loc_id,
|
||||
"latitude": self.context.latitude,
|
||||
"longitude": self.context.longitude,
|
||||
"next_move": i,
|
||||
},
|
||||
namespace="/",
|
||||
)
|
||||
await asyncio.sleep(1)
|
||||
await self.set(latitude, longitude)
|
||||
self.context.latitude = latitude
|
||||
self.context.longitude = longitude
|
||||
await self.context.sio.emit(
|
||||
"simulation_status",
|
||||
{
|
||||
"status": self.context.simulation_active,
|
||||
"loc_id": self.context.loc_id,
|
||||
"latitude": self.context.latitude,
|
||||
"longitude": self.context.longitude,
|
||||
"next_move": None,
|
||||
},
|
||||
namespace="/",
|
||||
)
|
||||
logger.info(
|
||||
"Set simulated location to %s, %s after %ss delay",
|
||||
latitude,
|
||||
longitude,
|
||||
delay,
|
||||
)
|
||||
self.context.queue.task_done()
|
||||
64
src/pymd3_vue_location_sim/models.py
Normal file
64
src/pymd3_vue_location_sim/models.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from typing import Optional, Dict
|
||||
from pydantic import BaseModel
|
||||
|
||||
class SimulationStatusData(BaseModel):
|
||||
latitude: float
|
||||
longitude: float
|
||||
start: float
|
||||
end: Optional[float]
|
||||
next_move: Optional[float]
|
||||
|
||||
|
||||
class SimulationStatus(BaseModel):
|
||||
status: bool
|
||||
data: Optional[SimulationStatusData]
|
||||
|
||||
|
||||
class SimulationRequestData(BaseModel):
|
||||
latitude: float
|
||||
longitude: float
|
||||
delay: int = 0
|
||||
start: Optional[str] = None
|
||||
end: Optional[str] = None
|
||||
|
||||
|
||||
class SimulationRequest(BaseModel):
|
||||
status: bool
|
||||
data: Optional[SimulationRequestData]
|
||||
|
||||
|
||||
class SimulationRequestResponseData(BaseModel):
|
||||
loc_id: str
|
||||
latitude: float
|
||||
longitude: float
|
||||
delay: int = 0
|
||||
start: Optional[str] = None
|
||||
end: Optional[str] = None
|
||||
|
||||
class SimulationQueueList(BaseModel):
|
||||
data: Optional[SimulationRequestResponseData]
|
||||
|
||||
|
||||
class SimulationRequestResponse(BaseModel):
|
||||
status: bool
|
||||
data: Optional[SimulationRequestResponseData]
|
||||
|
||||
class SimulationQueueDict(BaseModel):
|
||||
location_id: Dict[str, SimulationRequestResponseData]
|
||||
|
||||
class iCloudLocationData(BaseModel):
|
||||
latitude: float
|
||||
longitude: float
|
||||
timestamp: str
|
||||
|
||||
class iCloudReturnData(BaseModel):
|
||||
latitude: float
|
||||
longitude: float
|
||||
timeStamp: int
|
||||
altitude: float
|
||||
horizontalAccuracy: float
|
||||
verticalAccuracy: float
|
||||
batteryLevel: float
|
||||
deviceDisplayName: str
|
||||
deviceStatus: int
|
||||
name: str
|
||||
379
src/pymd3_vue_location_sim/routes/api.py
Normal file
379
src/pymd3_vue_location_sim/routes/api.py
Normal file
@@ -0,0 +1,379 @@
|
||||
""" Tunnel Functions"""
|
||||
|
||||
@self._app.get("/start-tunnel")
|
||||
async def start_tunnel(
|
||||
udid: Optional[str] = self.context.udid,
|
||||
ip: Optional[str] = None,
|
||||
connection_type: Optional[str] = None,
|
||||
) -> fastapi.Response:
|
||||
udid_tunnels = [
|
||||
t.tunnel
|
||||
for t in self._tunneld_core.tunnel_tasks.values()
|
||||
if t.udid == udid and t.tunnel is not None
|
||||
]
|
||||
if len(udid_tunnels) > 0:
|
||||
self.context.udid = udid
|
||||
data = {
|
||||
"interface": udid_tunnels[0].interface,
|
||||
"port": udid_tunnels[0].port,
|
||||
"address": udid_tunnels[0].address,
|
||||
}
|
||||
return generate_http_response(data)
|
||||
queue = asyncio.Queue()
|
||||
created_task = False
|
||||
try:
|
||||
if not created_task and connection_type in ("usbmux", None):
|
||||
task_identifier = f"usbmux-{udid}"
|
||||
try:
|
||||
async with await create_using_usbmux(udid) as lockdown:
|
||||
service = await CoreDeviceTunnelProxy.create(lockdown)
|
||||
task = asyncio.create_task(
|
||||
self._tunneld_core.start_tunnel_task(
|
||||
task_identifier,
|
||||
service,
|
||||
protocol=TunnelProtocol.TCP,
|
||||
queue=queue,
|
||||
),
|
||||
name=f"start-tunnel-task-{task_identifier}",
|
||||
)
|
||||
self._tunneld_core.tunnel_tasks[task_identifier] = TunnelTask(
|
||||
task=task, udid=udid
|
||||
)
|
||||
created_task = True
|
||||
except ConnectionFailedError, InvalidServiceError, MuxException:
|
||||
pass
|
||||
if connection_type in ("usb", None):
|
||||
for rsd in await get_rsds(udid=udid):
|
||||
rsd_ip = rsd.service.address[0]
|
||||
if ip is not None and rsd_ip != ip:
|
||||
await rsd.close()
|
||||
continue
|
||||
task = asyncio.create_task(
|
||||
self._tunneld_core.start_tunnel_task(
|
||||
rsd_ip,
|
||||
await create_core_device_tunnel_service_using_rsd(rsd),
|
||||
queue=queue,
|
||||
),
|
||||
name=f"start-tunnel-usb-{rsd_ip}",
|
||||
)
|
||||
self._tunneld_core.tunnel_tasks[rsd_ip] = TunnelTask(
|
||||
task=task, udid=rsd.udid
|
||||
)
|
||||
created_task = True
|
||||
if not created_task and connection_type in ("wifi", None):
|
||||
for remotepairing in await get_remote_pairing_tunnel_services(
|
||||
udid=udid
|
||||
):
|
||||
remotepairing_ip = remotepairing.hostname
|
||||
if ip is not None and remotepairing_ip != ip:
|
||||
await remotepairing.close()
|
||||
continue
|
||||
task = asyncio.create_task(
|
||||
self._tunneld_core.start_tunnel_task(
|
||||
remotepairing_ip, remotepairing, queue=queue
|
||||
),
|
||||
name=f"start-tunnel-wifi-{remotepairing_ip}",
|
||||
)
|
||||
self._tunneld_core.tunnel_tasks[remotepairing_ip] = TunnelTask(
|
||||
task=task, udid=remotepairing.remote_identifier
|
||||
)
|
||||
created_task = True
|
||||
except Exception as e:
|
||||
return fastapi.Response(
|
||||
status_code=501,
|
||||
content=json.dumps(
|
||||
{
|
||||
"error": {
|
||||
"exception": e.__class__.__name__,
|
||||
"traceback": traceback.format_exc(),
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
if not created_task:
|
||||
return fastapi.Response(
|
||||
status_code=501, content=json.dumps({"error": "task not created"})
|
||||
)
|
||||
tunnel: Optional[TunnelResult] = await queue.get()
|
||||
if tunnel is not None:
|
||||
self.context.udid = udid
|
||||
data = {
|
||||
"interface": tunnel.interface,
|
||||
"port": tunnel.port,
|
||||
"address": tunnel.address,
|
||||
}
|
||||
return generate_http_response(data)
|
||||
else:
|
||||
return fastapi.Response(
|
||||
status_code=404,
|
||||
content=json.dumps(
|
||||
{"error": "something went wrong during tunnel creation"}
|
||||
),
|
||||
)
|
||||
|
||||
@self._app.get("/restart-tunneld")
|
||||
async def restart() -> fastapi.Response:
|
||||
"""Restart Tunneld"""
|
||||
self._tunneld_core.clear()
|
||||
await asyncio.sleep(2)
|
||||
self._tunneld_core.start()
|
||||
data = {
|
||||
"operation": "restart-tunneld",
|
||||
"data": True,
|
||||
"message": "Restarting tunneld...",
|
||||
}
|
||||
return generate_http_response(data)
|
||||
|
||||
|
||||
@self._app.get("/shutdown")
|
||||
async def shutdown() -> fastapi.Response:
|
||||
"""Shutdown Tunneld"""
|
||||
os.kill(os.getpid(), signal.SIGINT)
|
||||
data = {
|
||||
"operation": "shutdown",
|
||||
"data": True,
|
||||
"message": "Server shutting down...",
|
||||
}
|
||||
return generate_http_response(data)
|
||||
|
||||
@self._app.get("/clear-tunnels")
|
||||
async def clear_tunnels() -> fastapi.Response:
|
||||
"""Clear all tunnels"""
|
||||
self._tunneld_core.clear()
|
||||
data = {
|
||||
"operation": "clear_tunnels",
|
||||
"data": True,
|
||||
"message": "Cleared tunnels...",
|
||||
}
|
||||
return generate_http_response(data)
|
||||
|
||||
@self._app.get("/cancel")
|
||||
async def cancel_tunnel(udid: str) -> fastapi.Response:
|
||||
"""Cancel a tunnel"""
|
||||
self._tunneld_core.cancel(udid=udid)
|
||||
data = {
|
||||
"operation": "cancel",
|
||||
"udid": udid,
|
||||
"data": True,
|
||||
"message": f"tunnel {udid} Canceled ...",
|
||||
}
|
||||
return generate_http_response(data)
|
||||
|
||||
"""Simulation Functions"""
|
||||
|
||||
@self._app.get("/start-simulation")
|
||||
async def app_start_simulation() -> fastapi.Response:
|
||||
logger.info("Simulation Start Requested ")
|
||||
if (
|
||||
self.context.simulation_task is None
|
||||
or self.context.simulation_task.done()
|
||||
):
|
||||
await start_icloud_monitor()
|
||||
self.context.simulation_active = True
|
||||
self.context.simulation_task = asyncio.create_task(
|
||||
start_simulation_queue(),
|
||||
name="location-simulation-worker",
|
||||
)
|
||||
data = {"status": "started", "message": "Simulation started"}
|
||||
else:
|
||||
data = {"status": "error", "message": "Simulation already running"}
|
||||
return generate_http_response(data)
|
||||
|
||||
@self._app.get("/start-icloud-monitor")
|
||||
async def app_start_icloud_monitor() -> fastapi.Response:
|
||||
await start_icloud_monitor()
|
||||
data = {
|
||||
"status": "started",
|
||||
"icloud_monitor_enabled": self.context.icloud_monitor_enabled,
|
||||
"icloud_monitor_running": is_icloud_monitor_running(),
|
||||
}
|
||||
return generate_http_response(data)
|
||||
|
||||
@self._app.get("/stop-icloud-monitor")
|
||||
async def app_stop_icloud_monitor() -> fastapi.Response:
|
||||
await end_icloud_monitor()
|
||||
data = {
|
||||
"status": "stopped",
|
||||
"icloud_monitor_enabled": self.context.icloud_monitor_enabled,
|
||||
"icloud_monitor_running": is_icloud_monitor_running(),
|
||||
}
|
||||
return generate_http_response(data)
|
||||
|
||||
@self._app.get("/icloud-monitor-status")
|
||||
async def app_icloud_monitor_status() -> fastapi.Response:
|
||||
data = {
|
||||
"status": "ok",
|
||||
"icloud_monitor_enabled": self.context.icloud_monitor_enabled,
|
||||
"icloud_monitor_running": is_icloud_monitor_running(),
|
||||
}
|
||||
return generate_http_response(data)
|
||||
|
||||
@self._app.post("/add-location")
|
||||
async def app_add_location(data: SimulationRequestData) -> fastapi.Response:
|
||||
"""Add a location to the simulation queue"""
|
||||
logger.info("Request to add new location to queue")
|
||||
|
||||
loc_id = str(uuid.uuid4())
|
||||
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)
|
||||
)
|
||||
delay = (
|
||||
data.get("delay", 0)
|
||||
if isinstance(data, dict)
|
||||
else getattr(data, "delay", 0)
|
||||
)
|
||||
try:
|
||||
delay = parse_delay_seconds(delay)
|
||||
except ValueError as e:
|
||||
return generate_http_response(
|
||||
{"status": "error", "message": str(e)},
|
||||
status_code=400,
|
||||
)
|
||||
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,
|
||||
delay,
|
||||
)
|
||||
accrued_delay = 0
|
||||
if self.context.queue_data:
|
||||
accrued_delay = sum(
|
||||
parse_delay_seconds(item.get("delay", 0))
|
||||
for item in self.context.queue_data.values()
|
||||
)
|
||||
now_time = datetime.now(timezone.utc)
|
||||
new_time = (
|
||||
now_time
|
||||
+ timedelta(seconds=accrued_delay)
|
||||
+ timedelta(seconds=delay)
|
||||
)
|
||||
start_time = new_time.isoformat()
|
||||
location_item = {
|
||||
"loc_id": loc_id,
|
||||
"latitude": latitude,
|
||||
"longitude": longitude,
|
||||
"delay": delay,
|
||||
"start": start_time,
|
||||
"status": "queued",
|
||||
}
|
||||
resp = {
|
||||
"status": "added",
|
||||
"message": f"Location {loc_id} added to the queue",
|
||||
"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:
|
||||
resp = {"status": "error", "message": "Invalid location data"}
|
||||
return generate_http_response(resp)
|
||||
|
||||
@self._app.get("/clear-queue")
|
||||
async def app_clear_queue() -> fastapi.Response:
|
||||
"""Clear the simulation queue"""
|
||||
logger.info("Simulation Start Requested ")
|
||||
await empty_simulation_queue()
|
||||
data = {"status": "cleared", "message": "Simulation cleared"}
|
||||
return generate_http_response(data)
|
||||
|
||||
@self._app.get("/pause-queue")
|
||||
async def app_pause_queue() -> fastapi.Response:
|
||||
"""Pause the simulation queue"""
|
||||
await pause_simulation_queue()
|
||||
data = {"status": "paused", "message": "Simulation paused"}
|
||||
return generate_http_response(data)
|
||||
|
||||
@self._app.get("/resume-queue")
|
||||
async def app_resume_queue() -> fastapi.Response:
|
||||
"""Resume the simulation queue"""
|
||||
await resume_simulation_queue()
|
||||
data = {"status": "resumed", "message": "Simulation resumed"}
|
||||
return generate_http_response(data)
|
||||
|
||||
@self._app.get("/end-simulation")
|
||||
async def app_end_simulation() -> fastapi.Response:
|
||||
"""End the simulation queue"""
|
||||
logger.info("End location simulation request")
|
||||
end_task = asyncio.create_task(
|
||||
end_simulation_queue(), name="end-simulation-worker"
|
||||
)
|
||||
result = await end_task
|
||||
data = {"status": result, "message": "Simulation ended"}
|
||||
return generate_http_response(data)
|
||||
|
||||
"""Status Functions"""
|
||||
|
||||
@self._app.get("/")
|
||||
async def list_tunnels() -> dict[str, list[dict]]:
|
||||
"""Retrieve the available tunnels and format them as {UUID: TUNNEL_ADDRESS}"""
|
||||
tunnels = {}
|
||||
for ip, active_tunnel in self._tunneld_core.tunnel_tasks.items():
|
||||
if (active_tunnel.udid is None) or (active_tunnel.tunnel is None):
|
||||
continue
|
||||
if active_tunnel.udid not in tunnels:
|
||||
tunnels[active_tunnel.udid] = []
|
||||
tunnels[active_tunnel.udid].append(
|
||||
{
|
||||
"tunnel-address": active_tunnel.tunnel.address,
|
||||
"tunnel-port": active_tunnel.tunnel.port,
|
||||
"interface": ip,
|
||||
}
|
||||
)
|
||||
return tunnels
|
||||
|
||||
|
||||
@self._app.get("/device-info")
|
||||
async def device_info():
|
||||
"""Get device information"""
|
||||
tunnels = {}
|
||||
for ip, active_tunnel in self._tunneld_core.tunnel_tasks.items():
|
||||
if (active_tunnel.udid is None) or (active_tunnel.tunnel is None):
|
||||
continue
|
||||
if active_tunnel.udid not in tunnels:
|
||||
tunnels[active_tunnel.udid] = {}
|
||||
try:
|
||||
lockdown = await create_using_usbmux(
|
||||
serial=active_tunnel.udid, autopair=False
|
||||
)
|
||||
tunnels[active_tunnel.udid] = iterate_multidim(lockdown.all_values)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to create lockdown session for device {active_tunnel.udid}: {e}"
|
||||
)
|
||||
continue
|
||||
return tunnels
|
||||
|
||||
@self._app.get("/device-name")
|
||||
async def rsd_info():
|
||||
"""Get rsd information"""
|
||||
device_name = await get_device_name()
|
||||
return generate_http_response(device_name)
|
||||
|
||||
@self._app.get("/rsd-info")
|
||||
async def rsd_info():
|
||||
"""Get rsd information"""
|
||||
rsd_info = {}
|
||||
if self.context.tunnel is None:
|
||||
await get_tun()
|
||||
if self.context.tunnel is not None:
|
||||
rsd_info = self.context.tunnel.peer_info
|
||||
return generate_http_response(rsd_info)
|
||||
|
||||
@self._app.get("/hello")
|
||||
async def hello() -> fastapi.Response:
|
||||
data = {"message": "Hello, I'm alive"}
|
||||
return generate_http_response(data)
|
||||
|
||||
@self._app.get("/context-status")
|
||||
async def app_context_status() -> fastapi.Response:
|
||||
data = get_status()
|
||||
return generate_http_response(data)
|
||||
425
src/pymd3_vue_location_sim/routes/socketio.py
Normal file
425
src/pymd3_vue_location_sim/routes/socketio.py
Normal file
@@ -0,0 +1,425 @@
|
||||
""" Socket.IO Functions"""
|
||||
|
||||
|
||||
async def sio_send_status(sid):
|
||||
"""Send Current Status"""
|
||||
await self.context.sio.emit(
|
||||
"status", jsonable_encoder(get_status()), namespace="/", to=sid
|
||||
)
|
||||
|
||||
|
||||
"""Socket.IO Connection Events"""
|
||||
|
||||
|
||||
@self.context.sio.event
|
||||
async def connect(sid, environ):
|
||||
"""Client connection event handler."""
|
||||
self.context.connected_clients.add(sid)
|
||||
logger.info("Client connected: %s", sid)
|
||||
await sio_send_status(sid)
|
||||
return "%s connected" % sid
|
||||
|
||||
|
||||
@self.context.sio.event
|
||||
async def disconnect(sid):
|
||||
"""Client disconnection event handler."""
|
||||
self.context.connected_clients.discard(sid)
|
||||
logger.info("Client disconnected: %s", sid)
|
||||
|
||||
|
||||
""" Socket.IO Request Events """
|
||||
|
||||
|
||||
@self.context.sio.event
|
||||
async def request_update(sid):
|
||||
status_update = jsonable_encoder(get_status())
|
||||
logger.info("Update request from %s sending %s", sid, status_update)
|
||||
return status_update
|
||||
|
||||
|
||||
@self.context.sio.event
|
||||
async def message(sid, data):
|
||||
logger.info("Received message from %s: %s", sid, data)
|
||||
return True, "Message received"
|
||||
|
||||
|
||||
# await self.context.sio.emit("message", f"Received message from {sid}: {data}", namespace="/")
|
||||
|
||||
""" Device Control"""
|
||||
|
||||
|
||||
@self.context.sio.event
|
||||
async def device_control(sid, data):
|
||||
"""Device Control"""
|
||||
command = (
|
||||
data.get("command")
|
||||
if isinstance(data, dict)
|
||||
else getattr(data, "command", None)
|
||||
)
|
||||
delay = (
|
||||
data.get("delay")
|
||||
if isinstance(data, dict)
|
||||
else getattr(data, "delay", None)
|
||||
)
|
||||
if delay is None:
|
||||
delay = 5
|
||||
match command:
|
||||
case "shutdown":
|
||||
""" Shutdown the device"""
|
||||
logger.info(
|
||||
"Shutdown command received from %s with delay %s", sid, delay
|
||||
)
|
||||
await device_shutdown(delay)
|
||||
return {
|
||||
"command": "shutdown",
|
||||
"status": "success",
|
||||
"message": f"Device shutdown initiated with {delay} seconds delay",
|
||||
}
|
||||
|
||||
case "reboot":
|
||||
""" Reboot the device"""
|
||||
logger.info(
|
||||
"Reboot command received from %s with delay %s", sid, delay
|
||||
)
|
||||
await device_reboot(delay)
|
||||
return {
|
||||
"command": "reboot",
|
||||
"status": "success",
|
||||
"message": f"Device reboot initiated with {delay} seconds delay",
|
||||
}
|
||||
|
||||
case _:
|
||||
return {
|
||||
"command": command,
|
||||
"status": "error",
|
||||
"message": f"Invalid command: {command}",
|
||||
}
|
||||
|
||||
|
||||
@self.context.sio.event
|
||||
async def simulation_control(sid, data):
|
||||
"""Simulation Control"""
|
||||
command = (
|
||||
data.get("command")
|
||||
if isinstance(data, dict)
|
||||
else getattr(data, "command", None)
|
||||
)
|
||||
logger.info(
|
||||
"Simulation Control command: %s requested from %s", command, sid
|
||||
)
|
||||
try:
|
||||
match command:
|
||||
case "add":
|
||||
""" Add a location to the simulation queue"""
|
||||
loc_id = str(uuid.uuid4())
|
||||
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)
|
||||
)
|
||||
delay = (
|
||||
data.get("delay", 0)
|
||||
if isinstance(data, dict)
|
||||
else getattr(data, "delay", 0)
|
||||
)
|
||||
try:
|
||||
delay = parse_delay_seconds(delay)
|
||||
except ValueError as e:
|
||||
return {
|
||||
"command": command,
|
||||
"status": "error",
|
||||
"message": str(e),
|
||||
}
|
||||
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,
|
||||
delay,
|
||||
)
|
||||
accrued_delay = 0
|
||||
if self.context.queue_data:
|
||||
accrued_delay = sum(
|
||||
parse_delay_seconds(item.get("delay", 0))
|
||||
for item in self.context.queue_data.values()
|
||||
)
|
||||
now_time = datetime.now(timezone.utc)
|
||||
new_time = (
|
||||
now_time
|
||||
+ timedelta(seconds=accrued_delay)
|
||||
+ timedelta(seconds=delay)
|
||||
)
|
||||
start_time = new_time.isoformat()
|
||||
coords = f"{latitude}, {longitude}"
|
||||
rev_geocode = self.context.reverse_geocode(coords)
|
||||
if rev_geocode:
|
||||
address = rev_geocode.address
|
||||
else:
|
||||
address = f"{latitude}, {longitude}"
|
||||
|
||||
location_item = {
|
||||
"loc_id": loc_id,
|
||||
"latitude": latitude,
|
||||
"longitude": longitude,
|
||||
"delay": delay,
|
||||
"start": start_time,
|
||||
"address": address,
|
||||
}
|
||||
ack = {
|
||||
"command": command,
|
||||
"status": "added",
|
||||
"message": f"Location {loc_id} added to the queue",
|
||||
"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)
|
||||
return ack
|
||||
else:
|
||||
logger.warning(
|
||||
"Invalid location data received from %s: %s", sid, data
|
||||
)
|
||||
return {
|
||||
"command": command,
|
||||
"status": "error",
|
||||
"message": "Invalid location data",
|
||||
"data": location_item,
|
||||
}
|
||||
case "clear":
|
||||
""" Clear the simulation queue"""
|
||||
await empty_simulation_queue()
|
||||
return {
|
||||
"command": command,
|
||||
"status": "cleared",
|
||||
"message": "Simulation cleared",
|
||||
}
|
||||
case "pause":
|
||||
""" Pause the simulation queue"""
|
||||
await pause_simulation_queue()
|
||||
return {
|
||||
"command": command,
|
||||
"status": "paused",
|
||||
"message": "Simulation paused",
|
||||
}
|
||||
case "resume":
|
||||
""" Resume the simulation queue"""
|
||||
await resume_simulation_queue()
|
||||
return {
|
||||
"command": command,
|
||||
"status": "resumed",
|
||||
"message": "Simulation resumed",
|
||||
}
|
||||
case "end":
|
||||
""" End the simulation queue"""
|
||||
logger.info("End location simulation request from %s", sid)
|
||||
end_task = asyncio.create_task(
|
||||
end_simulation_queue(), name="end-simulation-worker"
|
||||
)
|
||||
result = await end_task
|
||||
simstatus = not result
|
||||
return {
|
||||
"command": command,
|
||||
"status": simstatus,
|
||||
"message": "Simulation ended",
|
||||
}
|
||||
case "start":
|
||||
""" Start the simulation queue"""
|
||||
logger.info("Start location simulation request from %s", sid)
|
||||
if (
|
||||
self.context.simulation_task is None
|
||||
or self.context.simulation_task.done()
|
||||
):
|
||||
await start_icloud_monitor()
|
||||
self.context.simulation_active = True
|
||||
self.context.queue_state = "RUNNING"
|
||||
self.context.simulation_task = asyncio.create_task(
|
||||
start_simulation_queue(),
|
||||
name="location-simulation-worker",
|
||||
)
|
||||
return {
|
||||
"command": command,
|
||||
"status": self.context.queue_state,
|
||||
"message": "Simulation started",
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"command": command,
|
||||
"status": "error",
|
||||
"message": "Simulation already running",
|
||||
}
|
||||
case _:
|
||||
logger.warning(
|
||||
"Invalid command received from %s: %s", sid, command
|
||||
)
|
||||
return {"status": "error", "message": "Invalid command"}
|
||||
finally:
|
||||
await sio_send_status(sid)
|
||||
|
||||
|
||||
@self.context.sio.event
|
||||
async def icloud_monitor_control(sid, data):
|
||||
command = (
|
||||
data.get("command")
|
||||
if isinstance(data, dict)
|
||||
else getattr(data, "command", None)
|
||||
)
|
||||
logger.info(
|
||||
"iCloud Monitor control command: %s requested from %s", command, sid
|
||||
)
|
||||
try:
|
||||
match command:
|
||||
case "start":
|
||||
await start_icloud_monitor()
|
||||
return {
|
||||
"command": command,
|
||||
"status": "running",
|
||||
"icloud_monitor_enabled": self.context.icloud_monitor_enabled,
|
||||
"icloud_monitor_running": is_icloud_monitor_running(),
|
||||
}
|
||||
case "stop":
|
||||
await end_icloud_monitor()
|
||||
return {
|
||||
"command": command,
|
||||
"status": "stopped",
|
||||
"icloud_monitor_enabled": self.context.icloud_monitor_enabled,
|
||||
"icloud_monitor_running": is_icloud_monitor_running(),
|
||||
}
|
||||
case "status":
|
||||
return {
|
||||
"command": command,
|
||||
"status": "ok",
|
||||
"icloud_monitor_enabled": self.context.icloud_monitor_enabled,
|
||||
"icloud_monitor_running": is_icloud_monitor_running(),
|
||||
}
|
||||
case _:
|
||||
return {
|
||||
"command": command,
|
||||
"status": "error",
|
||||
"message": "Invalid command",
|
||||
}
|
||||
finally:
|
||||
await sio_send_status(sid)
|
||||
|
||||
|
||||
""" Tunnel Control """
|
||||
|
||||
|
||||
@self.context.sio.event
|
||||
async def tunneld_control(sid, data):
|
||||
command = (
|
||||
data.get("command")
|
||||
if isinstance(data, dict)
|
||||
else getattr(data, "command", None)
|
||||
)
|
||||
logger.info("Tunneld Control command: %s requested from %s", command, sid)
|
||||
match command:
|
||||
case "start":
|
||||
"""Start Tunneld"""
|
||||
logger.info("Start tunneld request from %s: %s", sid, data)
|
||||
try:
|
||||
self._tunneld_core.start()
|
||||
logger.info("Tunneld started successfully")
|
||||
return {
|
||||
"status": "running",
|
||||
"message": "Tunneld started successfully",
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Error starting tunneld: %s", e)
|
||||
return {
|
||||
"command": command,
|
||||
"status": "error",
|
||||
"message": f"Error starting tunneld: {e}",
|
||||
}
|
||||
|
||||
case "start-watcher":
|
||||
""" Start Tunneld Watcher """
|
||||
logger.info("Start tunneld watcher request from %s: %s", sid, data)
|
||||
await start_tunnel_watcher()
|
||||
return {
|
||||
"status": "running",
|
||||
"message": "Tunneld watcher started successfully",
|
||||
}
|
||||
|
||||
case "end-watcher":
|
||||
""" End Tunneld Watcher """
|
||||
logger.info("End tunneld watcher request from %s: %s", sid, data)
|
||||
await end_tunnel_watcher()
|
||||
return {
|
||||
"status": "stopped",
|
||||
"message": "Tunneld watcher stopped successfully",
|
||||
}
|
||||
|
||||
case "shutdown":
|
||||
"""Shutdown Tunneld"""
|
||||
logger.info("Shutdown tunneld request from %s: %s", sid, data)
|
||||
try:
|
||||
os.kill(os.getpid(), signal.SIGINT)
|
||||
return {
|
||||
"command": command,
|
||||
"status": "Success",
|
||||
"message": "Server shutting down...",
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Error shutting down tunneld: %s", e)
|
||||
return {
|
||||
"command": command,
|
||||
"status": "error",
|
||||
"message": f"Error shutting down tunneld: {e}",
|
||||
}
|
||||
|
||||
case "clear":
|
||||
"""Clear all tunnels"""
|
||||
logger.info("Clearing tunnels...")
|
||||
try:
|
||||
self._tunneld_core.clear()
|
||||
return {
|
||||
"command": command,
|
||||
"status": "Success",
|
||||
"message": "Cleared tunnels...",
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Error clearing tunnels: %s", e)
|
||||
return {
|
||||
"command": command,
|
||||
"status": "error",
|
||||
"message": f"Error clearing tunnels: {e}",
|
||||
}
|
||||
|
||||
case "cancel":
|
||||
"""Cancel a tunnel"""
|
||||
logger.info("Canceling tunnel request from %s: %s", sid, data)
|
||||
try:
|
||||
udid = (
|
||||
data.get("udid")
|
||||
if isinstance(data, dict)
|
||||
else getattr(data, "udid", self.context.udid)
|
||||
)
|
||||
if udid is None:
|
||||
udid = self.context.udid
|
||||
self._tunneld_core.cancel(udid=udid)
|
||||
return {
|
||||
"command": command,
|
||||
"status": "Success",
|
||||
"udid": udid,
|
||||
"message": f"tunnel {udid} Canceled ...",
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("Error canceling tunnel: %s", e)
|
||||
return {
|
||||
"command": command,
|
||||
"status": "error",
|
||||
"message": f"Error canceling tunnel: {e}",
|
||||
}
|
||||
|
||||
case _:
|
||||
return {
|
||||
"command": command,
|
||||
"status": "error",
|
||||
"message": f"Unknown operation: {command}",
|
||||
}
|
||||
1835
src/pymd3_vue_location_sim/server.py
Normal file
1835
src/pymd3_vue_location_sim/server.py
Normal file
File diff suppressed because it is too large
Load Diff
125
uv.lock
generated
125
uv.lock
generated
@@ -93,24 +93,36 @@ name = "back-end"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "daemonize" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "geopy" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pyicloud" },
|
||||
{ name = "pymobiledevice3" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "python-socketio" },
|
||||
{ name = "sqlalchemy" },
|
||||
{ name = "sqlalchemy-orm" },
|
||||
{ name = "typer" },
|
||||
{ name = "typing" },
|
||||
{ name = "uvicorn" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "click", specifier = ">=8.3.1" },
|
||||
{ name = "daemonize", specifier = ">=2.5.0" },
|
||||
{ name = "fastapi", specifier = "==0.135.1" },
|
||||
{ name = "geopy", specifier = "==2.4.1" },
|
||||
{ name = "pydantic", specifier = "==2.12.5" },
|
||||
{ name = "pyicloud", specifier = ">=2.4.1" },
|
||||
{ name = "pymobiledevice3", specifier = "==9.0.0" },
|
||||
{ name = "python-dotenv", specifier = ">=1.2.2" },
|
||||
{ name = "python-socketio", specifier = "==5.16.1" },
|
||||
{ name = "sqlalchemy", specifier = ">=2.0.48" },
|
||||
{ name = "sqlalchemy-orm", specifier = ">=1.2.10" },
|
||||
{ name = "typer", specifier = ">=0.24.1" },
|
||||
{ name = "typing", specifier = "==3.10.0.0" },
|
||||
{ name = "uvicorn", specifier = "==0.41.0" },
|
||||
]
|
||||
@@ -428,6 +440,27 @@ 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]]
|
||||
name = "geographiclib"
|
||||
version = "2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/78/4892343230a9d29faa1364564e525307a37e54ad776ea62c12129dbba704/geographiclib-2.1.tar.gz", hash = "sha256:6a6545e6262d0ed3522e13c515713718797e37ed8c672c31ad7b249f372ef108", size = 37004, upload-time = "2025-08-21T21:34:26Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/31/b3/802576f2ea5dcb48501bb162e4c7b7b3ca5654a42b2c968ef98a797a4c79/geographiclib-2.1-py3-none-any.whl", hash = "sha256:e2a873b9b9e7fc38721ad73d5f4e6c9ed140d428a339970f505c07056997d40b", size = 40740, upload-time = "2025-08-21T21:34:24.955Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "geopy"
|
||||
version = "2.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "geographiclib" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0e/fd/ef6d53875ceab72c1fad22dbed5ec1ad04eb378c2251a6a8024bad890c3b/geopy-2.4.1.tar.gz", hash = "sha256:50283d8e7ad07d89be5cb027338c6365a32044df3ae2556ad3f52f4840b3d0d1", size = 117625, upload-time = "2023-11-23T21:49:32.734Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/15/cf2a69ade4b194aa524ac75112d5caac37414b20a3a03e6865dfe0bd1539/geopy-2.4.1-py3-none-any.whl", hash = "sha256:ae8b4bc5c1131820f4d75fce9d4aaaca0c85189b3aa5d64c3dcaf5e3b7b882a7", size = 125437, upload-time = "2023-11-23T21:49:30.421Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gpxpy"
|
||||
version = "1.6.2"
|
||||
@@ -437,6 +470,29 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/44/9f/62df6c1e52462bdd04275b36cec49efa9e8af7e7b834499eb288f73dcfbc/gpxpy-1.6.2-py3-none-any.whl", hash = "sha256:289bc2d80f116c988d0a1e763fda22838f83005573ece2bbc6521817b26fb40a", size = 42649, upload-time = "2023-11-29T17:25:35.76Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "3.3.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
@@ -491,6 +547,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/1f/19ebc343cc71a7ffa78f17018535adc5cbdd87afb31d7c34874680148b32/ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", size = 12314, upload-time = "2022-06-15T21:40:25.756Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inflection"
|
||||
version = "0.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/7e/691d061b7329bc8d54edbf0ec22fbfb2afe61facb681f9aaa9bff7a27d04/inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", size = 15091, upload-time = "2020-08-22T08:16:29.139Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2", size = 9454, upload-time = "2020-08-22T08:16:27.816Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inquirer3"
|
||||
version = "0.6.1"
|
||||
@@ -728,6 +793,15 @@ 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]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opack2"
|
||||
version = "0.0.1"
|
||||
@@ -1354,6 +1428,44 @@ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.48"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/b3/f437eaa1cf028bb3c927172c7272366393e73ccd104dcf5b6963f4ab5318/sqlalchemy-2.0.48-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd", size = 2154401, upload-time = "2026-03-02T15:49:17.24Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/1c/b3abdf0f402aa3f60f0df6ea53d92a162b458fca2321d8f1f00278506402/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f", size = 3274528, upload-time = "2026-03-02T15:50:41.489Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/5e/327428a034407651a048f5e624361adf3f9fbac9d0fa98e981e9c6ff2f5e/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b", size = 3279523, upload-time = "2026-03-02T15:53:32.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/ca/ece73c81a918add0965b76b868b7b5359e068380b90ef1656ee995940c02/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0", size = 3224312, upload-time = "2026-03-02T15:50:42.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/11/fbaf1ae91fa4ee43f4fe79661cead6358644824419c26adb004941bdce7c/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2", size = 3246304, upload-time = "2026-03-02T15:53:34.937Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/a8/5fb0deb13930b4f2f698c5541ae076c18981173e27dd00376dbaea7a9c82/sqlalchemy-2.0.48-cp314-cp314-win32.whl", hash = "sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6", size = 2116565, upload-time = "2026-03-02T15:54:38.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/7e/e83615cb63f80047f18e61e31e8e32257d39458426c23006deeaf48f463b/sqlalchemy-2.0.48-cp314-cp314-win_amd64.whl", hash = "sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0", size = 2142205, upload-time = "2026-03-02T15:54:39.831Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/e3/69d8711b3f2c5135e9cde5f063bc1605860f0b2c53086d40c04017eb1f77/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241", size = 3563519, upload-time = "2026-03-02T15:57:52.387Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/4f/a7cce98facca73c149ea4578981594aaa5fd841e956834931de503359336/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0", size = 3528611, upload-time = "2026-03-02T16:04:42.097Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/7d/5936c7a03a0b0cb0fa0cc425998821c6029756b0855a8f7ee70fba1de955/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3", size = 3472326, upload-time = "2026-03-02T15:57:54.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/33/cea7dfc31b52904efe3dcdc169eb4514078887dff1f5ae28a7f4c5d54b3c/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b", size = 3478453, upload-time = "2026-03-02T16:04:44.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/95/32107c4d13be077a9cae61e9ae49966a35dc4bf442a8852dd871db31f62e/sqlalchemy-2.0.48-cp314-cp314t-win32.whl", hash = "sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f", size = 2147209, upload-time = "2026-03-02T15:52:54.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/d7/1e073da7a4bc645eb83c76067284a0374e643bc4be57f14cc6414656f92c/sqlalchemy-2.0.48-cp314-cp314t-win_amd64.whl", hash = "sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933", size = 2182198, upload-time = "2026-03-02T15:52:55.606Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy-orm"
|
||||
version = "1.2.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cached-property" },
|
||||
{ name = "inflection" },
|
||||
{ name = "sqlalchemy" },
|
||||
{ name = "typing-inspect" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c8/98/b0451ae949f8b16287965d4fc4a180fae50a78d9b186300e135ec22e1883/sqlalchemy-orm-1.2.10.tar.gz", hash = "sha256:7ab46d2a54a429d4fd384df9a37ad639dc87ff93be5205ed649c5ca4dad164bb", size = 21846, upload-time = "2023-05-20T09:18:19.064Z" }
|
||||
|
||||
[[package]]
|
||||
name = "srp"
|
||||
version = "1.0.22"
|
||||
@@ -1491,6 +1603,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspect"
|
||||
version = "0.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mypy-extensions" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.2"
|
||||
|
||||
Reference in New Issue
Block a user