reverse geocode, icloud monitor

This commit is contained in:
2026-04-01 10:32:35 -04:00
parent 1eef99e3b4
commit a7af0faefc
10 changed files with 515 additions and 382 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
__pycache__/* __pycache__/*
*.pyc *.pyc
cookies/*

10
.idea/back-end.iml generated
View File

@@ -1,13 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<module external.system.id="java-source" type="PYTHON_MODULE" version="4"> <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"> <component name="PyDocumentationSettings">
<option name="format" value="PLAIN" /> <option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" /> <option name="myDocStringFormat" value="Plain" />

2
.idea/modules.xml generated
View File

@@ -2,7 +2,7 @@
<project version="4"> <project version="4">
<component name="ProjectModuleManager"> <component name="ProjectModuleManager">
<modules> <modules>
<module fileurl="file://$PROJECT_DIR$/.idea/back-end.iml" filepath="$PROJECT_DIR$/.idea/back-end.iml" /> <module fileurl="file://$PROJECT_DIR$/.idea/pymd3_vue_location_sim.iml" filepath="$PROJECT_DIR$/.idea/pymd3_vue_location_sim.iml" />
</modules> </modules>
</component> </component>
</project> </project>

15
.idea/pymd3_vue_location_sim.iml generated Normal file
View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<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" />
</component>
</module>

View File

@@ -1,24 +1,25 @@
#LWP-Cookies-2.0 #LWP-Cookies-2.0
Set-Cookie3: dslang=US-EN; path="/"; domain=.apple.com; path_spec; secure; discard; HttpOnly=None; version=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: 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: acn01="CgJEhy4Wbol25G19NM8DS23DOwutDyN0r/4Z1gAMbj5n95Bf"; path="/"; domain=.apple.com; path_spec; secure; expires="2027-04-01 07:24:24Z"; HttpOnly=None; version=0
Set-Cookie3: aasp=90537484E4B81989243BAABCFA1DE44F53876FB5F9C433FD2C4687BD0D409642748545163AD333F3CDBBDDC6128BF84382082E6C0A4E27921C8792F0883DABFBFE8534D699F84266A14F842D6502C6902F2F00DC126EBD3E7D5FF7C490D9FE6740C97EFA884B52031C2E5BB2469A785CBDD236AD7014E383; path="/"; domain=.idmsa.apple.com; path_spec; secure; discard; HttpOnly=None; version=0 Set-Cookie3: aasp=0893DF20A53394DF6264EB0D0F8DB8E09184E1B9350AA9983DC5EB457D7466E0C4F6F402DA6AE23A4F2375865E4D40CEABC7C9BCCF30B7801A1566D157830BE7E3FE1BBC39D1C7027A3909782C03A6C4E39719F3D8C77A528AB04ED28F26A2FCEC7DE40693496A4786EB60DB8BB8F76652B08C8895936DF5; 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: 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-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-LOGIN="\"v=1:t=BA==BST_IAAAAAAABLwIAAAAAGnMyCgRDmdzLmljbG91ZC5hdXRovQCKfWIEBypQJL5pYW9WRSJSpnRRYSvdPtLCjiJH_dhIBChjwxje7ma7zbulaTStxnHLbp5oU1erVllk8eusG4zv5fUmpfacRsbFI8Y9UV3jVfOvIQhtJKksZvSyxW-j6Xn3QAtNPBc1WUK1l5TInpMCgNwl3A~~\""; 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-VALIDATE="\"v=1:t=BA==BST_IAAAAAAABLwIAAAAAGnMyCgRDmdzLmljbG91ZC5hdXRovQCKfWIEBypQJL5pYW9WRSJSpnRRYSvdPtLCjiJH_dhIBChjwxje7ma7zbulaTStxnHLbp5oU1erVllk8eusG4zv5fUmpfacRsbFI8Y9UV3jVRl05kUtq1dzNVX1bvl3ujJp0xX4p_ZDiqr-9Z1yeC_yxhShKg~~\""; 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-WEBAUTH-USER="\"v=1:s=1:d=157320350\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-01 07:24:27Z"; 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_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-DS-WEB-SESSION-TOKEN="\"AQEaqCRaCv071GuiR0UvtF5IqIvyXlvWeh7aOWEBzPx7Vy4s8hDUySzxI1nO2fkjPPVD5S6w0iH0VDbsHVVw4i89aXs3VaSH4Jl3FjbezwlL+G0lzZfTegOwx3VQyDG4vZoxzA4eo57U+gyfPSk1vj2K7vQr0ypz7gnUnqLL0MnkR//anTy0aidDy5jLoErjYZyHUeh2DU9ked3vaosk6UkZau5bos+QHWiUj4DDp345gTZAQemrQxgYfW3CNF413kMgy45TpIQ9ET6YdP7/6kMbnjPWJXIa/90wsCxfExUx66MWHO2BT0sYTEElc/3yZURTelHk2u0ANxjt+kN29XboudW5pO0/ikIlG39w82gr+fTN2IaBG791uLZg3GIIzqIv2Utt4xFD0W/vq2yhQ4en5dcmb+2JjYyrNhURI3ZssbREohejbnJheQFmWfqjD9BJFk0kO17FgPvrqSlXvU/LK0mpBXjVzWJHNYslxi7kqGIrLhMaMbJgD93p13Atnjqh0nLPuk4h7E3a4xJMWkbOvcjsc6X/2qL46BPaQp8GODmVgwY8qVu3G8rslvPdSWA1s8cw7Ca9Zw36gOjK6Yceka8+5T+g3nywYV/mu6XYc+REiBqdMk/g16VLMRpJ6XwwUP/zfzkhScNg/EAeqyuSTm5B+0tjDoOjSDftJPllvE3R55YymEgs66Yu3qM9m7m33M+hq6Y4fF2qfp3XJi5FX5nqZLmq6izHbAjJKcm9MJ7Dki0mNCALbuO1E+kdnnMG+bufH09VepNo5KPX0E91zURD494agCM=\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-01 07:24:27Z"; 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-HSA-TRUST="\"28a33818a1dce9a0eecde38e7c8fcc6f080b70bc9feb505599fb2855903a4792_HSARMTKNSRVXWFlajR2ecD1662phQjqU9vXxnL49ZjypuVYYXHDpA3wTiX6Mf2J4WDlIhZj52z81aDOuz+VC80bVhV41TSNN4ggoPjW8WnsQrjniTQYkgJycPQNnzhkK4hfe2AMrr/bhrJJm8sHHc+Oh1HUckN6T7T4c1bmf2Qg9tRwsdRDNyMMyFH/Ml/cQlWKj39/YHlY=SRVX\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-06-30 07:24:24Z"; 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-Events="\"S2V5QXBwbDoBAAAA8QQtAAAcRMnkLPigzsB/itJ9jaBjf4fy8r1RMlo1yUP2hCDneg==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-01 07:24:27Z"; 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-Documents="\"S2V5QXBwbDoBAAAAAgQtAAAgYberFOMZGUixcIOai9L24ZHNbu5DaZP6HnmRQ1MSvQ==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-01 07:24:27Z"; 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-Photos="\"S2V5QXBwbDoBAAAAAwQtAACUziNBcSBwEQgghccKEG9k/ygsjsac45cuvR6qqlUkFg==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-01 07:24:27Z"; 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-Cloudkit="\"S2V5QXBwbDoBAAAABAQtAAANeCQ7Cp2T6an+rFbqdrmQ0X4jOkNWxPsx9z1wCl0kTw==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-01 07:24:27Z"; 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-Safari="\"S2V5QXBwbDoBAAAAFgQtAACUhob4CIIW+xGVNeGa7FbKTRx+ZUuIVKj1pNH1kuHAow==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-01 07:24:27Z"; 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-Mail="\"S2V5QXBwbDoBAAAABwQtAABpQkHUvLVS2zGojJeukynTgPUeaeiIOiXhOpK7PRViLA==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-01 07:24:27Z"; 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-Notes="\"S2V5QXBwbDoBAAAACQQtAAAvXaphLtTg6NqJNcDsGV0/3Yz7q9fGYqKTv11baVhQkA==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-01 07:24:27Z"; 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-News="\"S2V5QXBwbDoBAAAACwQtAAA/aiJIyMlZL5ZfM0T8WFuMnV7U2GLwBEMEEbHvoFEFeA==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-01 07:24:27Z"; 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-PCS-Sharing="\"S2V5QXBwbDoBAAAADAQtAADjBjUschddlXGLyz3YOcAY3zH0qIO0CaVA3nBRCdP/ZQ==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-01 07:24:27Z"; 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: X-APPLE-WEBAUTH-TOKEN="\"v=2:t=BA==BST_IAAAAAAABLwIAAAAAGnNJKERDmdzLmljbG91ZC5hdXRovQBxaNXd8uCVvsFuSORQSJveyszMyViirh-1zzSxcPCoxcnSgwKOEgF3_8tM4gdejba-XwvouYJI1zUjfW3GiydXisVR2Dmv3DBWd4RCYvKYvXZAbCL6gCeDd-n6WbZHVCLgbh_ui1teFpujrhahVPqVS57-FQ~~\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-04-15 13:58:58Z"; 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 Set-Cookie3: X-APPLE-WEBAUTH-FMIP="\"BA==BST_IAAAAAAABLwIAAAAAGnNJKERDmdzLmljbG91ZC5hdXRovQBxaNXd8uCVvsFuSORQSJveyszMyViirh-1zzSxcPCoxcnSgwKOEgF3_8tM4gdejba-XwvouYJI1zUjfW3GiydXisVR2Dmv3DBWd4RCYvKYvYEkIgdydG0msGrjkyM6ZcaotQmxM0qAZGrGtEOZnjC6QlkocA~~\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; discard; HttpOnly=None; version=0
Set-Cookie3: xr_3n2093n1a="f16+Jo6W6qPJmRkZzBixaQfuGOyKO+kJ4iSEZyG94A=="; path="/"; domain=p144-fmipweb.icloud.com; path_spec; secure; discard; HttpOnly=None; version=0

View File

@@ -1 +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"} {"client_id": "a803c3ce-2586-11f1-a724-8f6777a1d2b5", "session_id": "0893DF20A53394DF6264EB0D0F8DB8E09184E1B9350AA9983DC5EB457D7466E0C4F6F402DA6AE23A4F2375865E4D40CEABC7C9BCCF30B7801A1566D157830BE7E3FE1BBC39D1C7027A3909782C03A6C4E39719F3D8C77A528AB04ED28F26A2FCEC7DE40693496A4786EB60DB8BB8F76652B08C8895936DF5", "auth_attributes": "UKS2E/hPEAScl9xio3Fb55QVB8lO4pEITqYS19Q2eMgmp+MefOgSFUg9Lvpy6wJB76Z+be0kfF5FpnQqoLWy1PEg1L88urm3SlGJy0t9/wXExC7kX2CIkqi/aU9uOjjcjwUZ0FDXtYXBggGhaIuZCPXw9q6QxPL69u6x7JbMMiZStduwKSINPqRph0cCxnEcP2CpLb7V95ckx8fWT/RDM1sJDp4tDJ16gpp68Lq5rmzg76T+R6E4XM1jI/IW/wuD3oLTfgMni7IWcCW1evDbAAxuPnNk2D4=", "scnt": "AAAA-jA4OTNERjIwQTUzMzk0REY2MjY0RUIwRDBGOERCOEUwOTE4NEUxQjkzNTBBQTk5ODNEQzVFQjQ1N0Q3NDY2RTBDNEY2RjQwMkRBNkFFMjNBNEYyMzc1ODY1RTRENDBDRUFCQzdDOUJDQ0YzMEI3ODAxQTE1NjZEMTU3ODMwQkU3RTNGRTFCQkMzOUQxQzcwMjdBMzkwOTc4MkMwM0E2QzRFMzk3MTlGM0Q4Qzc3QTUyOEFCMDRFRDI4RjI2QTJGQ0VDN0RFNDA2OTM0OTZBNDc4NkVCNjBEQjhCQjhGNzY2NTJCMDhDODg5NTkzNkRGNXwyAAABnUf7l4HF2wAVRtQDtvBaNPXuwIi8ILi0sURwYvD0XvQMHlohK0Rw09XdgbnOAAxuPln3yR2rVrA5k3Uvi2iVG6pdntPmgkWZlqF2lHyUJvt31x4_LQ", "account_country": "USA", "session_token": "ObPQlP1J6GPl0T7quRtCSjFkD/Nauze5JFS3oEhcZUQEY1eIaJhrnMzfuE3duncAF/X7fmhVhY1+UEMzv86A0I8YZJ4gHGVcDNGKtyA6ylYYY6BbLnqPzrYs87OHx5elnRjFMtliBzFa9tWxz29sVAhRKjgb4cyi/1LQY51l7MvODVO1/0xTG9sMP/9LCpKq4NSjj4LUs7rrifaAVB94Vz3v9ln5UlnpMMcrQt7TnMWS0UoHKX8rVk1FR9S/mAWdTq+L/4xC1mIspdC/QoqAzdlpFuDWhe/OtJ4xP6DUJBA3nmQM4R7bOfw5DT2iU4QUwnVUbro16wJxkITvwLSJCwpzdLnpU7jTD6ih7EcrnY1MRUA84LE0vjtOo69LqHsVa5XwW2rLiEiyLWfwmbWX7nxRhIJob9aGX/fRsYgcU95/Nyi0PrAUG6vIPoia39YzM6FX6h+AqPx6QPzSsYzGDGeja5a3Itpr3/PIGniWAsaBk5aJ47mEbMH4Kr18+qZDSGTLExB0+rs/t+EY1FBic1lkHIyzMWbzS18+jYjBY2JKgebqwVacY7c67KRn20yqz2t7JdOcX5wPPUtZw36yFsHpFrfc84O3pLQY55ZoLiH08AMJbcV+ARBVXDh8O7gLjlP75HWmgMNtQ0OeW/ihUC/sPokqoRL3Tm6tKUTEXyIIL9o4fLX1MlrUwOxPjkwODPaqw1tVdRVAopbSQXWvugy6os7nS7bOu4qwHBLsrAVYSY6ky8klP8APjBRo6b9txr/B3II6bZGxsxYKC78HrCZURixRrwEx5mU+Uo8CFOIYeh+cml2VGPiWJcL/j+Zgo+fETjuVszi+k0LA0IksVG3d7LoKCGGfEHkPIB7s3kfvw8zVP7XhNhdIZ7/iHFGjc3qW9fAI5NOJSgpIbOsjNPP2x+LCXfmjfVP5nz43KVFVSe19CgpRjlwzxKXQqs1FHQAMbj5417aW", "trust_eligible": "true", "grant_code": "c29b17d808a184d83999908e110e46cf4.0.srzwz.t2kk4FfezvyceWVfQKXZOg", "trust_token": "HSARMTKNSRVXWFlajR2ecD1662phQjqU9vXxnL49ZjypuVYYXHDpA3wTiX6Mf2J4WDlIhZj52z81aDOuz+VC80bVhV41TSNN4ggoPjW8WnsQrjniTQYkgJycPQNnzhkK4hfe2AMrr/bhrJJm8sHHc+Oh1HUckN6T7T4c1bmf2Qg9tRwsdRDNyMMyFH/Ml/cQlWKj39/YHlY=SRVX"}

View File

@@ -1,5 +1,5 @@
[project] [project]
name = "back-end" name = "pymd3_vue_location_sim"
version = "0.1.0" version = "0.1.0"
requires-python = ">=3.14" requires-python = ">=3.14"
dependencies = [ dependencies = [
@@ -7,6 +7,7 @@ dependencies = [
"daemonize>=2.5.0", "daemonize>=2.5.0",
"fastapi==0.135.1", "fastapi==0.135.1",
"geopy==2.4.1", "geopy==2.4.1",
"numpy==2.4.3",
"pydantic==2.12.5", "pydantic==2.12.5",
"pyicloud>=2.4.1", "pyicloud>=2.4.1",
"pymobiledevice3==9.0.0", "pymobiledevice3==9.0.0",
@@ -18,3 +19,8 @@ dependencies = [
"typing==3.10.0.0", "typing==3.10.0.0",
"uvicorn==0.41.0", "uvicorn==0.41.0",
] ]
[build-system]
requires = ["uv_build>=0.11.2,<0.12"]
build-backend = "uv_build"

View File

@@ -57,6 +57,7 @@ class FindMyMonitor:
self._logged_candidates = False self._logged_candidates = False
self._no_location_streak = 0 self._no_location_streak = 0
self._auth_error_streak = 0 self._auth_error_streak = 0
self._fetch_lock = asyncio.Lock()
async def _request_code_from_vue(self, prompt: str) -> str | None: async def _request_code_from_vue(self, prompt: str) -> str | None:
if self.sio is None or self.get_client_sids is None: if self.sio is None or self.get_client_sids is None:
@@ -287,6 +288,11 @@ class FindMyMonitor:
idx = min(self._auth_error_streak - 1, len(schedule) - 1) idx = min(self._auth_error_streak - 1, len(schedule) - 1)
return max(base_interval, schedule[idx]) return max(base_interval, schedule[idx])
async def refresh_location(self):
"""Fetch one location update while serializing iCloud API access."""
async with self._fetch_lock:
return await self.get_location()
async def run_monitor(self, interval=60): async def run_monitor(self, interval=60):
"""Runs the monitor loop.""" """Runs the monitor loop."""
if not await self.authenticate(): if not await self.authenticate():
@@ -298,7 +304,7 @@ class FindMyMonitor:
while self.running: while self.running:
sleep_seconds = interval sleep_seconds = interval
try: try:
device_data = await self.get_location() device_data = await self.refresh_location()
if device_data is not None: if device_data is not None:
self._no_location_streak = 0 self._no_location_streak = 0
self._auth_error_streak = 0 self._auth_error_streak = 0

View File

@@ -98,10 +98,11 @@ class LocationSimulationState:
self.loc_id: Optional[str] = None self.loc_id: Optional[str] = None
self.longitude: Optional[float] = None self.longitude: Optional[float] = None
self.next_move: Optional[float] = None self.next_move: Optional[float] = None
self.queue: asyncio.Queue = asyncio.Queue() self.simulation_queue: asyncio.Queue = asyncio.Queue()
self.queue_data: Dict = {} self.simulation_queue_data: Dict = {}
self.queue_order: list[str] = [] self.simulation_queue_order: list[str] = []
self.queue_state: str = "STOPPED" self.simulation_queue_state: str = "STOPPED"
self.simulation_noise: bool = False
self.set_location_enabled: bool = True self.set_location_enabled: bool = True
self.simulation_active: bool = False self.simulation_active: bool = False
self.simulation_task: Optional[asyncio.Task] = None self.simulation_task: Optional[asyncio.Task] = None
@@ -441,7 +442,7 @@ class TunneldRunnerSio:
"""Start Simulation Queue Worker""" """Start Simulation Queue Worker"""
logger.info("Starting location simulation worker...") logger.info("Starting location simulation worker...")
self.context.simulation_active = True self.context.simulation_active = True
self.context.queue_state = "RUNNING" self.context.simulation_queue_state = "RUNNING"
try: try:
if self.context.test_mode: if self.context.test_mode:
logger.info("Simulation worker: test mode enabled") logger.info("Simulation worker: test mode enabled")
@@ -544,6 +545,77 @@ class TunneldRunnerSio:
self.context.simulation_task = None self.context.simulation_task = None
await end_icloud_monitor() await end_icloud_monitor()
async def add_location_to_simulation_queue(data):
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 {"status": "error","command": "add", "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.simulation_queue_data and len(self.context.simulation_queue_order) > 1:
current_index = get_item_index(self.context.loc_id) if self.context.loc_id else 0
remaining_items = self.context.simulation_queue_order[current_index + 1:]
accrued_delay = sum(parse_delay_seconds(self.context.simulation_queue_data[loc_id]['delay']) for loc_id in remaining_items if loc_id in self.context.simulation_queue_data)
accrued_delay = accrued_delay + parse_delay_seconds(self.context.next_move)
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,
}
resp = {
"status": "OK",
"command": "add",
"message": f"Location {loc_id} added to the queue",
"item": location_item,
}
await self.context.simulation_queue.put(loc_id)
add_item(loc_id, location_item)
logger.info("Location %s added to the queue", loc_id)
else:
logger.error("Invalid location data: %s", data)
resp = {"status": "error", "message": "Invalid location data", "data": data}
return resp
async def start_icloud_monitor(): async def start_icloud_monitor():
"""Start Apple iCloud Find My Monitor to retreive actual reported device location""" """Start Apple iCloud Find My Monitor to retreive actual reported device location"""
logger.info("iCloud monitor start requested") logger.info("iCloud monitor start requested")
@@ -579,6 +651,44 @@ class TunneldRunnerSio:
finally: finally:
self.context.fmf_queue.task_done() self.context.fmf_queue.task_done()
async def refresh_icloud_location() -> dict:
"""Fetch one iCloud location update, with or without monitor loop running."""
logger.info("iCloud monitor refresh requested")
try:
updated_location = await self.context.icloud_monitor.refresh_location()
except Exception as e:
logger.exception("Failed to refresh iCloud location: %s", e)
return {
"status": "error",
"message": "Failed to refresh iCloud location",
"error": type(e).__name__,
"icloud_monitor_enabled": self.context.icloud_monitor_enabled,
"icloud_monitor_running": is_icloud_monitor_running(),
}
if updated_location is None:
return {
"status": "ok",
"location_found": False,
"location_updated": False,
"icloud_monitor_enabled": self.context.icloud_monitor_enabled,
"icloud_monitor_running": is_icloud_monitor_running(),
}
location_updated = self.context.fmf_location != updated_location
self.context.fmf_location = updated_location
await self.context.sio.emit(
"fmf_update", updated_location.model_dump(), namespace="/"
)
return {
"status": "ok",
"location_found": True,
"location_updated": location_updated,
"icloud_monitor_enabled": self.context.icloud_monitor_enabled,
"icloud_monitor_running": is_icloud_monitor_running(),
"fmf_location": updated_location,
}
async def end_icloud_monitor(): async def end_icloud_monitor():
logger.info("iCloud monitor stop requested") logger.info("iCloud monitor stop requested")
self.context.icloud_monitor_enabled = False self.context.icloud_monitor_enabled = False
@@ -604,29 +714,42 @@ class TunneldRunnerSio:
async def pause_simulation_queue(): async def pause_simulation_queue():
"""Pauses asyncio.Queue playback""" """Pauses asyncio.Queue playback"""
self.context.queue_state = "PAUSED" self.context.simulation_queue_state = "PAUSED"
async def resume_simulation_queue(): async def resume_simulation_queue():
"""Resumes asyncio.Queue playback""" """Resumes asyncio.simulation_queue playback"""
self.context.queue_state = "RUNNING" self.context.simulation_queue_state = "RUNNING"
update_queue_times() update_queue_times()
def update_queue_times(): def update_queue_times():
current_index = get_item_index(self.context.loc_id) current_index = get_item_index(self.context.loc_id)
remaining_items = self.context.queue_order[current_index + 1:] remaining_items = self.context.simulation_queue_order[current_index + 1:]
new_delay = self.context.next_move or 0 new_delay = self.context.next_move or 0
now_time = datetime.now(timezone.utc) now_time = datetime.now(timezone.utc)
for item in remaining_items: for item in remaining_items:
item_delay = parse_delay_seconds(self.context.queue_data[item].get("delay")) or 0 item_delay = parse_delay_seconds(self.context.simulation_queue_data[item].get("delay")) or 0
new_delay += item_delay new_delay += item_delay
new_time = (now_time + timedelta(seconds=new_delay)).isoformat() new_time = (now_time + timedelta(seconds=new_delay)).isoformat()
self.context.queue_data[item].start = new_time self.context.simulation_queue_data[item].start = new_time
update_queue_data()
def update_queue_data():
data = {
"simulation_queue": {
"active": self.context.simulation_active,
"data": self.context.simulation_queue_data,
"order": self.context.simulation_queue_order,
"state": self.context.simulation_queue_state,
"worker_task": self.context.simulation_task.get_name() if self.context.simulation_task else None,
}
}
self.context.sio.emit("queue_data_update", { "data": data }, namespace="/" )
async def empty_simulation_queue(): async def empty_simulation_queue():
"""Empties all items from an asyncio.Queue.""" """Empties all items from an asyncio.Queue."""
logger.info("Clearing location simulation queue...") logger.info("Clearing location simulation queue...")
q = self.context.queue q = self.context.simulation_queue
self.context.set_location_enabled = False self.context.set_location_enabled = False
while not q.empty(): while not q.empty():
try: try:
@@ -639,38 +762,38 @@ class TunneldRunnerSio:
def add_item(item_id, payload): def add_item(item_id, payload):
self.context.queue_data[item_id] = payload self.context.simulation_queue_data[item_id] = payload
self.context.queue_order.append(item_id) self.context.simulation_queue_order.append(item_id)
def remove_item(item_id): def remove_item(item_id):
if item_id in self.context.queue_order: if item_id in self.context.simulation_queue_order:
# self.context.queue_order.remove(item_id) # self.context.simulation_queue_order.remove(item_id)
self.context.queue_data[item_id]["status"] = "deleted" self.context.simulation_queue_data[item_id]["status"] = "deleted"
def clear_item(item_id): def clear_item(item_id):
if item_id in self.context.queue_order: if item_id in self.context.simulation_queue_order:
self.context.queue_order.remove(item_id) self.context.simulation_queue_order.remove(item_id)
del self.context.queue_data[item_id] del self.context.simulation_queue_data[item_id]
def clear_items(): def clear_items():
self.context.queue_data = {} self.context.simulation_queue_data = {}
self.context.queue_order = [] self.context.simulation_queue_order = []
def get_item(item_id): def get_item(item_id):
return self.context.queue_data[item_id] return self.context.simulation_queue_data[item_id]
def update_item(item_id, **updates): def update_item(item_id, **updates):
if item_id in self.context.queue_data: if item_id in self.context.simulation_queue_data:
self.context.queue_data[item_id].update(updates) self.context.simulation_queue_data[item_id].update(updates)
def get_item_index(item_id): def get_item_index(item_id):
return self.context.queue_order.index(item_id) return self.context.simulation_queue_order.index(item_id)
def get_item_id_by_index(index): def get_item_id_by_index(index):
return self.context.queue_order[index] return self.context.simulation_queue_order[index]
def get_items_in_order(): def get_items_in_order():
return [self.context.queue_data[i] for i in self.context.queue_order if self.context.queue_data[i].get("status") != "deleted"] return [self.context.simulation_queue_data[i] for i in self.context.simulation_queue_order if self.context.simulation_queue_data[i].get("status") != "deleted"]
def parse_delay_seconds(raw_delay) -> int: def parse_delay_seconds(raw_delay) -> int:
if raw_delay is None: if raw_delay is None:
@@ -695,9 +818,9 @@ class TunneldRunnerSio:
"""Ends asyncio.Queue playback and closes tunnel""" """Ends asyncio.Queue playback and closes tunnel"""
logger.info("End location simulation request") logger.info("End location simulation request")
try: try:
q = self.context.queue q = self.context.simulation_queue
self.context.set_location_enabled = False self.context.set_location_enabled = False
self.context.queue_state = "SHUTDOWN" self.context.simulation_queue_state = "SHUTDOWN"
# Drain pending queue entries. # Drain pending queue entries.
while not q.empty(): while not q.empty():
@@ -731,23 +854,26 @@ class TunneldRunnerSio:
self.context.simulation_active = False self.context.simulation_active = False
self.context.simulation_task = None self.context.simulation_task = None
self.context.simulation_state = "ENDED"
self.context.set_location_enabled = True self.context.set_location_enabled = True
self.context.next_move = None self.context.next_move = None
self.context.loc_id = None self.context.loc_id = None
self.context.latitude = None self.context.latitude = None
self.context.longitude = None self.context.longitude = None
self.context.queue_state = "STOPPED" self.context.simulation_queue_state = "STOPPED"
# Recreate queue to discard sentinel wakeup items and unblock clean restarts. # Recreate queue to discard sentinel wakeup items and unblock clean restarts.
self.context.queue = asyncio.Queue() self.context.simulation_queue = asyncio.Queue()
await end_icloud_monitor() await end_icloud_monitor()
return True return True
except Exception as e: except Exception as e:
logger.error(f"Error ending simulation queue: {e}") logger.error(f"Error ending simulation queue: {e}")
return False return False
def toggle_test_mode() -> dict:
self.context.test_mode = not self.context.test_mode
return {"test_mode": self.context.test_mode }
def get_status() -> dict: def get_status() -> dict:
current_item = self.context.queue_data.get(self.context.loc_id) if self.context.loc_id else None current_item = self.context.simulation_queue_data.get(self.context.loc_id) if self.context.loc_id else None
current_start = ( current_start = (
current_item.get("start") current_item.get("start")
if isinstance(current_item, dict) if isinstance(current_item, dict)
@@ -774,9 +900,9 @@ class TunneldRunnerSio:
"next_move": self.context.next_move, "next_move": self.context.next_move,
"simulation_queue": { "simulation_queue": {
"active": self.context.simulation_active, "active": self.context.simulation_active,
"data": self.context.queue_data, "data": self.context.simulation_queue_data,
"order": self.context.queue_order, "order": self.context.simulation_queue_order,
"state": self.context.queue_state, "state": self.context.simulation_queue_state,
"worker_task": self.context.simulation_task.get_name() if self.context.simulation_task else None, "worker_task": self.context.simulation_task.get_name() if self.context.simulation_task else None,
}, },
"set_location_enable": self.context.set_location_enabled, "set_location_enable": self.context.set_location_enabled,
@@ -786,6 +912,27 @@ class TunneldRunnerSio:
} }
return data return data
import numpy as np
def add_gps_noise(lat, lon, std_dev_meters=5):
"""
Simulates GPS noise by adding Gaussian noise to coordinates.
1 degree of latitude ~ 111,000 meters.
1 degree of longitude ~ 111,000 * cos(latitude) meters.
"""
# Earth's radius, meters
earth_radius = 6378137
# Convert meters to degrees
lat_diff = (std_dev_meters / earth_radius) * (180 / np.pi)
lon_diff = (std_dev_meters / (earth_radius * np.cos(np.radians(lat)))) * (180 / np.pi)
# Generate Gaussian noise
noised_lat = lat + np.random.normal(0, lat_diff)
noised_lon = lon + np.random.normal(0, lon_diff)
return noised_lat, noised_lon
""" FastAPI HTTP Functions""" """ FastAPI HTTP Functions"""
def generate_http_response( def generate_http_response(
@@ -923,7 +1070,7 @@ class TunneldRunnerSio:
task=task, udid=udid task=task, udid=udid
) )
created_task = True created_task = True
except ConnectionFailedError, InvalidServiceError, MuxException: except (ConnectionFailedError, InvalidServiceError, MuxException):
pass pass
if connection_type in ("usb", None): if connection_type in ("usb", None):
for rsd in await get_rsds(udid=udid): for rsd in await get_rsds(udid=udid):
@@ -1041,6 +1188,11 @@ class TunneldRunnerSio:
} }
return generate_http_response(data) return generate_http_response(data)
@self._app.get("/icloud-monitor/refresh")
async def app_refresh_icloud_monitor() -> fastapi.Response:
data = await refresh_icloud_location()
return generate_http_response(data)
"""Simulation Functions""" """Simulation Functions"""
""" start, add, clear, pause, resume, end, status """ """ start, add, clear, pause, resume, end, status """
@@ -1055,77 +1207,7 @@ class TunneldRunnerSio:
async def app_add_location(data: SimulationRequestData) -> fastapi.Response: async def app_add_location(data: SimulationRequestData) -> fastapi.Response:
"""Add a location to the simulation queue""" """Add a location to the simulation queue"""
logger.info("Request to add new location to queue") logger.info("Request to add new location to queue")
resp = await add_location_to_simulation_queue(data)
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:
current_index = get_item_index(self.context.loc_id)
remaining_items = self.context.queue_order[current_index + 1:]
accrued_delay = sum(parse_delay_seconds(self.context.queue_data[loc_id]['delay']) for loc_id in remaining_items if loc_id in self.context.queue_data)
accrued_delay = accrued_delay + parse_delay_seconds(self.context.next_move)
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,
}
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:
logger.error("Invalid location data: %s", data)
resp = {"status": "error", "message": "Invalid location data", "data": data}
return generate_http_response(resp) return generate_http_response(resp)
@self._app.get("/simulation/clear") @self._app.get("/simulation/clear")
@@ -1160,12 +1242,12 @@ class TunneldRunnerSio:
async def app_simulation_test_mode() -> fastapi.Response: async def app_simulation_test_mode() -> fastapi.Response:
"""Enable test mode for the simulation queue""" """Enable test mode for the simulation queue"""
before_toggle = self.context.test_mode before_toggle = self.context.test_mode
self.context.test_mode = not self.context.test_mode data = toggle_test_mode()
data = {"status": "Ok", "message": f"Test mode toggled from {before_toggle} to {self.context.test_mode}"} data['status'] = "OK"
data['message'] = f"Test mode toggled from {before_toggle} to {self.context.test_mode}"
return generate_http_response(data) return generate_http_response(data)
"""Status Functions""" """Status Functions"""
@self._app.get("/status/rsd") @self._app.get("/status/rsd")
@@ -1289,88 +1371,19 @@ class TunneldRunnerSio:
) )
try: try:
match command: match command:
case "test-mode":
data = toggle_test_mode()
return {
"command": command,
"status": "OK",
"message": "test-mode toggled",
"data": data
}
case "add": case "add":
""" Add a location to the simulation queue""" """ Add a location to the simulation queue"""
loc_id = str(uuid.uuid4()) resp = await add_location_to_simulation_queue(data)
latitude = ( return resp
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:
current_index = get_item_index(self.context.loc_id)
remaining_items = self.context.queue_order[current_index + 1:]
accrued_delay = sum(parse_delay_seconds(self.context.queue_data[loc_id]['delay']) for loc_id in remaining_items if loc_id in self.context.queue_data)
accrued_delay = accrued_delay + parse_delay_seconds(self.context.next_move)
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": data,
}
case "clear": case "clear":
""" Clear the simulation queue""" """ Clear the simulation queue"""
await empty_simulation_queue() await empty_simulation_queue()
@@ -1448,6 +1461,10 @@ class TunneldRunnerSio:
"icloud_monitor_enabled": self.context.icloud_monitor_enabled, "icloud_monitor_enabled": self.context.icloud_monitor_enabled,
"icloud_monitor_running": is_icloud_monitor_running(), "icloud_monitor_running": is_icloud_monitor_running(),
} }
case "refresh":
data = await refresh_icloud_location()
data["command"] = command
return data
case _: case _:
return { return {
"command": command, "command": command,
@@ -1583,27 +1600,84 @@ class LocationSimulationQueue(LocationSimulation):
def __init__(self, dvt, context: LocationSimulationState): def __init__(self, dvt, context: LocationSimulationState):
super().__init__(dvt) super().__init__(dvt)
self.context = context self.context = context
self._noise_task: Optional[asyncio.Task] = None
self._noise_loc_id: Optional[str] = None
@staticmethod
def _add_gps_noise(lat: float, lon: float, std_dev_meters: float = 5.0) -> tuple[float, float]:
"""Apply Gaussian jitter in meters and convert to lat/lon deltas."""
earth_radius = 6378137.0
lat_sigma_deg = (std_dev_meters / earth_radius) * (180.0 / math.pi)
cos_lat = math.cos(math.radians(lat))
if abs(cos_lat) < 1e-6:
cos_lat = 1e-6
lon_sigma_deg = (std_dev_meters / (earth_radius * cos_lat)) * (180.0 / math.pi)
noised_lat = lat + random.gauss(0.0, lat_sigma_deg)
noised_lon = lon + random.gauss(0.0, lon_sigma_deg)
return noised_lat, noised_lon
async def _stop_noise_task(self) -> None:
if self._noise_task is not None:
self._noise_task.cancel()
with suppress(asyncio.CancelledError):
await self._noise_task
self._noise_task = None
self._noise_loc_id = None
async def _noise_loop(self, loc_id: str, base_latitude: float, base_longitude: float) -> None:
while True:
await asyncio.sleep(random.randint(45, 180))
if not self.context.simulation_active:
break
if self.context.simulation_queue_state == "SHUTDOWN":
break
if not self.context.set_location_enabled:
continue
if not self.context.simulation_noise:
continue
if self.context.loc_id != loc_id:
break
noised_latitude, noised_longitude = self._add_gps_noise(
base_latitude, base_longitude
)
await self.set(noised_latitude, noised_longitude)
logger.info(
"Applied simulation noise to active location loc_id=%s sent=%s,%s base=%s,%s",
loc_id,
noised_latitude,
noised_longitude,
base_latitude,
base_longitude,
)
def _start_noise_task(self, loc_id: str, base_latitude: float, base_longitude: float) -> None:
self._noise_loc_id = loc_id
self._noise_task = asyncio.create_task(
self._noise_loop(loc_id, base_latitude, base_longitude),
name=f"simulation-noise-{loc_id}",
)
async def play_queue( async def play_queue(
self, disable_sleep: bool = False, timing_randomness_range: int = 0 self, disable_sleep: bool = False, timing_randomness_range: int = 0
) -> None: ) -> None:
try:
while True: while True:
if self.context.queue_state == "PAUSED": if self.context.simulation_queue_state == "PAUSED":
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
continue continue
if self.context.queue_state == "SHUTDOWN": if self.context.simulation_queue_state == "SHUTDOWN":
break break
loc_id = await self.context.queue.get() loc_id = await self.context.simulation_queue.get()
if loc_id is None: if loc_id is None:
self.context.queue.task_done() self.context.simulation_queue.task_done()
break break
location_item = self.context.queue_data.get(loc_id) location_item = self.context.simulation_queue_data.get(loc_id)
if location_item is None: if location_item is None:
logger.warning( logger.warning(
"Simulation queue item missing for loc_id=%s; skipping stale entry", "Simulation queue item missing for loc_id=%s; skipping stale entry",
loc_id, loc_id,
) )
self.context.queue.task_done() self.context.simulation_queue.task_done()
continue continue
new_latitude = location_item.get("latitude") new_latitude = location_item.get("latitude")
new_longitude = location_item.get("longitude") new_longitude = location_item.get("longitude")
@@ -1611,7 +1685,7 @@ class LocationSimulationQueue(LocationSimulation):
new_delay = 0 if new_delay is None else new_delay new_delay = 0 if new_delay is None else new_delay
new_start = location_item.get("start") new_start = location_item.get("start")
current_location_item = self.context.queue_data.get(self.context.loc_id) current_location_item = self.context.simulation_queue_data.get(self.context.loc_id)
current_latitude = ( current_latitude = (
current_location_item.get("latitude") current_location_item.get("latitude")
if isinstance(current_location_item, dict) if isinstance(current_location_item, dict)
@@ -1637,13 +1711,13 @@ class LocationSimulationQueue(LocationSimulation):
) )
countdown_delay = int(round(float(new_delay))) countdown_delay = int(round(float(new_delay)))
for i in range(max(0, countdown_delay), 0, -1): for i in range(max(0, countdown_delay), 0, -1):
if self.context.queue_state == "SHUTDOWN": if self.context.simulation_queue_state == "SHUTDOWN":
break break
while self.context.queue_state == "PAUSED": while self.context.simulation_queue_state == "PAUSED":
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
if self.context.queue_state == "SHUTDOWN": if self.context.simulation_queue_state == "SHUTDOWN":
break break
if self.context.queue_state == "SHUTDOWN": if self.context.simulation_queue_state == "SHUTDOWN":
break break
self.context.next_move = i self.context.next_move = i
await self.context.sio.emit( await self.context.sio.emit(
@@ -1659,16 +1733,21 @@ class LocationSimulationQueue(LocationSimulation):
namespace="/", namespace="/",
) )
await asyncio.sleep(1) await asyncio.sleep(1)
if self.context.queue_state == "SHUTDOWN": if self.context.simulation_queue_state == "SHUTDOWN":
self.context.queue.task_done() self.context.simulation_queue.task_done()
break break
self.context.queue_data[loc_id]["start"] = datetime.now(timezone.utc).isoformat() self.context.simulation_queue_data[loc_id]["start"] = datetime.now(timezone.utc).isoformat()
if self.context.loc_id is not None: if self.context.loc_id is not None:
self.context.queue_data[self.context.loc_id]["end"] = datetime.now(timezone.utc).isoformat() self.context.simulation_queue_data[self.context.loc_id]["end"] = datetime.now(timezone.utc).isoformat()
update_queue_data()
await self._stop_noise_task()
await self.set(new_latitude, new_longitude) await self.set(new_latitude, new_longitude)
self.context.loc_id = loc_id self.context.loc_id = loc_id
self.context.latitude = new_latitude self.context.latitude = new_latitude
self.context.longitude = new_longitude self.context.longitude = new_longitude
if self.context.simulation_noise:
self._start_noise_task(loc_id, new_latitude, new_longitude)
await self.context.sio.emit( await self.context.sio.emit(
"simulation_status", "simulation_status",
{ {
@@ -1687,7 +1766,9 @@ class LocationSimulationQueue(LocationSimulation):
new_longitude, new_longitude,
new_delay, new_delay,
) )
self.context.queue.task_done() self.context.simulation_queue.task_done()
finally:
await self._stop_noise_task()
class LocationSimulationTestQueue(LocationSimulationBase): class LocationSimulationTestQueue(LocationSimulationBase):
@@ -1706,9 +1787,9 @@ class LocationSimulationTestQueue(LocationSimulationBase):
logger.info("Simulated location set to %s, %s", latitude, longitude) logger.info("Simulated location set to %s, %s", latitude, longitude)
async def clear(self) -> None: async def clear(self) -> None:
q = self.context.queue q = self.context.simulation_queue
self.context.set_location_enabled = False self.context.set_location_enabled = False
self.context.queue_state = "SHUTDOWN" self.context.simulation_queue_state = "SHUTDOWN"
while not q.empty(): while not q.empty():
try: try:
item = q.get_nowait() item = q.get_nowait()
@@ -1726,36 +1807,36 @@ class LocationSimulationTestQueue(LocationSimulationBase):
with suppress(asyncio.CancelledError): with suppress(asyncio.CancelledError):
await self.context.simulation_task await self.context.simulation_task
self.context.simulation_active = False self.context.simulation_active = False
self.context.queue_state = "SHUTDOWN" self.context.simulation_queue_state = "SHUTDOWN"
async def play_queue( async def play_queue(
self, disable_sleep: bool = False, timing_randomness_range: int = 0 self, disable_sleep: bool = False, timing_randomness_range: int = 0
) -> None: ) -> None:
while True: while True:
if self.context.queue_state == "PAUSED": if self.context.simulation_queue_state == "PAUSED":
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
continue continue
if self.context.queue_state == "SHUTDOWN": if self.context.simulation_queue_state == "SHUTDOWN":
break break
loc_id = await self.context.queue.get() loc_id = await self.context.simulation_queue.get()
if loc_id is None: if loc_id is None:
self.context.queue.task_done() self.context.simulation_queue.task_done()
break break
location_item = self.context.queue_data.get(loc_id) location_item = self.context.simulation_queue_data.get(loc_id)
if location_item is None: if location_item is None:
logger.warning( logger.warning(
"Test simulation queue item missing for loc_id=%s; skipping stale entry", "Test simulation queue item missing for loc_id=%s; skipping stale entry",
loc_id, loc_id,
) )
self.context.queue.task_done() self.context.simulation_queue.task_done()
continue continue
new_latitude = location_item.get("latitude") new_latitude = location_item.get("latitude")
new_longitude = location_item.get("longitude") new_longitude = location_item.get("longitude")
new_delay = location_item.get("delay") new_delay = location_item.get("delay")
new_delay = 0 if delay is None else delay new_delay = 0 if new_delay is None else new_delay
new_start = location_item.get("start") new_start = location_item.get("start")
current_location_item = self.context.queue_data.get(self.context.loc_id) current_location_item = self.context.simulation_queue_data.get(self.context.loc_id)
current_latitude = ( current_latitude = (
current_location_item.get("latitude") current_location_item.get("latitude")
if isinstance(current_location_item, dict) if isinstance(current_location_item, dict)
@@ -1781,13 +1862,13 @@ class LocationSimulationTestQueue(LocationSimulationBase):
) )
countdown_delay = int(round(float(new_delay))) countdown_delay = int(round(float(new_delay)))
for i in range(max(0, countdown_delay), 0, -1): for i in range(max(0, countdown_delay), 0, -1):
if self.context.queue_state == "SHUTDOWN": if self.context.simulation_queue_state == "SHUTDOWN":
break break
while self.context.queue_state == "PAUSED": while self.context.simulation_queue_state == "PAUSED":
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
if self.context.queue_state == "SHUTDOWN": if self.context.simulation_queue_state == "SHUTDOWN":
break break
if self.context.queue_state == "SHUTDOWN": if self.context.simulation_queue_state == "SHUTDOWN":
break break
self.context.next_move = i self.context.next_move = i
await self.context.sio.emit( await self.context.sio.emit(
@@ -1803,14 +1884,15 @@ class LocationSimulationTestQueue(LocationSimulationBase):
namespace="/", namespace="/",
) )
await asyncio.sleep(1) await asyncio.sleep(1)
if self.context.queue_state == "SHUTDOWN": if self.context.simulation_queue_state == "SHUTDOWN":
self.context.queue.task_done() self.context.simulation_queue.task_done()
break break
self.context.queue_data[loc_id]["start"] = datetime.now(timezone.utc).isoformat() self.context.simulation_queue_data[loc_id]["start"] = datetime.now(timezone.utc).isoformat()
if self.context.loc_id is not None: if self.context.loc_id is not None:
self.context.queue_data[self.context.loc_id]["end"] = datetime.now(timezone.utc).isoformat() self.context.simulation_queue_data[self.context.loc_id]["end"] = datetime.now(timezone.utc).isoformat()
await self.set(new_latitude, new_longitude) await self.set(new_latitude, new_longitude)
self.context.loc_id = loc_id self.context.loc_id = loc_id
self.context.loc_id = loc_id
self.context.latitude = new_latitude self.context.latitude = new_latitude
self.context.longitude = new_longitude self.context.longitude = new_longitude
await self.context.sio.emit( await self.context.sio.emit(
@@ -1822,14 +1904,13 @@ class LocationSimulationTestQueue(LocationSimulationBase):
"longitude": self.context.longitude, "longitude": self.context.longitude,
"start": new_start, "start": new_start,
"next_move": None, "next_move": None,
"next_move": None,
}, },
namespace="/", namespace="/",
) )
logger.info( logger.info(
"Set simulated location to %s, %s after %ss delay", "Set simulated location to %s, %s after %ss delay",
latitude, new_latitude,
longitude, new_longitude,
delay, new_delay,
) )
self.context.queue.task_done() self.context.simulation_queue.task_done()

109
uv.lock generated
View File

@@ -88,45 +88,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" },
] ]
[[package]]
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" },
]
[[package]] [[package]]
name = "bidict" name = "bidict"
version = "0.23.1" version = "0.23.1"
@@ -802,6 +763,35 @@ 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" }, { 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 = "numpy"
version = "2.4.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563, upload-time = "2026-03-09T07:57:43.817Z" },
{ url = "https://files.pythonhosted.org/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161, upload-time = "2026-03-09T07:57:46.169Z" },
{ url = "https://files.pythonhosted.org/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738, upload-time = "2026-03-09T07:57:48.506Z" },
{ url = "https://files.pythonhosted.org/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618, upload-time = "2026-03-09T07:57:50.591Z" },
{ url = "https://files.pythonhosted.org/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676, upload-time = "2026-03-09T07:57:52.34Z" },
{ url = "https://files.pythonhosted.org/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492, upload-time = "2026-03-09T07:57:54.91Z" },
{ url = "https://files.pythonhosted.org/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789, upload-time = "2026-03-09T07:57:57.641Z" },
{ url = "https://files.pythonhosted.org/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941, upload-time = "2026-03-09T07:58:00.577Z" },
{ url = "https://files.pythonhosted.org/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503, upload-time = "2026-03-09T07:58:03.331Z" },
{ url = "https://files.pythonhosted.org/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915, upload-time = "2026-03-09T07:58:06.353Z" },
{ url = "https://files.pythonhosted.org/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875, upload-time = "2026-03-09T07:58:08.734Z" },
{ url = "https://files.pythonhosted.org/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225, upload-time = "2026-03-09T07:58:11.034Z" },
{ url = "https://files.pythonhosted.org/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769, upload-time = "2026-03-09T07:58:13.67Z" },
{ url = "https://files.pythonhosted.org/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461, upload-time = "2026-03-09T07:58:15.912Z" },
{ url = "https://files.pythonhosted.org/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809, upload-time = "2026-03-09T07:58:17.787Z" },
{ url = "https://files.pythonhosted.org/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242, upload-time = "2026-03-09T07:58:20.476Z" },
{ url = "https://files.pythonhosted.org/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660, upload-time = "2026-03-09T07:58:23.067Z" },
{ url = "https://files.pythonhosted.org/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384, upload-time = "2026-03-09T07:58:25.839Z" },
{ url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" },
{ url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" },
{ url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" },
]
[[package]] [[package]]
name = "opack2" name = "opack2"
version = "0.0.1" version = "0.0.1"
@@ -1125,6 +1115,47 @@ version = "0.3.4"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/dc/9ae75ede398b7adf538f2d1dca0f96c645c4e96789f8039340a0ed6a8a8f/pylzss-0.3.4.tar.gz", hash = "sha256:16818631742488e53a34fda0d402d80edb2b812e11877801e21a9e5ce9b9db1c", size = 25144, upload-time = "2023-11-24T00:26:37.787Z" } sdist = { url = "https://files.pythonhosted.org/packages/7d/dc/9ae75ede398b7adf538f2d1dca0f96c645c4e96789f8039340a0ed6a8a8f/pylzss-0.3.4.tar.gz", hash = "sha256:16818631742488e53a34fda0d402d80edb2b812e11877801e21a9e5ce9b9db1c", size = 25144, upload-time = "2023-11-24T00:26:37.787Z" }
[[package]]
name = "pymd3-vue-location-sim"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "click" },
{ name = "daemonize" },
{ name = "fastapi" },
{ name = "geopy" },
{ name = "numpy" },
{ 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 = "numpy", specifier = "==2.4.3" },
{ 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" },
]
[[package]] [[package]]
name = "pymobiledevice3" name = "pymobiledevice3"
version = "9.0.0" version = "9.0.0"