light-phone-api 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. light_phone_api-0.1.0/.gitignore +7 -0
  2. light_phone_api-0.1.0/PKG-INFO +11 -0
  3. light_phone_api-0.1.0/README.md +0 -0
  4. light_phone_api-0.1.0/light_api/__init__.py +11 -0
  5. light_phone_api-0.1.0/light_api/client.py +273 -0
  6. light_phone_api-0.1.0/light_api/music.py +552 -0
  7. light_phone_api-0.1.0/light_api/notes.py +228 -0
  8. light_phone_api-0.1.0/light_api/podcast.py +177 -0
  9. light_phone_api-0.1.0/light_api/tools.py +175 -0
  10. light_phone_api-0.1.0/open_api_specification_client/__init__.py +8 -0
  11. light_phone_api-0.1.0/open_api_specification_client/api/__init__.py +1 -0
  12. light_phone_api-0.1.0/open_api_specification_client/api/default/__init__.py +1 -0
  13. light_phone_api-0.1.0/open_api_specification_client/api/default/delete_api_audios_audio_id.py +102 -0
  14. light_phone_api-0.1.0/open_api_specification_client/api/default/delete_api_device_tools_device_tool_id.py +102 -0
  15. light_phone_api-0.1.0/open_api_specification_client/api/default/delete_api_followed_podcasts_followed_podcast_id.py +165 -0
  16. light_phone_api-0.1.0/open_api_specification_client/api/default/delete_api_notes_note_id.py +102 -0
  17. light_phone_api-0.1.0/open_api_specification_client/api/default/get_api_audio_capacity.py +168 -0
  18. light_phone_api-0.1.0/open_api_specification_client/api/default/get_api_devices.py +136 -0
  19. light_phone_api-0.1.0/open_api_specification_client/api/default/get_api_followed_podcasts.py +168 -0
  20. light_phone_api-0.1.0/open_api_specification_client/api/default/get_api_notes.py +166 -0
  21. light_phone_api-0.1.0/open_api_specification_client/api/default/get_api_notes_note_id.py +192 -0
  22. light_phone_api-0.1.0/open_api_specification_client/api/default/get_api_notes_note_id_generate_presigned_get_url.py +165 -0
  23. light_phone_api-0.1.0/open_api_specification_client/api/default/get_api_notes_note_id_generate_presigned_put_url.py +165 -0
  24. light_phone_api-0.1.0/open_api_specification_client/api/default/get_api_playlist_items.py +183 -0
  25. light_phone_api-0.1.0/open_api_specification_client/api/default/get_api_playlists.py +168 -0
  26. light_phone_api-0.1.0/open_api_specification_client/api/default/get_api_podcasts.py +168 -0
  27. light_phone_api-0.1.0/open_api_specification_client/api/default/get_api_tools.py +166 -0
  28. light_phone_api-0.1.0/open_api_specification_client/api/default/get_api_tools_tool_id.py +163 -0
  29. light_phone_api-0.1.0/open_api_specification_client/api/default/get_api_users_current.py +136 -0
  30. light_phone_api-0.1.0/open_api_specification_client/api/default/patch_api_audios_audio_id.py +184 -0
  31. light_phone_api-0.1.0/open_api_specification_client/api/default/patch_api_notes_note_id.py +192 -0
  32. light_phone_api-0.1.0/open_api_specification_client/api/default/patch_api_playlist_items_playlist_item_id.py +186 -0
  33. light_phone_api-0.1.0/open_api_specification_client/api/default/post_api_audios.py +168 -0
  34. light_phone_api-0.1.0/open_api_specification_client/api/default/post_api_audios_delete_all.py +103 -0
  35. light_phone_api-0.1.0/open_api_specification_client/api/default/post_api_authorizations.py +160 -0
  36. light_phone_api-0.1.0/open_api_specification_client/api/default/post_api_device_tools.py +168 -0
  37. light_phone_api-0.1.0/open_api_specification_client/api/default/post_api_followed_podcasts.py +168 -0
  38. light_phone_api-0.1.0/open_api_specification_client/api/default/post_api_notes.py +168 -0
  39. light_phone_api-0.1.0/open_api_specification_client/api/default/post_api_playlists_sort_mode.py +168 -0
  40. light_phone_api-0.1.0/open_api_specification_client/api/default/post_api_podcasts.py +168 -0
  41. light_phone_api-0.1.0/open_api_specification_client/client.py +268 -0
  42. light_phone_api-0.1.0/open_api_specification_client/errors.py +16 -0
  43. light_phone_api-0.1.0/open_api_specification_client/models/__init__.py +719 -0
  44. light_phone_api-0.1.0/open_api_specification_client/models/delete_api_followed_podcasts_followed_podcast_id_response_200.py +117 -0
  45. light_phone_api-0.1.0/open_api_specification_client/models/delete_api_followed_podcasts_followed_podcast_id_response_200_data_item.py +105 -0
  46. light_phone_api-0.1.0/open_api_specification_client/models/delete_api_followed_podcasts_followed_podcast_id_response_200_data_item_attributes.py +69 -0
  47. light_phone_api-0.1.0/open_api_specification_client/models/delete_api_followed_podcasts_followed_podcast_id_response_200_data_item_relationships.py +89 -0
  48. light_phone_api-0.1.0/open_api_specification_client/models/delete_api_followed_podcasts_followed_podcast_id_response_200_data_item_relationships_device_tool.py +73 -0
  49. light_phone_api-0.1.0/open_api_specification_client/models/delete_api_followed_podcasts_followed_podcast_id_response_200_data_item_relationships_device_tool_data.py +69 -0
  50. light_phone_api-0.1.0/open_api_specification_client/models/delete_api_followed_podcasts_followed_podcast_id_response_200_data_item_relationships_podcast.py +73 -0
  51. light_phone_api-0.1.0/open_api_specification_client/models/delete_api_followed_podcasts_followed_podcast_id_response_200_data_item_relationships_podcast_data.py +69 -0
  52. light_phone_api-0.1.0/open_api_specification_client/models/delete_api_followed_podcasts_followed_podcast_id_response_200_included_item.py +115 -0
  53. light_phone_api-0.1.0/open_api_specification_client/models/delete_api_followed_podcasts_followed_podcast_id_response_200_included_item_attributes.py +134 -0
  54. light_phone_api-0.1.0/open_api_specification_client/models/delete_api_followed_podcasts_followed_podcast_id_response_200_included_item_attributes_config.py +47 -0
  55. light_phone_api-0.1.0/open_api_specification_client/models/delete_api_followed_podcasts_followed_podcast_id_response_200_included_item_relationships.py +89 -0
  56. light_phone_api-0.1.0/open_api_specification_client/models/delete_api_followed_podcasts_followed_podcast_id_response_200_included_item_relationships_device.py +73 -0
  57. light_phone_api-0.1.0/open_api_specification_client/models/delete_api_followed_podcasts_followed_podcast_id_response_200_included_item_relationships_device_data.py +69 -0
  58. light_phone_api-0.1.0/open_api_specification_client/models/delete_api_followed_podcasts_followed_podcast_id_response_200_included_item_relationships_tool.py +73 -0
  59. light_phone_api-0.1.0/open_api_specification_client/models/delete_api_followed_podcasts_followed_podcast_id_response_200_included_item_relationships_tool_data.py +69 -0
  60. light_phone_api-0.1.0/open_api_specification_client/models/delete_api_followed_podcasts_followed_podcast_id_response_200_jsonapi.py +61 -0
  61. light_phone_api-0.1.0/open_api_specification_client/models/get_api_audio_capacity_response_200.py +85 -0
  62. light_phone_api-0.1.0/open_api_specification_client/models/get_api_devices_response_200.py +103 -0
  63. light_phone_api-0.1.0/open_api_specification_client/models/get_api_devices_response_200_data_item.py +99 -0
  64. light_phone_api-0.1.0/open_api_specification_client/models/get_api_devices_response_200_data_item_attributes.py +85 -0
  65. light_phone_api-0.1.0/open_api_specification_client/models/get_api_devices_response_200_data_item_relationships.py +115 -0
  66. light_phone_api-0.1.0/open_api_specification_client/models/get_api_devices_response_200_data_item_relationships_device_tool_location.py +71 -0
  67. light_phone_api-0.1.0/open_api_specification_client/models/get_api_devices_response_200_data_item_relationships_device_tool_location_data.py +69 -0
  68. light_phone_api-0.1.0/open_api_specification_client/models/get_api_devices_response_200_data_item_relationships_device_tools.py +79 -0
  69. light_phone_api-0.1.0/open_api_specification_client/models/get_api_devices_response_200_data_item_relationships_device_tools_data_item.py +69 -0
  70. light_phone_api-0.1.0/open_api_specification_client/models/get_api_devices_response_200_data_item_relationships_sim.py +71 -0
  71. light_phone_api-0.1.0/open_api_specification_client/models/get_api_devices_response_200_data_item_relationships_sim_data.py +69 -0
  72. light_phone_api-0.1.0/open_api_specification_client/models/get_api_devices_response_200_data_item_relationships_user.py +71 -0
  73. light_phone_api-0.1.0/open_api_specification_client/models/get_api_devices_response_200_data_item_relationships_user_data.py +69 -0
  74. light_phone_api-0.1.0/open_api_specification_client/models/get_api_devices_response_200_included_item.py +101 -0
  75. light_phone_api-0.1.0/open_api_specification_client/models/get_api_devices_response_200_included_item_attributes.py +132 -0
  76. light_phone_api-0.1.0/open_api_specification_client/models/get_api_devices_response_200_included_item_attributes_config.py +114 -0
  77. light_phone_api-0.1.0/open_api_specification_client/models/get_api_devices_response_200_included_item_attributes_config_address.py +117 -0
  78. light_phone_api-0.1.0/open_api_specification_client/models/get_api_devices_response_200_included_item_relationships.py +117 -0
  79. light_phone_api-0.1.0/open_api_specification_client/models/get_api_devices_response_200_included_item_relationships_device.py +71 -0
  80. light_phone_api-0.1.0/open_api_specification_client/models/get_api_devices_response_200_included_item_relationships_device_data.py +69 -0
  81. light_phone_api-0.1.0/open_api_specification_client/models/get_api_devices_response_200_included_item_relationships_tool.py +71 -0
  82. light_phone_api-0.1.0/open_api_specification_client/models/get_api_devices_response_200_included_item_relationships_tool_data.py +69 -0
  83. light_phone_api-0.1.0/open_api_specification_client/models/get_api_devices_response_200_included_item_relationships_user.py +71 -0
  84. light_phone_api-0.1.0/open_api_specification_client/models/get_api_devices_response_200_included_item_relationships_user_data.py +69 -0
  85. light_phone_api-0.1.0/open_api_specification_client/models/get_api_devices_response_200_jsonapi.py +61 -0
  86. light_phone_api-0.1.0/open_api_specification_client/models/get_api_followed_podcasts_response_200.py +107 -0
  87. light_phone_api-0.1.0/open_api_specification_client/models/get_api_followed_podcasts_response_200_data_item.py +101 -0
  88. light_phone_api-0.1.0/open_api_specification_client/models/get_api_followed_podcasts_response_200_data_item_attributes.py +69 -0
  89. light_phone_api-0.1.0/open_api_specification_client/models/get_api_followed_podcasts_response_200_data_item_relationships.py +85 -0
  90. light_phone_api-0.1.0/open_api_specification_client/models/get_api_followed_podcasts_response_200_data_item_relationships_device_tool.py +71 -0
  91. light_phone_api-0.1.0/open_api_specification_client/models/get_api_followed_podcasts_response_200_data_item_relationships_device_tool_data.py +69 -0
  92. light_phone_api-0.1.0/open_api_specification_client/models/get_api_followed_podcasts_response_200_data_item_relationships_podcast.py +71 -0
  93. light_phone_api-0.1.0/open_api_specification_client/models/get_api_followed_podcasts_response_200_data_item_relationships_podcast_data.py +69 -0
  94. light_phone_api-0.1.0/open_api_specification_client/models/get_api_followed_podcasts_response_200_included_item.py +111 -0
  95. light_phone_api-0.1.0/open_api_specification_client/models/get_api_followed_podcasts_response_200_included_item_attributes.py +155 -0
  96. light_phone_api-0.1.0/open_api_specification_client/models/get_api_followed_podcasts_response_200_included_item_attributes_config.py +47 -0
  97. light_phone_api-0.1.0/open_api_specification_client/models/get_api_followed_podcasts_response_200_included_item_relationships.py +85 -0
  98. light_phone_api-0.1.0/open_api_specification_client/models/get_api_followed_podcasts_response_200_included_item_relationships_device.py +71 -0
  99. light_phone_api-0.1.0/open_api_specification_client/models/get_api_followed_podcasts_response_200_included_item_relationships_device_data.py +69 -0
  100. light_phone_api-0.1.0/open_api_specification_client/models/get_api_followed_podcasts_response_200_included_item_relationships_tool.py +71 -0
  101. light_phone_api-0.1.0/open_api_specification_client/models/get_api_followed_podcasts_response_200_included_item_relationships_tool_data.py +69 -0
  102. light_phone_api-0.1.0/open_api_specification_client/models/get_api_followed_podcasts_response_200_jsonapi.py +61 -0
  103. light_phone_api-0.1.0/open_api_specification_client/models/get_api_notes_note_id_generate_presigned_get_url_response_200.py +61 -0
  104. light_phone_api-0.1.0/open_api_specification_client/models/get_api_notes_note_id_generate_presigned_put_url_response_200.py +61 -0
  105. light_phone_api-0.1.0/open_api_specification_client/models/get_api_notes_note_id_response_200.py +97 -0
  106. light_phone_api-0.1.0/open_api_specification_client/models/get_api_notes_note_id_response_200_data.py +94 -0
  107. light_phone_api-0.1.0/open_api_specification_client/models/get_api_notes_note_id_response_200_data_attributes.py +88 -0
  108. light_phone_api-0.1.0/open_api_specification_client/models/get_api_notes_note_id_response_200_included_item.py +96 -0
  109. light_phone_api-0.1.0/open_api_specification_client/models/get_api_notes_note_id_response_200_included_item_attributes.py +61 -0
  110. light_phone_api-0.1.0/open_api_specification_client/models/get_api_notes_response_200.py +110 -0
  111. light_phone_api-0.1.0/open_api_specification_client/models/get_api_notes_response_200_data_item.py +95 -0
  112. light_phone_api-0.1.0/open_api_specification_client/models/get_api_notes_response_200_data_item_attributes.py +93 -0
  113. light_phone_api-0.1.0/open_api_specification_client/models/get_api_notes_response_200_data_item_relationships.py +71 -0
  114. light_phone_api-0.1.0/open_api_specification_client/models/get_api_notes_response_200_data_item_relationships_file.py +71 -0
  115. light_phone_api-0.1.0/open_api_specification_client/models/get_api_notes_response_200_data_item_relationships_file_data.py +69 -0
  116. light_phone_api-0.1.0/open_api_specification_client/models/get_api_notes_response_200_included_item.py +87 -0
  117. light_phone_api-0.1.0/open_api_specification_client/models/get_api_notes_response_200_included_item_attributes.py +113 -0
  118. light_phone_api-0.1.0/open_api_specification_client/models/get_api_notes_response_200_jsonapi.py +61 -0
  119. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlist_items_response_200.py +112 -0
  120. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlist_items_response_200_data_item.py +101 -0
  121. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlist_items_response_200_data_item_attributes.py +77 -0
  122. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlist_items_response_200_data_item_relationships.py +71 -0
  123. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlist_items_response_200_data_item_relationships_audio.py +71 -0
  124. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlist_items_response_200_data_item_relationships_audio_data.py +69 -0
  125. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlist_items_response_200_included_item.py +111 -0
  126. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlist_items_response_200_included_item_attributes.py +153 -0
  127. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlist_items_response_200_included_item_relationships.py +87 -0
  128. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlist_items_response_200_included_item_relationships_processed_file.py +71 -0
  129. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlist_items_response_200_included_item_relationships_processed_file_data.py +69 -0
  130. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlist_items_response_200_included_item_relationships_raw_file.py +71 -0
  131. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlist_items_response_200_included_item_relationships_raw_file_data.py +69 -0
  132. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlist_items_response_200_jsonapi.py +61 -0
  133. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlists_response_200.py +103 -0
  134. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlists_response_200_data_item.py +101 -0
  135. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlists_response_200_data_item_attributes.py +101 -0
  136. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlists_response_200_data_item_relationships.py +99 -0
  137. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlists_response_200_data_item_relationships_device_tool.py +71 -0
  138. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlists_response_200_data_item_relationships_device_tool_data.py +69 -0
  139. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlists_response_200_data_item_relationships_playlist_items.py +71 -0
  140. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlists_response_200_data_item_relationships_playlist_items_links.py +61 -0
  141. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlists_response_200_data_item_relationships_user.py +71 -0
  142. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlists_response_200_data_item_relationships_user_data.py +69 -0
  143. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlists_response_200_included_item.py +111 -0
  144. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlists_response_200_included_item_attributes.py +134 -0
  145. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlists_response_200_included_item_attributes_config.py +47 -0
  146. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlists_response_200_included_item_relationships.py +85 -0
  147. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlists_response_200_included_item_relationships_device.py +71 -0
  148. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlists_response_200_included_item_relationships_device_data.py +69 -0
  149. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlists_response_200_included_item_relationships_tool.py +71 -0
  150. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlists_response_200_included_item_relationships_tool_data.py +69 -0
  151. light_phone_api-0.1.0/open_api_specification_client/models/get_api_playlists_response_200_jsonapi.py +61 -0
  152. light_phone_api-0.1.0/open_api_specification_client/models/get_api_podcasts_response_200.py +85 -0
  153. light_phone_api-0.1.0/open_api_specification_client/models/get_api_podcasts_response_200_data_item.py +85 -0
  154. light_phone_api-0.1.0/open_api_specification_client/models/get_api_podcasts_response_200_data_item_attributes.py +99 -0
  155. light_phone_api-0.1.0/open_api_specification_client/models/get_api_podcasts_response_200_jsonapi.py +61 -0
  156. light_phone_api-0.1.0/open_api_specification_client/models/get_api_tools_response_200.py +85 -0
  157. light_phone_api-0.1.0/open_api_specification_client/models/get_api_tools_response_200_data_item.py +83 -0
  158. light_phone_api-0.1.0/open_api_specification_client/models/get_api_tools_response_200_data_item_attributes.py +93 -0
  159. light_phone_api-0.1.0/open_api_specification_client/models/get_api_tools_response_200_jsonapi.py +61 -0
  160. light_phone_api-0.1.0/open_api_specification_client/models/get_api_tools_tool_id_response_200.py +77 -0
  161. light_phone_api-0.1.0/open_api_specification_client/models/get_api_tools_tool_id_response_200_data.py +85 -0
  162. light_phone_api-0.1.0/open_api_specification_client/models/get_api_tools_tool_id_response_200_data_attributes.py +93 -0
  163. light_phone_api-0.1.0/open_api_specification_client/models/get_api_tools_tool_id_response_200_jsonapi.py +61 -0
  164. light_phone_api-0.1.0/open_api_specification_client/models/get_api_users_current_response_200.py +77 -0
  165. light_phone_api-0.1.0/open_api_specification_client/models/get_api_users_current_response_200_data.py +99 -0
  166. light_phone_api-0.1.0/open_api_specification_client/models/get_api_users_current_response_200_data_attributes.py +93 -0
  167. light_phone_api-0.1.0/open_api_specification_client/models/get_api_users_current_response_200_data_relationships.py +85 -0
  168. light_phone_api-0.1.0/open_api_specification_client/models/get_api_users_current_response_200_data_relationships_affiliate.py +61 -0
  169. light_phone_api-0.1.0/open_api_specification_client/models/get_api_users_current_response_200_data_relationships_subscriptions.py +61 -0
  170. light_phone_api-0.1.0/open_api_specification_client/models/get_api_users_current_response_200_jsonapi.py +61 -0
  171. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_audios_audio_id_body.py +67 -0
  172. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_audios_audio_id_body_data.py +85 -0
  173. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_audios_audio_id_body_data_attributes.py +70 -0
  174. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_audios_audio_id_body_data_type.py +8 -0
  175. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_audios_audio_id_response_200.py +99 -0
  176. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_audios_audio_id_response_200_data.py +101 -0
  177. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_audios_audio_id_response_200_data_attributes.py +85 -0
  178. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_audios_audio_id_response_200_data_relationships.py +87 -0
  179. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_audios_audio_id_response_200_data_relationships_processed_file.py +71 -0
  180. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_audios_audio_id_response_200_data_relationships_processed_file_data.py +69 -0
  181. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_audios_audio_id_response_200_data_relationships_raw_file.py +71 -0
  182. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_audios_audio_id_response_200_data_relationships_raw_file_data.py +69 -0
  183. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_audios_audio_id_response_200_included_item.py +87 -0
  184. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_audios_audio_id_response_200_included_item_attributes.py +101 -0
  185. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_audios_audio_id_response_200_jsonapi.py +61 -0
  186. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_notes_note_id_body.py +74 -0
  187. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_notes_note_id_body_data.py +110 -0
  188. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_notes_note_id_body_data_attributes.py +79 -0
  189. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_notes_note_id_body_data_relationships.py +78 -0
  190. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_notes_note_id_body_data_relationships_file.py +78 -0
  191. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_notes_note_id_body_data_relationships_file_data.py +70 -0
  192. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_notes_note_id_response_200.py +74 -0
  193. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_notes_note_id_response_200_data.py +96 -0
  194. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_notes_note_id_response_200_data_attributes.py +79 -0
  195. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_playlist_items_playlist_item_id_body.py +69 -0
  196. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_playlist_items_playlist_item_id_body_data.py +91 -0
  197. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_playlist_items_playlist_item_id_body_data_attributes.py +61 -0
  198. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_playlist_items_playlist_item_id_body_data_type.py +8 -0
  199. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_playlist_items_playlist_item_id_response_200.py +85 -0
  200. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_playlist_items_playlist_item_id_response_200_data.py +103 -0
  201. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_playlist_items_playlist_item_id_response_200_data_attributes.py +85 -0
  202. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_playlist_items_playlist_item_id_response_200_data_relationships.py +87 -0
  203. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_playlist_items_playlist_item_id_response_200_data_relationships_processed_file.py +61 -0
  204. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_playlist_items_playlist_item_id_response_200_data_relationships_raw_file.py +61 -0
  205. light_phone_api-0.1.0/open_api_specification_client/models/patch_api_playlist_items_playlist_item_id_response_200_jsonapi.py +61 -0
  206. light_phone_api-0.1.0/open_api_specification_client/models/post_api_audios_body.py +67 -0
  207. light_phone_api-0.1.0/open_api_specification_client/models/post_api_audios_body_data.py +77 -0
  208. light_phone_api-0.1.0/open_api_specification_client/models/post_api_audios_body_data_attributes.py +69 -0
  209. light_phone_api-0.1.0/open_api_specification_client/models/post_api_audios_body_data_type.py +8 -0
  210. light_phone_api-0.1.0/open_api_specification_client/models/post_api_audios_delete_all_body.py +61 -0
  211. light_phone_api-0.1.0/open_api_specification_client/models/post_api_audios_response_201.py +95 -0
  212. light_phone_api-0.1.0/open_api_specification_client/models/post_api_audios_response_201_data.py +83 -0
  213. light_phone_api-0.1.0/open_api_specification_client/models/post_api_audios_response_201_data_relationships.py +85 -0
  214. light_phone_api-0.1.0/open_api_specification_client/models/post_api_audios_response_201_data_relationships_processed_file.py +61 -0
  215. light_phone_api-0.1.0/open_api_specification_client/models/post_api_audios_response_201_data_relationships_raw_file.py +71 -0
  216. light_phone_api-0.1.0/open_api_specification_client/models/post_api_audios_response_201_data_relationships_raw_file_data.py +69 -0
  217. light_phone_api-0.1.0/open_api_specification_client/models/post_api_audios_response_201_included_item.py +87 -0
  218. light_phone_api-0.1.0/open_api_specification_client/models/post_api_audios_response_201_included_item_attributes.py +101 -0
  219. light_phone_api-0.1.0/open_api_specification_client/models/post_api_audios_response_201_jsonapi.py +61 -0
  220. light_phone_api-0.1.0/open_api_specification_client/models/post_api_authorizations_body.py +69 -0
  221. light_phone_api-0.1.0/open_api_specification_client/models/post_api_authorizations_response_200.py +99 -0
  222. light_phone_api-0.1.0/open_api_specification_client/models/post_api_authorizations_response_200_data.py +96 -0
  223. light_phone_api-0.1.0/open_api_specification_client/models/post_api_authorizations_response_200_data_attributes.py +79 -0
  224. light_phone_api-0.1.0/open_api_specification_client/models/post_api_authorizations_response_200_included_item.py +96 -0
  225. light_phone_api-0.1.0/open_api_specification_client/models/post_api_authorizations_response_200_included_item_attributes.py +70 -0
  226. light_phone_api-0.1.0/open_api_specification_client/models/post_api_device_tools_body.py +67 -0
  227. light_phone_api-0.1.0/open_api_specification_client/models/post_api_device_tools_body_data.py +77 -0
  228. light_phone_api-0.1.0/open_api_specification_client/models/post_api_device_tools_body_data_attributes.py +80 -0
  229. light_phone_api-0.1.0/open_api_specification_client/models/post_api_device_tools_body_data_type.py +8 -0
  230. light_phone_api-0.1.0/open_api_specification_client/models/post_api_device_tools_response_201.py +87 -0
  231. light_phone_api-0.1.0/open_api_specification_client/models/post_api_device_tools_response_201_data.py +117 -0
  232. light_phone_api-0.1.0/open_api_specification_client/models/post_api_device_tools_response_201_data_attributes.py +98 -0
  233. light_phone_api-0.1.0/open_api_specification_client/models/post_api_device_tools_response_201_data_attributes_config.py +47 -0
  234. light_phone_api-0.1.0/open_api_specification_client/models/post_api_device_tools_response_201_data_relationships.py +100 -0
  235. light_phone_api-0.1.0/open_api_specification_client/models/post_api_device_tools_response_201_data_relationships_device.py +78 -0
  236. light_phone_api-0.1.0/open_api_specification_client/models/post_api_device_tools_response_201_data_relationships_device_data.py +69 -0
  237. light_phone_api-0.1.0/open_api_specification_client/models/post_api_device_tools_response_201_data_relationships_tool.py +78 -0
  238. light_phone_api-0.1.0/open_api_specification_client/models/post_api_device_tools_response_201_data_relationships_tool_data.py +69 -0
  239. light_phone_api-0.1.0/open_api_specification_client/models/post_api_device_tools_response_201_jsonapi.py +61 -0
  240. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_body.py +67 -0
  241. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_body_data.py +79 -0
  242. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_body_data_relationships.py +85 -0
  243. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_body_data_relationships_device_tool.py +71 -0
  244. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_body_data_relationships_device_tool_data.py +73 -0
  245. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_body_data_relationships_device_tool_data_type.py +8 -0
  246. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_body_data_relationships_podcast.py +71 -0
  247. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_body_data_relationships_podcast_data.py +73 -0
  248. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_body_data_relationships_podcast_data_type.py +8 -0
  249. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_body_data_type.py +8 -0
  250. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_response_201.py +99 -0
  251. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_response_201_data.py +101 -0
  252. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_response_201_data_attributes.py +69 -0
  253. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_response_201_data_relationships.py +85 -0
  254. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_response_201_data_relationships_device_tool.py +71 -0
  255. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_response_201_data_relationships_device_tool_data.py +69 -0
  256. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_response_201_data_relationships_podcast.py +71 -0
  257. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_response_201_data_relationships_podcast_data.py +69 -0
  258. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_response_201_included_item.py +111 -0
  259. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_response_201_included_item_attributes.py +155 -0
  260. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_response_201_included_item_attributes_config.py +47 -0
  261. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_response_201_included_item_relationships.py +85 -0
  262. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_response_201_included_item_relationships_device.py +71 -0
  263. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_response_201_included_item_relationships_device_data.py +69 -0
  264. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_response_201_included_item_relationships_tool.py +71 -0
  265. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_response_201_included_item_relationships_tool_data.py +69 -0
  266. light_phone_api-0.1.0/open_api_specification_client/models/post_api_followed_podcasts_response_201_jsonapi.py +61 -0
  267. light_phone_api-0.1.0/open_api_specification_client/models/post_api_notes_body.py +67 -0
  268. light_phone_api-0.1.0/open_api_specification_client/models/post_api_notes_body_data.py +77 -0
  269. light_phone_api-0.1.0/open_api_specification_client/models/post_api_notes_body_data_attributes.py +87 -0
  270. light_phone_api-0.1.0/open_api_specification_client/models/post_api_notes_body_data_attributes_note_type.py +9 -0
  271. light_phone_api-0.1.0/open_api_specification_client/models/post_api_notes_body_data_type.py +8 -0
  272. light_phone_api-0.1.0/open_api_specification_client/models/post_api_notes_response_201.py +95 -0
  273. light_phone_api-0.1.0/open_api_specification_client/models/post_api_notes_response_201_data.py +93 -0
  274. light_phone_api-0.1.0/open_api_specification_client/models/post_api_notes_response_201_data_attributes.py +93 -0
  275. light_phone_api-0.1.0/open_api_specification_client/models/post_api_notes_response_201_data_relationships.py +71 -0
  276. light_phone_api-0.1.0/open_api_specification_client/models/post_api_notes_response_201_data_relationships_file.py +71 -0
  277. light_phone_api-0.1.0/open_api_specification_client/models/post_api_notes_response_201_data_relationships_file_data.py +69 -0
  278. light_phone_api-0.1.0/open_api_specification_client/models/post_api_notes_response_201_included_item.py +87 -0
  279. light_phone_api-0.1.0/open_api_specification_client/models/post_api_notes_response_201_included_item_attributes.py +101 -0
  280. light_phone_api-0.1.0/open_api_specification_client/models/post_api_notes_response_201_jsonapi.py +61 -0
  281. light_phone_api-0.1.0/open_api_specification_client/models/post_api_playlists_sort_mode_body.py +80 -0
  282. light_phone_api-0.1.0/open_api_specification_client/models/post_api_playlists_sort_mode_body_sort_mode.py +10 -0
  283. light_phone_api-0.1.0/open_api_specification_client/models/post_api_playlists_sort_mode_response_400.py +67 -0
  284. light_phone_api-0.1.0/open_api_specification_client/models/post_api_playlists_sort_mode_response_400_errors.py +61 -0
  285. light_phone_api-0.1.0/open_api_specification_client/models/post_api_podcasts_body.py +67 -0
  286. light_phone_api-0.1.0/open_api_specification_client/models/post_api_podcasts_body_data.py +77 -0
  287. light_phone_api-0.1.0/open_api_specification_client/models/post_api_podcasts_body_data_attributes.py +143 -0
  288. light_phone_api-0.1.0/open_api_specification_client/models/post_api_podcasts_body_data_type.py +8 -0
  289. light_phone_api-0.1.0/open_api_specification_client/models/post_api_podcasts_response_201.py +77 -0
  290. light_phone_api-0.1.0/open_api_specification_client/models/post_api_podcasts_response_201_data.py +83 -0
  291. light_phone_api-0.1.0/open_api_specification_client/models/post_api_podcasts_response_201_data_attributes.py +93 -0
  292. light_phone_api-0.1.0/open_api_specification_client/models/post_api_podcasts_response_201_jsonapi.py +61 -0
  293. light_phone_api-0.1.0/open_api_specification_client/py.typed +1 -0
  294. light_phone_api-0.1.0/open_api_specification_client/types.py +54 -0
  295. light_phone_api-0.1.0/openapi-spec.json +4240 -0
  296. light_phone_api-0.1.0/pyproject.toml +21 -0
  297. light_phone_api-0.1.0/test/test_music.py +17 -0
@@ -0,0 +1,7 @@
1
+ __pycache__
2
+ *.pyc
3
+ _build
4
+ .env
5
+ .venv
6
+ tests/fixtures/
7
+ .coverage
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: light-phone-api
3
+ Version: 0.1.0
4
+ Summary: Unofficial Python API for the Light Phone
5
+ Author-email: Alexis Garado <alexisgarado@proton.me>
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: attrs>=22.2.0
8
+ Requires-Dist: httpx<0.29.0,>=0.23.0
9
+ Requires-Dist: keyring
10
+ Requires-Dist: mutagen
11
+ Requires-Dist: python-dateutil>=2.8.0
File without changes
@@ -0,0 +1,11 @@
1
+ import functools
2
+ from .client import Light
3
+
4
+
5
+ def with_light(f):
6
+ @functools.wraps(f)
7
+ def wrapper(*args, **kwargs):
8
+ with Light() as light:
9
+ return f(light, *args, **kwargs)
10
+
11
+ return wrapper
@@ -0,0 +1,273 @@
1
+ from __future__ import annotations
2
+
3
+ import httpx
4
+ import json
5
+ import keyring
6
+ import logging
7
+ import os
8
+
9
+ from typing import TYPE_CHECKING, Any, Callable, final
10
+
11
+ from open_api_specification_client.api.default import get_api_playlists
12
+ from open_api_specification_client.client import AuthenticatedClient
13
+
14
+ if TYPE_CHECKING:
15
+ from light_api.music import LightMusic
16
+ from light_api.podcast import LightPodcasts
17
+ from light_api.notes import LightNotes
18
+ from light_api.tools import LightTools
19
+
20
+ KEYRING_SERVICE = "unofficial-light-api"
21
+ KEYRING_USER = "session"
22
+ API_BASE = "https://production.lightphonecloud.com"
23
+ API_HEADERS = {"Accept": "application/vnd.api+json"}
24
+
25
+ log = logging.getLogger(f"light.{__name__}")
26
+
27
+
28
+ @final
29
+ class Light:
30
+ """Methods for interfacing with Light devices."""
31
+
32
+ def __init__(
33
+ self,
34
+ email: str | None = None,
35
+ email_file: str | None = None,
36
+ password: str | None = None,
37
+ password_file: str | None = None,
38
+ phone: str | None = None,
39
+ phone_file: str | None = None,
40
+ ) -> None:
41
+ self.email: str | None = email or self._resolve(email_file, "LIGHT_EMAIL")
42
+ self.password: str | None = password or self._resolve(
43
+ password_file, "LIGHT_PASSWORD"
44
+ )
45
+ self.phone: str | None = phone or self._resolve(
46
+ phone_file, "LIGHT_PHONE_NUMBER"
47
+ )
48
+ self._api_token: str | None = None
49
+ self._api_client: AuthenticatedClient | None = None
50
+ self._device_tool_ids: dict[str, str] = {}
51
+ self._playlist_id: str | None = None
52
+
53
+ self.music: LightMusic
54
+ self.podcast: LightPodcasts
55
+ self.notes: LightNotes
56
+ self.tools: LightTools
57
+
58
+ def login(self) -> None:
59
+ """Authenticate via the authorizations API and store the bearer token."""
60
+ if self._api_token is not None:
61
+ return
62
+
63
+ if not self.email or not self.password:
64
+ raise RuntimeError("No cached session - provide email and password")
65
+
66
+ resp = httpx.post(
67
+ f"{API_BASE}/api/authorizations",
68
+ json={"email": self.email, "password": self.password},
69
+ headers={**API_HEADERS, "Content-Type": "application/vnd.api+json"},
70
+ timeout=30,
71
+ )
72
+
73
+ if not resp.is_success:
74
+ raise RuntimeError(f"Login failed: {resp.status_code}")
75
+
76
+ included = resp.json()["included"][0]
77
+ token = included["attributes"]["token"]
78
+
79
+ if token is None:
80
+ raise RuntimeError("Login succeeded but no token found in response")
81
+
82
+ self._api_token = token
83
+
84
+ def reauth(self) -> None:
85
+ """Re-authenticate and refresh the API client. Called on 401."""
86
+ log.info("Re-authenticating")
87
+ self._api_token = None
88
+ self.login()
89
+ self._save_cache()
90
+ self._api_client = AuthenticatedClient(
91
+ base_url=API_BASE,
92
+ token=self._api_token,
93
+ headers=API_HEADERS,
94
+ )
95
+
96
+ def call_api(self, func: Callable[..., Any], **kwargs: Any) -> Any:
97
+ """Call an API function, re-authenticating once on 401."""
98
+ resp = func(**kwargs)
99
+ if resp.status_code == 401:
100
+ self.reauth()
101
+ resp = func(**kwargs)
102
+ return resp
103
+
104
+ def __enter__(self) -> Light:
105
+ """Sets up API session."""
106
+ log.info("Authenticating")
107
+
108
+ if self._load_cache() and self._validate_cache():
109
+ log.info("Using cached session")
110
+ else:
111
+ self.login()
112
+ self._save_cache()
113
+
114
+ self._api_client = AuthenticatedClient(
115
+ base_url=API_BASE,
116
+ token=self._api_token,
117
+ headers=API_HEADERS,
118
+ )
119
+
120
+ expected = {"music", "notes", "podcast"}
121
+ if not expected.issubset(self._device_tool_ids) or not self._playlist_id:
122
+ self._fetch_device_tool_ids()
123
+ self._fetch_playlist_id()
124
+ self._save_cache()
125
+
126
+ from light_api.music import LightMusic
127
+ from light_api.podcast import LightPodcasts
128
+ from light_api.notes import LightNotes
129
+ from light_api.tools import LightTools
130
+
131
+ self.music = LightMusic(self)
132
+ self.podcast = LightPodcasts(self)
133
+ self.notes = LightNotes(self)
134
+ self.tools = LightTools(self)
135
+
136
+ log.info("Authentication complete")
137
+ return self
138
+
139
+ def __exit__(self, *_: object) -> None:
140
+ pass
141
+
142
+ def _load_cache(self) -> bool:
143
+ """Load cached data from keyring."""
144
+ log.debug("Loading cache")
145
+
146
+ try:
147
+ raw = keyring.get_password(KEYRING_SERVICE, KEYRING_USER)
148
+ except (keyring.errors.NoKeyringError, keyring.errors.KeyringLocked) as e:
149
+ log.debug(f"Keyring error: {e}")
150
+ return False
151
+
152
+ if raw is None:
153
+ log.debug("Keyring: no entry found")
154
+ return False
155
+
156
+ try:
157
+ data = json.loads(raw)
158
+ self._api_token = data["api_token"]
159
+ self._device_tool_ids = data["device_tool_ids"]
160
+ self._playlist_id = data["playlist_id"]
161
+ log.debug("Cache loaded successfully")
162
+ return True
163
+ except (KeyError, json.JSONDecodeError) as e:
164
+ log.debug(f"Error: {e}")
165
+ return False
166
+
167
+ def _save_cache(self) -> None:
168
+ """Cache data to keyring."""
169
+ try:
170
+ keyring.set_password(
171
+ KEYRING_SERVICE,
172
+ KEYRING_USER,
173
+ json.dumps(
174
+ {
175
+ "api_token": self._api_token,
176
+ "device_tool_ids": self._device_tool_ids,
177
+ "playlist_id": self._playlist_id,
178
+ }
179
+ ),
180
+ )
181
+ except (keyring.errors.NoKeyringError, keyring.errors.KeyringLocked) as e:
182
+ log.warning(f"Keyring error: {e}")
183
+
184
+ def _validate_cache(self) -> bool:
185
+ """Check if cached auth token is valid."""
186
+ client = AuthenticatedClient(
187
+ base_url=API_BASE,
188
+ token=self._api_token,
189
+ headers=API_HEADERS,
190
+ )
191
+ resp = get_api_playlists.sync_detailed(
192
+ client=client,
193
+ device_tool_id=self._device_tool_ids.get("music"),
194
+ )
195
+ return resp.status_code == 200
196
+
197
+ def _fetch_playlist_id(self) -> None:
198
+ music_id = self._device_tool_ids.get("music")
199
+ if not music_id:
200
+ raise RuntimeError("Could not find music device_tool_id in /api/devices")
201
+ resp = get_api_playlists.sync_detailed(
202
+ client=self._api_client,
203
+ device_tool_id=music_id,
204
+ )
205
+ if resp.status_code != 200 or not resp.parsed or not resp.parsed.data:
206
+ raise RuntimeError(f"Could not fetch playlists: {resp.status_code}")
207
+ self._playlist_id = resp.parsed.data[0].id
208
+
209
+ def _fetch_device_tool_ids(self) -> None:
210
+ """Populate _device_tool_ids for all installed tools.
211
+
212
+ The Light API has two relevant endpoints:
213
+ - GET /api/devices: returns the device record plus "included" items which are
214
+ device_tool records (one per installed tool). Each has a unique `id` (the
215
+ device_tool_id used in subsequent API calls) and a relationship pointing to
216
+ its global tool ID.
217
+ - GET /api/tools?device_id=...: returns the global tool catalog with human-readable
218
+ namespace strings (e.g. "com.light.music", "com.light.notes").
219
+
220
+ To get each device tool id, cross-reference the two: build a map of global_tool_id -> namespace
221
+ from /api/tools, then walk the /api/devices included items, look up each item's global tool ID in
222
+ that map, and classify it as "music", "notes", or "podcast" based on the namespace string.
223
+ """
224
+ from open_api_specification_client.api.default import (
225
+ get_api_devices,
226
+ get_api_tools,
227
+ )
228
+ from open_api_specification_client.types import Unset
229
+
230
+ devices_resp = get_api_devices.sync_detailed(client=self._api_client)
231
+ if (
232
+ devices_resp.status_code != 200
233
+ or not devices_resp.parsed
234
+ or not devices_resp.parsed.data
235
+ ):
236
+ raise RuntimeError(f"Could not fetch devices: {devices_resp.status_code}")
237
+ device_id = devices_resp.parsed.data[0].id
238
+
239
+ tools_resp = get_api_tools.sync_detailed(
240
+ client=self._api_client, device_id=device_id
241
+ )
242
+ if tools_resp.status_code != 200 or not tools_resp.parsed:
243
+ raise RuntimeError(f"Could not fetch tools: {tools_resp.status_code}")
244
+
245
+ tool_ns: dict[str, str] = {
246
+ t.id: t.attributes.namespace.lower() for t in tools_resp.parsed.data
247
+ }
248
+
249
+ for item in devices_resp.parsed.included:
250
+ if isinstance(item.relationships.tool, Unset):
251
+ continue
252
+ ns = tool_ns.get(item.relationships.tool.data.id, "")
253
+ if "note" in ns:
254
+ self._device_tool_ids["notes"] = item.id
255
+ elif "podcast" in ns:
256
+ self._device_tool_ids["podcast"] = item.id
257
+ elif "music" in ns or "playlist" in ns:
258
+ self._device_tool_ids["music"] = item.id
259
+
260
+ @staticmethod
261
+ def _resolve(filepath: str | None, env_key: str) -> str | None:
262
+ if filepath:
263
+ try:
264
+ with open(filepath) as f:
265
+ return f.read().strip()
266
+ except OSError as e:
267
+ raise RuntimeError(f"Could not read {filepath}: {e}") from e
268
+ return os.environ.get(env_key)
269
+
270
+ @staticmethod
271
+ def _format_phone(number: str) -> str:
272
+ digits: str = "".join(c for c in number if c.isdigit())[-10:]
273
+ return f"+1 {digits[0:3]} {digits[3:6]} {digits[6:10]}"