kestrel-app 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 (272) hide show
  1. kestrel_app-0.1.0/.env.example +53 -0
  2. kestrel_app-0.1.0/LICENSE +21 -0
  3. kestrel_app-0.1.0/MANIFEST.in +5 -0
  4. kestrel_app-0.1.0/PKG-INFO +28 -0
  5. kestrel_app-0.1.0/README.md +108 -0
  6. kestrel_app-0.1.0/alembic/env.py +68 -0
  7. kestrel_app-0.1.0/alembic/script.py.mako +28 -0
  8. kestrel_app-0.1.0/alembic/versions/07cca291f71f_add_saved_searches_table.py +47 -0
  9. kestrel_app-0.1.0/alembic/versions/775c5d68cc2d_add_discovery_models_discovered_jobs_.py +114 -0
  10. kestrel_app-0.1.0/alembic/versions/a1b2c3d4e5f6_add_job_requirements_table.py +44 -0
  11. kestrel_app-0.1.0/alembic/versions/a7b8c9d0e1f2_add_scoring_breakdown_market_schedule.py +63 -0
  12. kestrel_app-0.1.0/alembic/versions/b192ca99adc9_add_gap_id_and_started_at_to_learning_.py +36 -0
  13. kestrel_app-0.1.0/alembic/versions/b3c4d5e6f7a8_add_ondelete_set_null_to_discovery_run_fk.py +66 -0
  14. kestrel_app-0.1.0/alembic/versions/c0b4dc9ab23a_initial_schema_profiles_applications_.py +132 -0
  15. kestrel_app-0.1.0/alembic/versions/c4d5e6f7a8b9_add_interview_prep_tables.py +108 -0
  16. kestrel_app-0.1.0/alembic/versions/d5e6f7a8b9c0_add_star_stories_table.py +58 -0
  17. kestrel_app-0.1.0/alembic/versions/e6f7a8b9c0d1_add_company_research_reports_table.py +61 -0
  18. kestrel_app-0.1.0/alembic/versions/e730f1c7a51e_add_skills_intelligence_models.py +139 -0
  19. kestrel_app-0.1.0/alembic/versions/e8b54d7d5d6a_add_scoring_engine_models_scoring_.py +85 -0
  20. kestrel_app-0.1.0/alembic/versions/eac8c4fc464e_add_parent_event_id_to_calendar_events.py +37 -0
  21. kestrel_app-0.1.0/alembic/versions/f1a2b3c4d5e6_add_profile_id_to_dedup_constraint.py +46 -0
  22. kestrel_app-0.1.0/alembic/versions/f7a8b9c0d1e2_add_integration_configs_table.py +54 -0
  23. kestrel_app-0.1.0/alembic/versions/g8b9c0d1e2f3_add_ticktick_sync_tasks_table.py +64 -0
  24. kestrel_app-0.1.0/alembic/versions/h9c0d1e2f3g4_add_calendar_events_table.py +82 -0
  25. kestrel_app-0.1.0/alembic/versions/i0d1e2f3g4h5_add_time_sessions_table.py +50 -0
  26. kestrel_app-0.1.0/alembic/versions/j1e2f3g4h5i6_add_pushover_notification_tables.py +86 -0
  27. kestrel_app-0.1.0/alembic/versions/k2f3g4h5i6j7_add_voice_session_tables.py +57 -0
  28. kestrel_app-0.1.0/alembic.ini +151 -0
  29. kestrel_app-0.1.0/config/personal.yaml.example +26 -0
  30. kestrel_app-0.1.0/frontend/dist/assets/index-DH_vCftM.js +50 -0
  31. kestrel_app-0.1.0/frontend/dist/assets/index-Qt6RZzNz.css +2 -0
  32. kestrel_app-0.1.0/frontend/dist/index.html +13 -0
  33. kestrel_app-0.1.0/pyproject.toml +76 -0
  34. kestrel_app-0.1.0/setup.cfg +4 -0
  35. kestrel_app-0.1.0/src/career_os/__init__.py +3 -0
  36. kestrel_app-0.1.0/src/career_os/__main__.py +5 -0
  37. kestrel_app-0.1.0/src/career_os/_alembic/alembic/env.py +68 -0
  38. kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/07cca291f71f_add_saved_searches_table.py +47 -0
  39. kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/775c5d68cc2d_add_discovery_models_discovered_jobs_.py +114 -0
  40. kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/a1b2c3d4e5f6_add_job_requirements_table.py +44 -0
  41. kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/a7b8c9d0e1f2_add_scoring_breakdown_market_schedule.py +63 -0
  42. kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/b192ca99adc9_add_gap_id_and_started_at_to_learning_.py +36 -0
  43. kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/b3c4d5e6f7a8_add_ondelete_set_null_to_discovery_run_fk.py +66 -0
  44. kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/c0b4dc9ab23a_initial_schema_profiles_applications_.py +132 -0
  45. kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/c4d5e6f7a8b9_add_interview_prep_tables.py +108 -0
  46. kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/d5e6f7a8b9c0_add_star_stories_table.py +58 -0
  47. kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/e6f7a8b9c0d1_add_company_research_reports_table.py +61 -0
  48. kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/e730f1c7a51e_add_skills_intelligence_models.py +139 -0
  49. kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/e8b54d7d5d6a_add_scoring_engine_models_scoring_.py +85 -0
  50. kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/eac8c4fc464e_add_parent_event_id_to_calendar_events.py +37 -0
  51. kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/f1a2b3c4d5e6_add_profile_id_to_dedup_constraint.py +46 -0
  52. kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/f7a8b9c0d1e2_add_integration_configs_table.py +54 -0
  53. kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/g8b9c0d1e2f3_add_ticktick_sync_tasks_table.py +64 -0
  54. kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/h9c0d1e2f3g4_add_calendar_events_table.py +82 -0
  55. kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/i0d1e2f3g4h5_add_time_sessions_table.py +50 -0
  56. kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/j1e2f3g4h5i6_add_pushover_notification_tables.py +86 -0
  57. kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/k2f3g4h5i6j7_add_voice_session_tables.py +57 -0
  58. kestrel_app-0.1.0/src/career_os/_alembic/env.py +68 -0
  59. kestrel_app-0.1.0/src/career_os/_alembic/script.py.mako +28 -0
  60. kestrel_app-0.1.0/src/career_os/_alembic/versions/07cca291f71f_add_saved_searches_table.py +55 -0
  61. kestrel_app-0.1.0/src/career_os/_alembic/versions/775c5d68cc2d_add_discovery_models_discovered_jobs_.py +155 -0
  62. kestrel_app-0.1.0/src/career_os/_alembic/versions/a1b2c3d4e5f6_add_job_requirements_table.py +59 -0
  63. kestrel_app-0.1.0/src/career_os/_alembic/versions/a7b8c9d0e1f2_add_scoring_breakdown_market_schedule.py +57 -0
  64. kestrel_app-0.1.0/src/career_os/_alembic/versions/b192ca99adc9_add_gap_id_and_started_at_to_learning_.py +40 -0
  65. kestrel_app-0.1.0/src/career_os/_alembic/versions/b3c4d5e6f7a8_add_ondelete_set_null_to_discovery_run_fk.py +67 -0
  66. kestrel_app-0.1.0/src/career_os/_alembic/versions/c0b4dc9ab23a_initial_schema_profiles_applications_.py +172 -0
  67. kestrel_app-0.1.0/src/career_os/_alembic/versions/c4d5e6f7a8b9_add_interview_prep_tables.py +109 -0
  68. kestrel_app-0.1.0/src/career_os/_alembic/versions/d5e6f7a8b9c0_add_star_stories_table.py +59 -0
  69. kestrel_app-0.1.0/src/career_os/_alembic/versions/e6f7a8b9c0d1_add_company_research_reports_table.py +62 -0
  70. kestrel_app-0.1.0/src/career_os/_alembic/versions/e730f1c7a51e_add_skills_intelligence_models.py +175 -0
  71. kestrel_app-0.1.0/src/career_os/_alembic/versions/e8b54d7d5d6a_add_scoring_engine_models_scoring_.py +107 -0
  72. kestrel_app-0.1.0/src/career_os/_alembic/versions/eac8c4fc464e_add_parent_event_id_to_calendar_events.py +44 -0
  73. kestrel_app-0.1.0/src/career_os/_alembic/versions/f1a2b3c4d5e6_add_profile_id_to_dedup_constraint.py +48 -0
  74. kestrel_app-0.1.0/src/career_os/_alembic/versions/f7a8b9c0d1e2_add_integration_configs_table.py +55 -0
  75. kestrel_app-0.1.0/src/career_os/_alembic/versions/g8b9c0d1e2f3_add_ticktick_sync_tasks_table.py +65 -0
  76. kestrel_app-0.1.0/src/career_os/_alembic/versions/h9c0d1e2f3g4_add_calendar_events_table.py +83 -0
  77. kestrel_app-0.1.0/src/career_os/_alembic/versions/i0d1e2f3g4h5_add_time_sessions_table.py +51 -0
  78. kestrel_app-0.1.0/src/career_os/_alembic/versions/j1e2f3g4h5i6_add_pushover_notification_tables.py +87 -0
  79. kestrel_app-0.1.0/src/career_os/_alembic/versions/k2f3g4h5i6j7_add_voice_session_tables.py +58 -0
  80. kestrel_app-0.1.0/src/career_os/_alembic.ini +151 -0
  81. kestrel_app-0.1.0/src/career_os/_frontend_dist/assets/index-DhdrttW4.css +2 -0
  82. kestrel_app-0.1.0/src/career_os/_frontend_dist/assets/index-KDnWhwGY.js +50 -0
  83. kestrel_app-0.1.0/src/career_os/_frontend_dist/dist/assets/index-DH_vCftM.js +50 -0
  84. kestrel_app-0.1.0/src/career_os/_frontend_dist/dist/assets/index-Qt6RZzNz.css +2 -0
  85. kestrel_app-0.1.0/src/career_os/_frontend_dist/dist/index.html +13 -0
  86. kestrel_app-0.1.0/src/career_os/_frontend_dist/index.html +13 -0
  87. kestrel_app-0.1.0/src/career_os/ai/__init__.py +14 -0
  88. kestrel_app-0.1.0/src/career_os/ai/base.py +59 -0
  89. kestrel_app-0.1.0/src/career_os/ai/factory.py +48 -0
  90. kestrel_app-0.1.0/src/career_os/ai/mock_provider.py +1664 -0
  91. kestrel_app-0.1.0/src/career_os/ai/openrouter_provider.py +229 -0
  92. kestrel_app-0.1.0/src/career_os/api/__init__.py +1 -0
  93. kestrel_app-0.1.0/src/career_os/api/ai.py +86 -0
  94. kestrel_app-0.1.0/src/career_os/api/analytics.py +24 -0
  95. kestrel_app-0.1.0/src/career_os/api/applications.py +230 -0
  96. kestrel_app-0.1.0/src/career_os/api/calendar.py +263 -0
  97. kestrel_app-0.1.0/src/career_os/api/coaching.py +61 -0
  98. kestrel_app-0.1.0/src/career_os/api/contacts.py +274 -0
  99. kestrel_app-0.1.0/src/career_os/api/discovery.py +191 -0
  100. kestrel_app-0.1.0/src/career_os/api/follow_ups.py +123 -0
  101. kestrel_app-0.1.0/src/career_os/api/gaps.py +152 -0
  102. kestrel_app-0.1.0/src/career_os/api/goals.py +205 -0
  103. kestrel_app-0.1.0/src/career_os/api/integrations.py +69 -0
  104. kestrel_app-0.1.0/src/career_os/api/intelligence.py +136 -0
  105. kestrel_app-0.1.0/src/career_os/api/interview_prep.py +111 -0
  106. kestrel_app-0.1.0/src/career_os/api/jobs.py +204 -0
  107. kestrel_app-0.1.0/src/career_os/api/learning.py +139 -0
  108. kestrel_app-0.1.0/src/career_os/api/market.py +190 -0
  109. kestrel_app-0.1.0/src/career_os/api/profiles.py +126 -0
  110. kestrel_app-0.1.0/src/career_os/api/pushover.py +173 -0
  111. kestrel_app-0.1.0/src/career_os/api/research.py +74 -0
  112. kestrel_app-0.1.0/src/career_os/api/scoring.py +195 -0
  113. kestrel_app-0.1.0/src/career_os/api/skills.py +184 -0
  114. kestrel_app-0.1.0/src/career_os/api/star_stories.py +164 -0
  115. kestrel_app-0.1.0/src/career_os/api/ticktick.py +169 -0
  116. kestrel_app-0.1.0/src/career_os/api/timingsapp.py +169 -0
  117. kestrel_app-0.1.0/src/career_os/api/voice.py +152 -0
  118. kestrel_app-0.1.0/src/career_os/cli/__init__.py +1 -0
  119. kestrel_app-0.1.0/src/career_os/cli/contacts.py +431 -0
  120. kestrel_app-0.1.0/src/career_os/cli/main.py +2190 -0
  121. kestrel_app-0.1.0/src/career_os/config.py +58 -0
  122. kestrel_app-0.1.0/src/career_os/database.py +63 -0
  123. kestrel_app-0.1.0/src/career_os/discovery/__init__.py +5 -0
  124. kestrel_app-0.1.0/src/career_os/discovery/adapters.py +409 -0
  125. kestrel_app-0.1.0/src/career_os/discovery/scheduler.py +101 -0
  126. kestrel_app-0.1.0/src/career_os/main.py +248 -0
  127. kestrel_app-0.1.0/src/career_os/middleware.py +48 -0
  128. kestrel_app-0.1.0/src/career_os/migration/__init__.py +1 -0
  129. kestrel_app-0.1.0/src/career_os/migration/csv_import.py +251 -0
  130. kestrel_app-0.1.0/src/career_os/migration/link_packages.py +193 -0
  131. kestrel_app-0.1.0/src/career_os/migration/run_migration.py +107 -0
  132. kestrel_app-0.1.0/src/career_os/migration/seed.py +107 -0
  133. kestrel_app-0.1.0/src/career_os/models/__init__.py +73 -0
  134. kestrel_app-0.1.0/src/career_os/models/calendar.py +78 -0
  135. kestrel_app-0.1.0/src/career_os/models/company_research.py +53 -0
  136. kestrel_app-0.1.0/src/career_os/models/contacts.py +123 -0
  137. kestrel_app-0.1.0/src/career_os/models/discovery.py +178 -0
  138. kestrel_app-0.1.0/src/career_os/models/integrations.py +49 -0
  139. kestrel_app-0.1.0/src/career_os/models/interview_prep.py +96 -0
  140. kestrel_app-0.1.0/src/career_os/models/models.py +217 -0
  141. kestrel_app-0.1.0/src/career_os/models/pushover.py +96 -0
  142. kestrel_app-0.1.0/src/career_os/models/scoring.py +121 -0
  143. kestrel_app-0.1.0/src/career_os/models/skills.py +208 -0
  144. kestrel_app-0.1.0/src/career_os/models/star_stories.py +58 -0
  145. kestrel_app-0.1.0/src/career_os/models/ticktick_sync.py +62 -0
  146. kestrel_app-0.1.0/src/career_os/models/timingsapp.py +68 -0
  147. kestrel_app-0.1.0/src/career_os/models/voice.py +77 -0
  148. kestrel_app-0.1.0/src/career_os/schemas/__init__.py +114 -0
  149. kestrel_app-0.1.0/src/career_os/schemas/ai.py +205 -0
  150. kestrel_app-0.1.0/src/career_os/schemas/ai_health.py +43 -0
  151. kestrel_app-0.1.0/src/career_os/schemas/analytics.py +93 -0
  152. kestrel_app-0.1.0/src/career_os/schemas/applications.py +258 -0
  153. kestrel_app-0.1.0/src/career_os/schemas/calendar.py +133 -0
  154. kestrel_app-0.1.0/src/career_os/schemas/coaching.py +67 -0
  155. kestrel_app-0.1.0/src/career_os/schemas/contacts.py +361 -0
  156. kestrel_app-0.1.0/src/career_os/schemas/discovery.py +246 -0
  157. kestrel_app-0.1.0/src/career_os/schemas/follow_ups.py +79 -0
  158. kestrel_app-0.1.0/src/career_os/schemas/gaps.py +135 -0
  159. kestrel_app-0.1.0/src/career_os/schemas/goals.py +189 -0
  160. kestrel_app-0.1.0/src/career_os/schemas/integrations.py +239 -0
  161. kestrel_app-0.1.0/src/career_os/schemas/interview_prep.py +114 -0
  162. kestrel_app-0.1.0/src/career_os/schemas/jobs.py +153 -0
  163. kestrel_app-0.1.0/src/career_os/schemas/learning.py +138 -0
  164. kestrel_app-0.1.0/src/career_os/schemas/market.py +199 -0
  165. kestrel_app-0.1.0/src/career_os/schemas/profiles.py +63 -0
  166. kestrel_app-0.1.0/src/career_os/schemas/pushover.py +121 -0
  167. kestrel_app-0.1.0/src/career_os/schemas/research.py +174 -0
  168. kestrel_app-0.1.0/src/career_os/schemas/role_intelligence.py +128 -0
  169. kestrel_app-0.1.0/src/career_os/schemas/scoring.py +156 -0
  170. kestrel_app-0.1.0/src/career_os/schemas/skills.py +158 -0
  171. kestrel_app-0.1.0/src/career_os/schemas/star_stories.py +128 -0
  172. kestrel_app-0.1.0/src/career_os/schemas/ticktick.py +65 -0
  173. kestrel_app-0.1.0/src/career_os/schemas/timingsapp.py +109 -0
  174. kestrel_app-0.1.0/src/career_os/schemas/voice.py +99 -0
  175. kestrel_app-0.1.0/src/career_os/services/__init__.py +1 -0
  176. kestrel_app-0.1.0/src/career_os/services/activity.py +42 -0
  177. kestrel_app-0.1.0/src/career_os/services/ai_health.py +295 -0
  178. kestrel_app-0.1.0/src/career_os/services/analytics.py +331 -0
  179. kestrel_app-0.1.0/src/career_os/services/applications.py +289 -0
  180. kestrel_app-0.1.0/src/career_os/services/calendar.py +536 -0
  181. kestrel_app-0.1.0/src/career_os/services/coaching.py +439 -0
  182. kestrel_app-0.1.0/src/career_os/services/company_research.py +495 -0
  183. kestrel_app-0.1.0/src/career_os/services/contacts.py +402 -0
  184. kestrel_app-0.1.0/src/career_os/services/discovery.py +635 -0
  185. kestrel_app-0.1.0/src/career_os/services/follow_ups.py +297 -0
  186. kestrel_app-0.1.0/src/career_os/services/gap_analysis.py +425 -0
  187. kestrel_app-0.1.0/src/career_os/services/goals.py +619 -0
  188. kestrel_app-0.1.0/src/career_os/services/integrations.py +233 -0
  189. kestrel_app-0.1.0/src/career_os/services/interview_prep.py +762 -0
  190. kestrel_app-0.1.0/src/career_os/services/jobs.py +312 -0
  191. kestrel_app-0.1.0/src/career_os/services/learning.py +354 -0
  192. kestrel_app-0.1.0/src/career_os/services/market.py +538 -0
  193. kestrel_app-0.1.0/src/career_os/services/pushover.py +899 -0
  194. kestrel_app-0.1.0/src/career_os/services/pushover_client.py +146 -0
  195. kestrel_app-0.1.0/src/career_os/services/role_intelligence.py +520 -0
  196. kestrel_app-0.1.0/src/career_os/services/salary.py +69 -0
  197. kestrel_app-0.1.0/src/career_os/services/scoring.py +616 -0
  198. kestrel_app-0.1.0/src/career_os/services/skills.py +286 -0
  199. kestrel_app-0.1.0/src/career_os/services/skills_parsing.py +738 -0
  200. kestrel_app-0.1.0/src/career_os/services/star_stories.py +444 -0
  201. kestrel_app-0.1.0/src/career_os/services/ticktick_client.py +201 -0
  202. kestrel_app-0.1.0/src/career_os/services/ticktick_scheduler.py +88 -0
  203. kestrel_app-0.1.0/src/career_os/services/ticktick_sync.py +588 -0
  204. kestrel_app-0.1.0/src/career_os/services/timingsapp.py +572 -0
  205. kestrel_app-0.1.0/src/career_os/services/timingsapp_client.py +188 -0
  206. kestrel_app-0.1.0/src/career_os/services/voice.py +365 -0
  207. kestrel_app-0.1.0/src/kestrel_app.egg-info/PKG-INFO +28 -0
  208. kestrel_app-0.1.0/src/kestrel_app.egg-info/SOURCES.txt +270 -0
  209. kestrel_app-0.1.0/src/kestrel_app.egg-info/dependency_links.txt +1 -0
  210. kestrel_app-0.1.0/src/kestrel_app.egg-info/entry_points.txt +3 -0
  211. kestrel_app-0.1.0/src/kestrel_app.egg-info/requires.txt +22 -0
  212. kestrel_app-0.1.0/src/kestrel_app.egg-info/top_level.txt +1 -0
  213. kestrel_app-0.1.0/tests/test_activity_log_extended.py +132 -0
  214. kestrel_app-0.1.0/tests/test_ai_health.py +295 -0
  215. kestrel_app-0.1.0/tests/test_ai_provider.py +760 -0
  216. kestrel_app-0.1.0/tests/test_analytics.py +649 -0
  217. kestrel_app-0.1.0/tests/test_auth.py +108 -0
  218. kestrel_app-0.1.0/tests/test_auto_apply.py +1084 -0
  219. kestrel_app-0.1.0/tests/test_batch_apply_browser.py +1167 -0
  220. kestrel_app-0.1.0/tests/test_calendar.py +848 -0
  221. kestrel_app-0.1.0/tests/test_cli.py +55 -0
  222. kestrel_app-0.1.0/tests/test_cli_contacts.py +274 -0
  223. kestrel_app-0.1.0/tests/test_cli_discovery.py +633 -0
  224. kestrel_app-0.1.0/tests/test_cli_pipeline.py +554 -0
  225. kestrel_app-0.1.0/tests/test_cli_prep.py +672 -0
  226. kestrel_app-0.1.0/tests/test_cli_skills.py +570 -0
  227. kestrel_app-0.1.0/tests/test_coaching.py +890 -0
  228. kestrel_app-0.1.0/tests/test_company_research.py +1109 -0
  229. kestrel_app-0.1.0/tests/test_contacts.py +437 -0
  230. kestrel_app-0.1.0/tests/test_contacts_api.py +330 -0
  231. kestrel_app-0.1.0/tests/test_contacts_integration.py +256 -0
  232. kestrel_app-0.1.0/tests/test_daily_pipeline.py +306 -0
  233. kestrel_app-0.1.0/tests/test_dep_security.py +44 -0
  234. kestrel_app-0.1.0/tests/test_discovery.py +1167 -0
  235. kestrel_app-0.1.0/tests/test_discovery_salary_fixes.py +672 -0
  236. kestrel_app-0.1.0/tests/test_docs.py +20 -0
  237. kestrel_app-0.1.0/tests/test_follow_ups.py +509 -0
  238. kestrel_app-0.1.0/tests/test_gap_analysis.py +1162 -0
  239. kestrel_app-0.1.0/tests/test_germany_jobs.py +275 -0
  240. kestrel_app-0.1.0/tests/test_goals.py +1292 -0
  241. kestrel_app-0.1.0/tests/test_health.py +17 -0
  242. kestrel_app-0.1.0/tests/test_html_stripping.py +76 -0
  243. kestrel_app-0.1.0/tests/test_integrations_config.py +474 -0
  244. kestrel_app-0.1.0/tests/test_interview_prep.py +1186 -0
  245. kestrel_app-0.1.0/tests/test_job_scorer.py +577 -0
  246. kestrel_app-0.1.0/tests/test_kestrel_start.py +103 -0
  247. kestrel_app-0.1.0/tests/test_learning.py +969 -0
  248. kestrel_app-0.1.0/tests/test_market.py +809 -0
  249. kestrel_app-0.1.0/tests/test_md_to_pdf.py +206 -0
  250. kestrel_app-0.1.0/tests/test_migration.py +837 -0
  251. kestrel_app-0.1.0/tests/test_new_sources.py +576 -0
  252. kestrel_app-0.1.0/tests/test_pipeline_api.py +716 -0
  253. kestrel_app-0.1.0/tests/test_prep_gap_driven.py +574 -0
  254. kestrel_app-0.1.0/tests/test_profile_scoping.py +498 -0
  255. kestrel_app-0.1.0/tests/test_profiles_crud.py +528 -0
  256. kestrel_app-0.1.0/tests/test_pushover.py +1184 -0
  257. kestrel_app-0.1.0/tests/test_render_tailored_cvs.py +265 -0
  258. kestrel_app-0.1.0/tests/test_role_intelligence.py +1015 -0
  259. kestrel_app-0.1.0/tests/test_round2_residuals.py +517 -0
  260. kestrel_app-0.1.0/tests/test_scoring.py +1496 -0
  261. kestrel_app-0.1.0/tests/test_scoring_rescore.py +226 -0
  262. kestrel_app-0.1.0/tests/test_scrape_resilient.py +483 -0
  263. kestrel_app-0.1.0/tests/test_scraper.py +199 -0
  264. kestrel_app-0.1.0/tests/test_search_filter.py +744 -0
  265. kestrel_app-0.1.0/tests/test_skills_api.py +856 -0
  266. kestrel_app-0.1.0/tests/test_skills_parsing.py +1412 -0
  267. kestrel_app-0.1.0/tests/test_star_stories.py +1214 -0
  268. kestrel_app-0.1.0/tests/test_ticktick.py +1403 -0
  269. kestrel_app-0.1.0/tests/test_timingsapp.py +1140 -0
  270. kestrel_app-0.1.0/tests/test_ut_cross_flow_fixes.py +565 -0
  271. kestrel_app-0.1.0/tests/test_ut_prep_salary_fixes.py +696 -0
  272. kestrel_app-0.1.0/tests/test_voice.py +650 -0
@@ -0,0 +1,53 @@
1
+ # Kestrel Configuration
2
+ # Copy to .env and customize: cp .env.example .env
3
+
4
+ # -- AI Provider ----------------------------------------------------------
5
+ # mock = fully offline, no API key needed (default)
6
+ # openrouter = real AI via OpenRouter (requires API key below)
7
+ AI_PROVIDER=mock
8
+
9
+ # Required when AI_PROVIDER=openrouter
10
+ # Sign up at https://openrouter.ai to get a key
11
+ OPENROUTER_API_KEY=
12
+ OPENROUTER_MODEL=anthropic/claude-sonnet-4
13
+
14
+ # -- Database --------------------------------------------------------------
15
+ DATABASE_URL=sqlite:///data/career_os.db
16
+
17
+ # -- Server ----------------------------------------------------------------
18
+ HOST=0.0.0.0
19
+ PORT=8100
20
+ FRONTEND_URL=http://localhost:8101
21
+ DEBUG=false
22
+
23
+ # -- Auth (optional) -------------------------------------------------------
24
+ # Enable to protect your instance with an API key
25
+ AUTH_ENABLED=false
26
+ AUTH_API_KEY=
27
+
28
+ # -- Anti-Captcha (optional) -----------------------------------------------
29
+ # For automated form filling with captcha solving
30
+ # Sign up at https://anti-captcha.com ($10 lasts a long time)
31
+ ANTICAPTCHA_KEY=
32
+
33
+ # -- Notifications (optional) ----------------------------------------------
34
+ # Pushover: https://pushover.net
35
+ PUSHOVER_USER_KEY=
36
+ PUSHOVER_APP_TOKEN=
37
+
38
+ # Mailgun: https://mailgun.com
39
+ MAILGUN_API_KEY=
40
+ MAILGUN_DOMAIN=
41
+ NOTIFY_EMAIL=
42
+
43
+ # -- Integrations (optional) -----------------------------------------------
44
+ # TickTick task sync
45
+ TICKTICK_API_TOKEN=
46
+
47
+ # Google Sheets (for daily scan logging)
48
+ GOOGLE_SERVICE_ACCOUNT_JSON=
49
+ GOOGLE_SHEET_ID=
50
+
51
+ # -- Pipeline (optional) ---------------------------------------------------
52
+ # Default location for job search (used by daily scan CI)
53
+ PIPELINE_LOCATION=Remote
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kestrel Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,5 @@
1
+ include alembic.ini
2
+ recursive-include alembic *.py *.mako
3
+ recursive-include frontend/dist *
4
+ include .env.example
5
+ include config/personal.yaml.example
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: kestrel-app
3
+ Version: 0.1.0
4
+ Summary: AI-powered job search platform. Self-hosted, open-source, privacy-first.
5
+ Requires-Python: >=3.13
6
+ License-File: LICENSE
7
+ Requires-Dist: fastapi>=0.115.0
8
+ Requires-Dist: uvicorn[standard]>=0.34.0
9
+ Requires-Dist: sqlalchemy>=2.0
10
+ Requires-Dist: alembic>=1.15.0
11
+ Requires-Dist: typer>=0.15.0
12
+ Requires-Dist: pydantic>=2.10.0
13
+ Requires-Dist: pydantic-settings>=2.8.0
14
+ Requires-Dist: python-dotenv>=1.1.0
15
+ Requires-Dist: httpx>=0.28.0
16
+ Requires-Dist: aiosqlite>=0.21.0
17
+ Requires-Dist: rich>=13.9.0
18
+ Requires-Dist: icalendar>=7.0.0
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=8.3.0; extra == "dev"
21
+ Requires-Dist: pytest-asyncio>=0.25.0; extra == "dev"
22
+ Requires-Dist: pytest-cov>=6.1.0; extra == "dev"
23
+ Requires-Dist: ruff>=0.11.0; extra == "dev"
24
+ Requires-Dist: httpx>=0.28.0; extra == "dev"
25
+ Requires-Dist: pandas>=2.2.0; extra == "dev"
26
+ Requires-Dist: python-jobspy>=1.1.0; extra == "dev"
27
+ Requires-Dist: fpdf2>=2.8.0; extra == "dev"
28
+ Dynamic: license-file
@@ -0,0 +1,108 @@
1
+ <h1 align="center">Kestrel</h1>
2
+
3
+ <p align="center">
4
+ <strong>A job search system that runs on your computer.</strong><br>
5
+ Finds jobs. Scores them. Tracks your pipeline. Your data stays yours.
6
+ </p>
7
+
8
+ <p align="center">
9
+ <img src="https://img.shields.io/badge/Docker-one--click_setup-2496ED?style=flat-square&logo=docker&logoColor=white" alt="Docker">
10
+ <img src="https://img.shields.io/badge/AI-built--in_(free)-22c55e?style=flat-square" alt="AI included">
11
+ <img src="https://img.shields.io/badge/License-MIT-yellow?style=flat-square" alt="MIT License">
12
+ <img src="https://img.shields.io/badge/No_coding_required-blue?style=flat-square" alt="No coding required">
13
+ </p>
14
+
15
+ <p align="center">
16
+ <a href="https://codespaces.new/pleasedodisturb/kestrel"><img src="https://github.com/codespaces/badge.svg" alt="Open in GitHub Codespaces" height="32"></a>
17
+ </p>
18
+
19
+ ---
20
+
21
+ ## Install
22
+
23
+ Pick whichever feels right. All three give you the same app.
24
+
25
+ ### Option 1: pip install (no Docker needed)
26
+
27
+ ```bash
28
+ pip install kestrel-app
29
+ kestrel start
30
+ ```
31
+
32
+ Opens your browser automatically. Data stored in `~/.kestrel/`. Requires Python 3.13+.
33
+
34
+ ### Option 2: Docker (isolated, one command)
35
+
36
+ ```bash
37
+ git clone https://github.com/pleasedodisturb/kestrel.git && cd kestrel
38
+ bash setup.sh
39
+ ```
40
+
41
+ ### Option 3: Try in your browser (zero install)
42
+
43
+ [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/pleasedodisturb/kestrel)
44
+
45
+ Free with a GitHub account. Your own instance in 2 minutes. No install, nothing on your computer.
46
+
47
+ **Need more help?** [Step-by-step guide](docs/QUICKSTART.md) - no coding knowledge required.
48
+
49
+ ---
50
+
51
+ ## What it does
52
+
53
+ - **Discovers jobs** from multiple boards automatically (Indeed, LinkedIn, Glassdoor, Arbeitsagentur)
54
+ - **Scores them** against your profile with AI - stop guessing which jobs are worth applying to
55
+ - **Tracks your pipeline** on a Kanban board - drag applications between stages
56
+ - **Prepares you for interviews** - company research, mock questions, STAR story library
57
+ - **Runs daily scans** via GitHub Actions - wake up to a scored digest of new matches
58
+ - **Works offline** - Demo Mode included, zero cost to start. Add real AI when ready.
59
+
60
+ Everything runs on your machine. No account needed. No data leaves your computer (unless you connect an AI provider).
61
+
62
+ ---
63
+
64
+ ## Docs
65
+
66
+ | Guide | For |
67
+ |-------|-----|
68
+ | [Quickstart](docs/QUICKSTART.md) | First-time setup, step by step |
69
+ | [FAQ](docs/FAQ.md) | Common questions answered |
70
+ | [Help](docs/HELP.md) | Troubleshooting when something breaks |
71
+ | [AI Provider Guide](docs/AI-PROVIDERS.md) | Choosing and configuring an AI provider |
72
+ | [Comparison](docs/COMPARISON.md) | How Kestrel compares to other tools |
73
+ | [Features & API Reference](docs/REFERENCE.md) | Full feature list, architecture, CLI, API endpoints |
74
+ | [Deployment](DEPLOY.md) | Railway, Fly.io, VPS hosting |
75
+ | [Contributing](CONTRIBUTING.md) | Development setup and pull request guidelines |
76
+
77
+ ---
78
+
79
+ ## Quick look
80
+
81
+ **Web dashboard** with Kanban board, AI scoring, discovery, analytics, and more:
82
+
83
+ ```
84
+ http://localhost:8101 - Dashboard (what you use)
85
+ http://localhost:8100/docs - API docs (if you're curious)
86
+ ```
87
+
88
+ **Add real AI** (optional - works fine without it):
89
+
90
+ 1. Sign up at [openrouter.ai](https://openrouter.ai) and copy your API key
91
+ 2. Open the settings file in your Kestrel folder:
92
+ - **Mac:** Open Terminal, go to your Kestrel folder, type `open .env`
93
+ - **Windows:** Open the Kestrel folder, type `.env` in the address bar
94
+ - **Or any text editor:** the file is called `.env` (hidden file - [how to see it](docs/FAQ.md#hidden-files-on-mac))
95
+ 3. Change these two lines:
96
+ ```
97
+ AI_PROVIDER=openrouter
98
+ OPENROUTER_API_KEY=sk-or-paste-your-key-here
99
+ ```
100
+ 4. Save the file, then restart: `docker compose restart`
101
+
102
+ Costs about $1-3/month. Full guide: [AI Provider Guide](docs/AI-PROVIDERS.md)
103
+
104
+ ---
105
+
106
+ ## License
107
+
108
+ [MIT](LICENSE) - free forever, do whatever you want with it.
@@ -0,0 +1,68 @@
1
+ """Alembic environment configuration."""
2
+
3
+ import sys
4
+ from logging.config import fileConfig
5
+ from pathlib import Path
6
+
7
+ from sqlalchemy import engine_from_config, pool
8
+
9
+ from alembic import context
10
+
11
+ # Ensure src/ is on sys.path so career_os is importable
12
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
13
+
14
+ from career_os.database import Base # noqa: E402
15
+ from career_os.models import models as _models # noqa: E402, F401
16
+ from career_os.models import skills as _skills # noqa: E402, F401
17
+ from career_os.models import discovery as _discovery # noqa: E402, F401
18
+
19
+ # this is the Alembic Config object, which provides
20
+ # access to the values within the .ini file in use.
21
+ config = context.config
22
+
23
+ # Interpret the config file for Python logging.
24
+ if config.config_file_name is not None:
25
+ fileConfig(config.config_file_name)
26
+
27
+ # Model metadata for autogenerate support
28
+ target_metadata = Base.metadata
29
+
30
+
31
+ def run_migrations_offline() -> None:
32
+ """Run migrations in 'offline' mode."""
33
+ url = config.get_main_option("sqlalchemy.url")
34
+ context.configure(
35
+ url=url,
36
+ target_metadata=target_metadata,
37
+ literal_binds=True,
38
+ dialect_opts={"paramstyle": "named"},
39
+ render_as_batch=True,
40
+ )
41
+
42
+ with context.begin_transaction():
43
+ context.run_migrations()
44
+
45
+
46
+ def run_migrations_online() -> None:
47
+ """Run migrations in 'online' mode."""
48
+ connectable = engine_from_config(
49
+ config.get_section(config.config_ini_section, {}),
50
+ prefix="sqlalchemy.",
51
+ poolclass=pool.NullPool,
52
+ )
53
+
54
+ with connectable.connect() as connection:
55
+ context.configure(
56
+ connection=connection,
57
+ target_metadata=target_metadata,
58
+ render_as_batch=True,
59
+ )
60
+
61
+ with context.begin_transaction():
62
+ context.run_migrations()
63
+
64
+
65
+ if context.is_offline_mode():
66
+ run_migrations_offline()
67
+ else:
68
+ run_migrations_online()
@@ -0,0 +1,28 @@
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ ${imports if imports else ""}
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = ${repr(up_revision)}
16
+ down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
17
+ branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
18
+ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Upgrade schema."""
23
+ ${upgrades if upgrades else "pass"}
24
+
25
+
26
+ def downgrade() -> None:
27
+ """Downgrade schema."""
28
+ ${downgrades if downgrades else "pass"}
@@ -0,0 +1,47 @@
1
+ """add saved_searches table
2
+
3
+ Revision ID: 07cca291f71f
4
+ Revises: e8b54d7d5d6a
5
+ Create Date: 2026-03-13 21:30:04.969376
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = '07cca291f71f'
16
+ down_revision: Union[str, Sequence[str], None] = 'e8b54d7d5d6a'
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Upgrade schema."""
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ op.create_table('saved_searches',
25
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
26
+ sa.Column('profile_id', sa.Integer(), nullable=False),
27
+ sa.Column('name', sa.String(length=255), nullable=False),
28
+ sa.Column('config', sa.Text(), nullable=False),
29
+ sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
30
+ sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
31
+ sa.ForeignKeyConstraint(['profile_id'], ['profiles.id'], ),
32
+ sa.PrimaryKeyConstraint('id')
33
+ )
34
+ with op.batch_alter_table('saved_searches', schema=None) as batch_op:
35
+ batch_op.create_index(batch_op.f('ix_saved_searches_profile_id'), ['profile_id'], unique=False)
36
+
37
+ # ### end Alembic commands ###
38
+
39
+
40
+ def downgrade() -> None:
41
+ """Downgrade schema."""
42
+ # ### commands auto generated by Alembic - please adjust! ###
43
+ with op.batch_alter_table('saved_searches', schema=None) as batch_op:
44
+ batch_op.drop_index(batch_op.f('ix_saved_searches_profile_id'))
45
+
46
+ op.drop_table('saved_searches')
47
+ # ### end Alembic commands ###
@@ -0,0 +1,114 @@
1
+ """add discovery models: discovered_jobs, search_profiles, discovery_runs
2
+
3
+ Revision ID: 775c5d68cc2d
4
+ Revises: b192ca99adc9
5
+ Create Date: 2026-03-13 20:57:12.532122
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = '775c5d68cc2d'
16
+ down_revision: Union[str, Sequence[str], None] = 'b192ca99adc9'
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Upgrade schema."""
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ op.create_table('search_profiles',
25
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
26
+ sa.Column('profile_id', sa.Integer(), nullable=False),
27
+ sa.Column('name', sa.String(length=255), nullable=False),
28
+ sa.Column('keywords', sa.Text(), nullable=True),
29
+ sa.Column('locations', sa.Text(), nullable=True),
30
+ sa.Column('remote_only', sa.Boolean(), nullable=False),
31
+ sa.Column('sources', sa.Text(), nullable=True),
32
+ sa.Column('filters', sa.Text(), nullable=True),
33
+ sa.Column('is_active', sa.Boolean(), nullable=False),
34
+ sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
35
+ sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
36
+ sa.ForeignKeyConstraint(['profile_id'], ['profiles.id'], ),
37
+ sa.PrimaryKeyConstraint('id')
38
+ )
39
+ with op.batch_alter_table('search_profiles', schema=None) as batch_op:
40
+ batch_op.create_index(batch_op.f('ix_search_profiles_profile_id'), ['profile_id'], unique=False)
41
+
42
+ op.create_table('discovered_jobs',
43
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
44
+ sa.Column('profile_id', sa.Integer(), nullable=False),
45
+ sa.Column('title', sa.String(length=500), nullable=False),
46
+ sa.Column('company', sa.String(length=255), nullable=False),
47
+ sa.Column('location', sa.String(length=255), nullable=False),
48
+ sa.Column('url', sa.Text(), nullable=True),
49
+ sa.Column('description', sa.Text(), nullable=True),
50
+ sa.Column('salary_range', sa.String(length=255), nullable=True),
51
+ sa.Column('remote', sa.Boolean(), nullable=False),
52
+ sa.Column('posted_at', sa.DateTime(timezone=True), nullable=True),
53
+ sa.Column('title_normalized', sa.String(length=500), nullable=False),
54
+ sa.Column('company_normalized', sa.String(length=255), nullable=False),
55
+ sa.Column('location_normalized', sa.String(length=255), nullable=False),
56
+ sa.Column('sources', sa.Text(), nullable=False),
57
+ sa.Column('source_urls', sa.Text(), nullable=False),
58
+ sa.Column('fit_score', sa.Float(), nullable=True),
59
+ sa.Column('application_id', sa.Integer(), nullable=True),
60
+ sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
61
+ sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
62
+ sa.ForeignKeyConstraint(['application_id'], ['applications.id'], ),
63
+ sa.ForeignKeyConstraint(['profile_id'], ['profiles.id'], ),
64
+ sa.PrimaryKeyConstraint('id'),
65
+ sa.UniqueConstraint('title_normalized', 'company_normalized', 'location_normalized', name='uq_discovered_job_dedup')
66
+ )
67
+ with op.batch_alter_table('discovered_jobs', schema=None) as batch_op:
68
+ batch_op.create_index(batch_op.f('ix_discovered_jobs_company_normalized'), ['company_normalized'], unique=False)
69
+ batch_op.create_index(batch_op.f('ix_discovered_jobs_location_normalized'), ['location_normalized'], unique=False)
70
+ batch_op.create_index(batch_op.f('ix_discovered_jobs_profile_id'), ['profile_id'], unique=False)
71
+ batch_op.create_index(batch_op.f('ix_discovered_jobs_title_normalized'), ['title_normalized'], unique=False)
72
+
73
+ op.create_table('discovery_runs',
74
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
75
+ sa.Column('profile_id', sa.Integer(), nullable=False),
76
+ sa.Column('search_profile_id', sa.Integer(), nullable=True),
77
+ sa.Column('trigger', sa.String(length=50), nullable=False),
78
+ sa.Column('status', sa.String(length=50), nullable=False),
79
+ sa.Column('total_found', sa.Integer(), nullable=False),
80
+ sa.Column('new_jobs', sa.Integer(), nullable=False),
81
+ sa.Column('duplicates', sa.Integer(), nullable=False),
82
+ sa.Column('errors', sa.Integer(), nullable=False),
83
+ sa.Column('warnings', sa.Text(), nullable=True),
84
+ sa.Column('started_at', sa.DateTime(timezone=True), nullable=False),
85
+ sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
86
+ sa.ForeignKeyConstraint(['profile_id'], ['profiles.id'], ),
87
+ sa.ForeignKeyConstraint(['search_profile_id'], ['search_profiles.id'], ),
88
+ sa.PrimaryKeyConstraint('id')
89
+ )
90
+ with op.batch_alter_table('discovery_runs', schema=None) as batch_op:
91
+ batch_op.create_index(batch_op.f('ix_discovery_runs_profile_id'), ['profile_id'], unique=False)
92
+
93
+ # ### end Alembic commands ###
94
+
95
+
96
+ def downgrade() -> None:
97
+ """Downgrade schema."""
98
+ # ### commands auto generated by Alembic - please adjust! ###
99
+ with op.batch_alter_table('discovery_runs', schema=None) as batch_op:
100
+ batch_op.drop_index(batch_op.f('ix_discovery_runs_profile_id'))
101
+
102
+ op.drop_table('discovery_runs')
103
+ with op.batch_alter_table('discovered_jobs', schema=None) as batch_op:
104
+ batch_op.drop_index(batch_op.f('ix_discovered_jobs_title_normalized'))
105
+ batch_op.drop_index(batch_op.f('ix_discovered_jobs_profile_id'))
106
+ batch_op.drop_index(batch_op.f('ix_discovered_jobs_location_normalized'))
107
+ batch_op.drop_index(batch_op.f('ix_discovered_jobs_company_normalized'))
108
+
109
+ op.drop_table('discovered_jobs')
110
+ with op.batch_alter_table('search_profiles', schema=None) as batch_op:
111
+ batch_op.drop_index(batch_op.f('ix_search_profiles_profile_id'))
112
+
113
+ op.drop_table('search_profiles')
114
+ # ### end Alembic commands ###
@@ -0,0 +1,44 @@
1
+ """add job_requirements table
2
+
3
+ Revision ID: a1b2c3d4e5f6
4
+ Revises: e730f1c7a51e
5
+ Create Date: 2026-03-13 16:00:00.000000
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = 'a1b2c3d4e5f6'
16
+ down_revision: Union[str, Sequence[str], None] = 'e730f1c7a51e'
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Upgrade schema."""
23
+ op.create_table(
24
+ 'job_requirements',
25
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
26
+ sa.Column('application_id', sa.Integer(), nullable=False),
27
+ sa.Column('profile_id', sa.Integer(), nullable=False),
28
+ sa.Column('skill_name', sa.String(length=255), nullable=False),
29
+ sa.Column('required_level', sa.String(length=50), nullable=False),
30
+ sa.Column('severity', sa.String(length=50), nullable=False),
31
+ sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
32
+ sa.ForeignKeyConstraint(['application_id'], ['applications.id'], ),
33
+ sa.ForeignKeyConstraint(['profile_id'], ['profiles.id'], ),
34
+ sa.PrimaryKeyConstraint('id'),
35
+ )
36
+ op.create_index(op.f('ix_job_requirements_application_id'), 'job_requirements', ['application_id'], unique=False)
37
+ op.create_index(op.f('ix_job_requirements_profile_id'), 'job_requirements', ['profile_id'], unique=False)
38
+
39
+
40
+ def downgrade() -> None:
41
+ """Downgrade schema."""
42
+ op.drop_index(op.f('ix_job_requirements_profile_id'), table_name='job_requirements')
43
+ op.drop_index(op.f('ix_job_requirements_application_id'), table_name='job_requirements')
44
+ op.drop_table('job_requirements')
@@ -0,0 +1,63 @@
1
+ """add score_breakdown, dream_companies, market_refresh, schedule columns
2
+
3
+ Revision ID: a7b8c9d0e1f2
4
+ Revises: f1a2b3c4d5e6
5
+ Create Date: 2026-03-13 23:00:00.000000
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ import sqlalchemy as sa
11
+ from alembic import op
12
+
13
+ # revision identifiers, used by Alembic.
14
+ revision: str = "a7b8c9d0e1f2"
15
+ down_revision: Union[str, Sequence[str], None] = "f1a2b3c4d5e6"
16
+ branch_labels: Union[str, Sequence[str], None] = None
17
+ depends_on: Union[str, Sequence[str], None] = None
18
+
19
+
20
+ def upgrade() -> None:
21
+ # Fix 1: Add score_breakdown JSON column to scored_jobs
22
+ with op.batch_alter_table("scored_jobs") as batch_op:
23
+ batch_op.add_column(
24
+ sa.Column("score_breakdown", sa.Text(), nullable=True)
25
+ )
26
+
27
+ # Fix 4: Add dream_companies JSON column to profiles
28
+ with op.batch_alter_table("profiles") as batch_op:
29
+ batch_op.add_column(
30
+ sa.Column("dream_companies", sa.Text(), nullable=True)
31
+ )
32
+
33
+ # Fix 5: Add last_market_refreshed_at to profiles
34
+ with op.batch_alter_table("profiles") as batch_op:
35
+ batch_op.add_column(
36
+ sa.Column(
37
+ "last_market_refreshed_at",
38
+ sa.DateTime(timezone=True),
39
+ nullable=True,
40
+ )
41
+ )
42
+
43
+ # Fix 7: Add cadence and next_run columns to search_profiles
44
+ with op.batch_alter_table("search_profiles") as batch_op:
45
+ batch_op.add_column(
46
+ sa.Column("cadence", sa.String(50), nullable=True)
47
+ )
48
+ batch_op.add_column(
49
+ sa.Column("next_run", sa.DateTime(timezone=True), nullable=True)
50
+ )
51
+
52
+
53
+ def downgrade() -> None:
54
+ with op.batch_alter_table("search_profiles") as batch_op:
55
+ batch_op.drop_column("next_run")
56
+ batch_op.drop_column("cadence")
57
+
58
+ with op.batch_alter_table("profiles") as batch_op:
59
+ batch_op.drop_column("last_market_refreshed_at")
60
+ batch_op.drop_column("dream_companies")
61
+
62
+ with op.batch_alter_table("scored_jobs") as batch_op:
63
+ batch_op.drop_column("score_breakdown")
@@ -0,0 +1,36 @@
1
+ """add gap_id and started_at to learning_resources
2
+
3
+ Revision ID: b192ca99adc9
4
+ Revises: a1b2c3d4e5f6
5
+ Create Date: 2026-03-13 15:25:01.704693
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = 'b192ca99adc9'
16
+ down_revision: Union[str, Sequence[str], None] = 'a1b2c3d4e5f6'
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Upgrade schema."""
23
+ with op.batch_alter_table('learning_resources', schema=None) as batch_op:
24
+ batch_op.add_column(sa.Column('gap_id', sa.Integer(), nullable=True))
25
+ batch_op.add_column(sa.Column('started_at', sa.DateTime(timezone=True), nullable=True))
26
+ batch_op.create_index(batch_op.f('ix_learning_resources_gap_id'), ['gap_id'], unique=False)
27
+ batch_op.create_foreign_key('fk_learning_resources_gap_id', 'job_requirements', ['gap_id'], ['id'])
28
+
29
+
30
+ def downgrade() -> None:
31
+ """Downgrade schema."""
32
+ with op.batch_alter_table('learning_resources', schema=None) as batch_op:
33
+ batch_op.drop_constraint('fk_learning_resources_gap_id', type_='foreignkey')
34
+ batch_op.drop_index(batch_op.f('ix_learning_resources_gap_id'))
35
+ batch_op.drop_column('started_at')
36
+ batch_op.drop_column('gap_id')