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.
- kestrel_app-0.1.0/.env.example +53 -0
- kestrel_app-0.1.0/LICENSE +21 -0
- kestrel_app-0.1.0/MANIFEST.in +5 -0
- kestrel_app-0.1.0/PKG-INFO +28 -0
- kestrel_app-0.1.0/README.md +108 -0
- kestrel_app-0.1.0/alembic/env.py +68 -0
- kestrel_app-0.1.0/alembic/script.py.mako +28 -0
- kestrel_app-0.1.0/alembic/versions/07cca291f71f_add_saved_searches_table.py +47 -0
- kestrel_app-0.1.0/alembic/versions/775c5d68cc2d_add_discovery_models_discovered_jobs_.py +114 -0
- kestrel_app-0.1.0/alembic/versions/a1b2c3d4e5f6_add_job_requirements_table.py +44 -0
- kestrel_app-0.1.0/alembic/versions/a7b8c9d0e1f2_add_scoring_breakdown_market_schedule.py +63 -0
- kestrel_app-0.1.0/alembic/versions/b192ca99adc9_add_gap_id_and_started_at_to_learning_.py +36 -0
- kestrel_app-0.1.0/alembic/versions/b3c4d5e6f7a8_add_ondelete_set_null_to_discovery_run_fk.py +66 -0
- kestrel_app-0.1.0/alembic/versions/c0b4dc9ab23a_initial_schema_profiles_applications_.py +132 -0
- kestrel_app-0.1.0/alembic/versions/c4d5e6f7a8b9_add_interview_prep_tables.py +108 -0
- kestrel_app-0.1.0/alembic/versions/d5e6f7a8b9c0_add_star_stories_table.py +58 -0
- kestrel_app-0.1.0/alembic/versions/e6f7a8b9c0d1_add_company_research_reports_table.py +61 -0
- kestrel_app-0.1.0/alembic/versions/e730f1c7a51e_add_skills_intelligence_models.py +139 -0
- kestrel_app-0.1.0/alembic/versions/e8b54d7d5d6a_add_scoring_engine_models_scoring_.py +85 -0
- kestrel_app-0.1.0/alembic/versions/eac8c4fc464e_add_parent_event_id_to_calendar_events.py +37 -0
- kestrel_app-0.1.0/alembic/versions/f1a2b3c4d5e6_add_profile_id_to_dedup_constraint.py +46 -0
- kestrel_app-0.1.0/alembic/versions/f7a8b9c0d1e2_add_integration_configs_table.py +54 -0
- kestrel_app-0.1.0/alembic/versions/g8b9c0d1e2f3_add_ticktick_sync_tasks_table.py +64 -0
- kestrel_app-0.1.0/alembic/versions/h9c0d1e2f3g4_add_calendar_events_table.py +82 -0
- kestrel_app-0.1.0/alembic/versions/i0d1e2f3g4h5_add_time_sessions_table.py +50 -0
- kestrel_app-0.1.0/alembic/versions/j1e2f3g4h5i6_add_pushover_notification_tables.py +86 -0
- kestrel_app-0.1.0/alembic/versions/k2f3g4h5i6j7_add_voice_session_tables.py +57 -0
- kestrel_app-0.1.0/alembic.ini +151 -0
- kestrel_app-0.1.0/config/personal.yaml.example +26 -0
- kestrel_app-0.1.0/frontend/dist/assets/index-DH_vCftM.js +50 -0
- kestrel_app-0.1.0/frontend/dist/assets/index-Qt6RZzNz.css +2 -0
- kestrel_app-0.1.0/frontend/dist/index.html +13 -0
- kestrel_app-0.1.0/pyproject.toml +76 -0
- kestrel_app-0.1.0/setup.cfg +4 -0
- kestrel_app-0.1.0/src/career_os/__init__.py +3 -0
- kestrel_app-0.1.0/src/career_os/__main__.py +5 -0
- kestrel_app-0.1.0/src/career_os/_alembic/alembic/env.py +68 -0
- kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/07cca291f71f_add_saved_searches_table.py +47 -0
- kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/775c5d68cc2d_add_discovery_models_discovered_jobs_.py +114 -0
- kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/a1b2c3d4e5f6_add_job_requirements_table.py +44 -0
- kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/a7b8c9d0e1f2_add_scoring_breakdown_market_schedule.py +63 -0
- kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/b192ca99adc9_add_gap_id_and_started_at_to_learning_.py +36 -0
- kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/b3c4d5e6f7a8_add_ondelete_set_null_to_discovery_run_fk.py +66 -0
- kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/c0b4dc9ab23a_initial_schema_profiles_applications_.py +132 -0
- kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/c4d5e6f7a8b9_add_interview_prep_tables.py +108 -0
- kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/d5e6f7a8b9c0_add_star_stories_table.py +58 -0
- kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/e6f7a8b9c0d1_add_company_research_reports_table.py +61 -0
- kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/e730f1c7a51e_add_skills_intelligence_models.py +139 -0
- kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/e8b54d7d5d6a_add_scoring_engine_models_scoring_.py +85 -0
- kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/eac8c4fc464e_add_parent_event_id_to_calendar_events.py +37 -0
- kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/f1a2b3c4d5e6_add_profile_id_to_dedup_constraint.py +46 -0
- kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/f7a8b9c0d1e2_add_integration_configs_table.py +54 -0
- kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/g8b9c0d1e2f3_add_ticktick_sync_tasks_table.py +64 -0
- kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/h9c0d1e2f3g4_add_calendar_events_table.py +82 -0
- kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/i0d1e2f3g4h5_add_time_sessions_table.py +50 -0
- kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/j1e2f3g4h5i6_add_pushover_notification_tables.py +86 -0
- kestrel_app-0.1.0/src/career_os/_alembic/alembic/versions/k2f3g4h5i6j7_add_voice_session_tables.py +57 -0
- kestrel_app-0.1.0/src/career_os/_alembic/env.py +68 -0
- kestrel_app-0.1.0/src/career_os/_alembic/script.py.mako +28 -0
- kestrel_app-0.1.0/src/career_os/_alembic/versions/07cca291f71f_add_saved_searches_table.py +55 -0
- kestrel_app-0.1.0/src/career_os/_alembic/versions/775c5d68cc2d_add_discovery_models_discovered_jobs_.py +155 -0
- kestrel_app-0.1.0/src/career_os/_alembic/versions/a1b2c3d4e5f6_add_job_requirements_table.py +59 -0
- kestrel_app-0.1.0/src/career_os/_alembic/versions/a7b8c9d0e1f2_add_scoring_breakdown_market_schedule.py +57 -0
- kestrel_app-0.1.0/src/career_os/_alembic/versions/b192ca99adc9_add_gap_id_and_started_at_to_learning_.py +40 -0
- kestrel_app-0.1.0/src/career_os/_alembic/versions/b3c4d5e6f7a8_add_ondelete_set_null_to_discovery_run_fk.py +67 -0
- kestrel_app-0.1.0/src/career_os/_alembic/versions/c0b4dc9ab23a_initial_schema_profiles_applications_.py +172 -0
- kestrel_app-0.1.0/src/career_os/_alembic/versions/c4d5e6f7a8b9_add_interview_prep_tables.py +109 -0
- kestrel_app-0.1.0/src/career_os/_alembic/versions/d5e6f7a8b9c0_add_star_stories_table.py +59 -0
- kestrel_app-0.1.0/src/career_os/_alembic/versions/e6f7a8b9c0d1_add_company_research_reports_table.py +62 -0
- kestrel_app-0.1.0/src/career_os/_alembic/versions/e730f1c7a51e_add_skills_intelligence_models.py +175 -0
- kestrel_app-0.1.0/src/career_os/_alembic/versions/e8b54d7d5d6a_add_scoring_engine_models_scoring_.py +107 -0
- kestrel_app-0.1.0/src/career_os/_alembic/versions/eac8c4fc464e_add_parent_event_id_to_calendar_events.py +44 -0
- kestrel_app-0.1.0/src/career_os/_alembic/versions/f1a2b3c4d5e6_add_profile_id_to_dedup_constraint.py +48 -0
- kestrel_app-0.1.0/src/career_os/_alembic/versions/f7a8b9c0d1e2_add_integration_configs_table.py +55 -0
- kestrel_app-0.1.0/src/career_os/_alembic/versions/g8b9c0d1e2f3_add_ticktick_sync_tasks_table.py +65 -0
- kestrel_app-0.1.0/src/career_os/_alembic/versions/h9c0d1e2f3g4_add_calendar_events_table.py +83 -0
- kestrel_app-0.1.0/src/career_os/_alembic/versions/i0d1e2f3g4h5_add_time_sessions_table.py +51 -0
- kestrel_app-0.1.0/src/career_os/_alembic/versions/j1e2f3g4h5i6_add_pushover_notification_tables.py +87 -0
- kestrel_app-0.1.0/src/career_os/_alembic/versions/k2f3g4h5i6j7_add_voice_session_tables.py +58 -0
- kestrel_app-0.1.0/src/career_os/_alembic.ini +151 -0
- kestrel_app-0.1.0/src/career_os/_frontend_dist/assets/index-DhdrttW4.css +2 -0
- kestrel_app-0.1.0/src/career_os/_frontend_dist/assets/index-KDnWhwGY.js +50 -0
- kestrel_app-0.1.0/src/career_os/_frontend_dist/dist/assets/index-DH_vCftM.js +50 -0
- kestrel_app-0.1.0/src/career_os/_frontend_dist/dist/assets/index-Qt6RZzNz.css +2 -0
- kestrel_app-0.1.0/src/career_os/_frontend_dist/dist/index.html +13 -0
- kestrel_app-0.1.0/src/career_os/_frontend_dist/index.html +13 -0
- kestrel_app-0.1.0/src/career_os/ai/__init__.py +14 -0
- kestrel_app-0.1.0/src/career_os/ai/base.py +59 -0
- kestrel_app-0.1.0/src/career_os/ai/factory.py +48 -0
- kestrel_app-0.1.0/src/career_os/ai/mock_provider.py +1664 -0
- kestrel_app-0.1.0/src/career_os/ai/openrouter_provider.py +229 -0
- kestrel_app-0.1.0/src/career_os/api/__init__.py +1 -0
- kestrel_app-0.1.0/src/career_os/api/ai.py +86 -0
- kestrel_app-0.1.0/src/career_os/api/analytics.py +24 -0
- kestrel_app-0.1.0/src/career_os/api/applications.py +230 -0
- kestrel_app-0.1.0/src/career_os/api/calendar.py +263 -0
- kestrel_app-0.1.0/src/career_os/api/coaching.py +61 -0
- kestrel_app-0.1.0/src/career_os/api/contacts.py +274 -0
- kestrel_app-0.1.0/src/career_os/api/discovery.py +191 -0
- kestrel_app-0.1.0/src/career_os/api/follow_ups.py +123 -0
- kestrel_app-0.1.0/src/career_os/api/gaps.py +152 -0
- kestrel_app-0.1.0/src/career_os/api/goals.py +205 -0
- kestrel_app-0.1.0/src/career_os/api/integrations.py +69 -0
- kestrel_app-0.1.0/src/career_os/api/intelligence.py +136 -0
- kestrel_app-0.1.0/src/career_os/api/interview_prep.py +111 -0
- kestrel_app-0.1.0/src/career_os/api/jobs.py +204 -0
- kestrel_app-0.1.0/src/career_os/api/learning.py +139 -0
- kestrel_app-0.1.0/src/career_os/api/market.py +190 -0
- kestrel_app-0.1.0/src/career_os/api/profiles.py +126 -0
- kestrel_app-0.1.0/src/career_os/api/pushover.py +173 -0
- kestrel_app-0.1.0/src/career_os/api/research.py +74 -0
- kestrel_app-0.1.0/src/career_os/api/scoring.py +195 -0
- kestrel_app-0.1.0/src/career_os/api/skills.py +184 -0
- kestrel_app-0.1.0/src/career_os/api/star_stories.py +164 -0
- kestrel_app-0.1.0/src/career_os/api/ticktick.py +169 -0
- kestrel_app-0.1.0/src/career_os/api/timingsapp.py +169 -0
- kestrel_app-0.1.0/src/career_os/api/voice.py +152 -0
- kestrel_app-0.1.0/src/career_os/cli/__init__.py +1 -0
- kestrel_app-0.1.0/src/career_os/cli/contacts.py +431 -0
- kestrel_app-0.1.0/src/career_os/cli/main.py +2190 -0
- kestrel_app-0.1.0/src/career_os/config.py +58 -0
- kestrel_app-0.1.0/src/career_os/database.py +63 -0
- kestrel_app-0.1.0/src/career_os/discovery/__init__.py +5 -0
- kestrel_app-0.1.0/src/career_os/discovery/adapters.py +409 -0
- kestrel_app-0.1.0/src/career_os/discovery/scheduler.py +101 -0
- kestrel_app-0.1.0/src/career_os/main.py +248 -0
- kestrel_app-0.1.0/src/career_os/middleware.py +48 -0
- kestrel_app-0.1.0/src/career_os/migration/__init__.py +1 -0
- kestrel_app-0.1.0/src/career_os/migration/csv_import.py +251 -0
- kestrel_app-0.1.0/src/career_os/migration/link_packages.py +193 -0
- kestrel_app-0.1.0/src/career_os/migration/run_migration.py +107 -0
- kestrel_app-0.1.0/src/career_os/migration/seed.py +107 -0
- kestrel_app-0.1.0/src/career_os/models/__init__.py +73 -0
- kestrel_app-0.1.0/src/career_os/models/calendar.py +78 -0
- kestrel_app-0.1.0/src/career_os/models/company_research.py +53 -0
- kestrel_app-0.1.0/src/career_os/models/contacts.py +123 -0
- kestrel_app-0.1.0/src/career_os/models/discovery.py +178 -0
- kestrel_app-0.1.0/src/career_os/models/integrations.py +49 -0
- kestrel_app-0.1.0/src/career_os/models/interview_prep.py +96 -0
- kestrel_app-0.1.0/src/career_os/models/models.py +217 -0
- kestrel_app-0.1.0/src/career_os/models/pushover.py +96 -0
- kestrel_app-0.1.0/src/career_os/models/scoring.py +121 -0
- kestrel_app-0.1.0/src/career_os/models/skills.py +208 -0
- kestrel_app-0.1.0/src/career_os/models/star_stories.py +58 -0
- kestrel_app-0.1.0/src/career_os/models/ticktick_sync.py +62 -0
- kestrel_app-0.1.0/src/career_os/models/timingsapp.py +68 -0
- kestrel_app-0.1.0/src/career_os/models/voice.py +77 -0
- kestrel_app-0.1.0/src/career_os/schemas/__init__.py +114 -0
- kestrel_app-0.1.0/src/career_os/schemas/ai.py +205 -0
- kestrel_app-0.1.0/src/career_os/schemas/ai_health.py +43 -0
- kestrel_app-0.1.0/src/career_os/schemas/analytics.py +93 -0
- kestrel_app-0.1.0/src/career_os/schemas/applications.py +258 -0
- kestrel_app-0.1.0/src/career_os/schemas/calendar.py +133 -0
- kestrel_app-0.1.0/src/career_os/schemas/coaching.py +67 -0
- kestrel_app-0.1.0/src/career_os/schemas/contacts.py +361 -0
- kestrel_app-0.1.0/src/career_os/schemas/discovery.py +246 -0
- kestrel_app-0.1.0/src/career_os/schemas/follow_ups.py +79 -0
- kestrel_app-0.1.0/src/career_os/schemas/gaps.py +135 -0
- kestrel_app-0.1.0/src/career_os/schemas/goals.py +189 -0
- kestrel_app-0.1.0/src/career_os/schemas/integrations.py +239 -0
- kestrel_app-0.1.0/src/career_os/schemas/interview_prep.py +114 -0
- kestrel_app-0.1.0/src/career_os/schemas/jobs.py +153 -0
- kestrel_app-0.1.0/src/career_os/schemas/learning.py +138 -0
- kestrel_app-0.1.0/src/career_os/schemas/market.py +199 -0
- kestrel_app-0.1.0/src/career_os/schemas/profiles.py +63 -0
- kestrel_app-0.1.0/src/career_os/schemas/pushover.py +121 -0
- kestrel_app-0.1.0/src/career_os/schemas/research.py +174 -0
- kestrel_app-0.1.0/src/career_os/schemas/role_intelligence.py +128 -0
- kestrel_app-0.1.0/src/career_os/schemas/scoring.py +156 -0
- kestrel_app-0.1.0/src/career_os/schemas/skills.py +158 -0
- kestrel_app-0.1.0/src/career_os/schemas/star_stories.py +128 -0
- kestrel_app-0.1.0/src/career_os/schemas/ticktick.py +65 -0
- kestrel_app-0.1.0/src/career_os/schemas/timingsapp.py +109 -0
- kestrel_app-0.1.0/src/career_os/schemas/voice.py +99 -0
- kestrel_app-0.1.0/src/career_os/services/__init__.py +1 -0
- kestrel_app-0.1.0/src/career_os/services/activity.py +42 -0
- kestrel_app-0.1.0/src/career_os/services/ai_health.py +295 -0
- kestrel_app-0.1.0/src/career_os/services/analytics.py +331 -0
- kestrel_app-0.1.0/src/career_os/services/applications.py +289 -0
- kestrel_app-0.1.0/src/career_os/services/calendar.py +536 -0
- kestrel_app-0.1.0/src/career_os/services/coaching.py +439 -0
- kestrel_app-0.1.0/src/career_os/services/company_research.py +495 -0
- kestrel_app-0.1.0/src/career_os/services/contacts.py +402 -0
- kestrel_app-0.1.0/src/career_os/services/discovery.py +635 -0
- kestrel_app-0.1.0/src/career_os/services/follow_ups.py +297 -0
- kestrel_app-0.1.0/src/career_os/services/gap_analysis.py +425 -0
- kestrel_app-0.1.0/src/career_os/services/goals.py +619 -0
- kestrel_app-0.1.0/src/career_os/services/integrations.py +233 -0
- kestrel_app-0.1.0/src/career_os/services/interview_prep.py +762 -0
- kestrel_app-0.1.0/src/career_os/services/jobs.py +312 -0
- kestrel_app-0.1.0/src/career_os/services/learning.py +354 -0
- kestrel_app-0.1.0/src/career_os/services/market.py +538 -0
- kestrel_app-0.1.0/src/career_os/services/pushover.py +899 -0
- kestrel_app-0.1.0/src/career_os/services/pushover_client.py +146 -0
- kestrel_app-0.1.0/src/career_os/services/role_intelligence.py +520 -0
- kestrel_app-0.1.0/src/career_os/services/salary.py +69 -0
- kestrel_app-0.1.0/src/career_os/services/scoring.py +616 -0
- kestrel_app-0.1.0/src/career_os/services/skills.py +286 -0
- kestrel_app-0.1.0/src/career_os/services/skills_parsing.py +738 -0
- kestrel_app-0.1.0/src/career_os/services/star_stories.py +444 -0
- kestrel_app-0.1.0/src/career_os/services/ticktick_client.py +201 -0
- kestrel_app-0.1.0/src/career_os/services/ticktick_scheduler.py +88 -0
- kestrel_app-0.1.0/src/career_os/services/ticktick_sync.py +588 -0
- kestrel_app-0.1.0/src/career_os/services/timingsapp.py +572 -0
- kestrel_app-0.1.0/src/career_os/services/timingsapp_client.py +188 -0
- kestrel_app-0.1.0/src/career_os/services/voice.py +365 -0
- kestrel_app-0.1.0/src/kestrel_app.egg-info/PKG-INFO +28 -0
- kestrel_app-0.1.0/src/kestrel_app.egg-info/SOURCES.txt +270 -0
- kestrel_app-0.1.0/src/kestrel_app.egg-info/dependency_links.txt +1 -0
- kestrel_app-0.1.0/src/kestrel_app.egg-info/entry_points.txt +3 -0
- kestrel_app-0.1.0/src/kestrel_app.egg-info/requires.txt +22 -0
- kestrel_app-0.1.0/src/kestrel_app.egg-info/top_level.txt +1 -0
- kestrel_app-0.1.0/tests/test_activity_log_extended.py +132 -0
- kestrel_app-0.1.0/tests/test_ai_health.py +295 -0
- kestrel_app-0.1.0/tests/test_ai_provider.py +760 -0
- kestrel_app-0.1.0/tests/test_analytics.py +649 -0
- kestrel_app-0.1.0/tests/test_auth.py +108 -0
- kestrel_app-0.1.0/tests/test_auto_apply.py +1084 -0
- kestrel_app-0.1.0/tests/test_batch_apply_browser.py +1167 -0
- kestrel_app-0.1.0/tests/test_calendar.py +848 -0
- kestrel_app-0.1.0/tests/test_cli.py +55 -0
- kestrel_app-0.1.0/tests/test_cli_contacts.py +274 -0
- kestrel_app-0.1.0/tests/test_cli_discovery.py +633 -0
- kestrel_app-0.1.0/tests/test_cli_pipeline.py +554 -0
- kestrel_app-0.1.0/tests/test_cli_prep.py +672 -0
- kestrel_app-0.1.0/tests/test_cli_skills.py +570 -0
- kestrel_app-0.1.0/tests/test_coaching.py +890 -0
- kestrel_app-0.1.0/tests/test_company_research.py +1109 -0
- kestrel_app-0.1.0/tests/test_contacts.py +437 -0
- kestrel_app-0.1.0/tests/test_contacts_api.py +330 -0
- kestrel_app-0.1.0/tests/test_contacts_integration.py +256 -0
- kestrel_app-0.1.0/tests/test_daily_pipeline.py +306 -0
- kestrel_app-0.1.0/tests/test_dep_security.py +44 -0
- kestrel_app-0.1.0/tests/test_discovery.py +1167 -0
- kestrel_app-0.1.0/tests/test_discovery_salary_fixes.py +672 -0
- kestrel_app-0.1.0/tests/test_docs.py +20 -0
- kestrel_app-0.1.0/tests/test_follow_ups.py +509 -0
- kestrel_app-0.1.0/tests/test_gap_analysis.py +1162 -0
- kestrel_app-0.1.0/tests/test_germany_jobs.py +275 -0
- kestrel_app-0.1.0/tests/test_goals.py +1292 -0
- kestrel_app-0.1.0/tests/test_health.py +17 -0
- kestrel_app-0.1.0/tests/test_html_stripping.py +76 -0
- kestrel_app-0.1.0/tests/test_integrations_config.py +474 -0
- kestrel_app-0.1.0/tests/test_interview_prep.py +1186 -0
- kestrel_app-0.1.0/tests/test_job_scorer.py +577 -0
- kestrel_app-0.1.0/tests/test_kestrel_start.py +103 -0
- kestrel_app-0.1.0/tests/test_learning.py +969 -0
- kestrel_app-0.1.0/tests/test_market.py +809 -0
- kestrel_app-0.1.0/tests/test_md_to_pdf.py +206 -0
- kestrel_app-0.1.0/tests/test_migration.py +837 -0
- kestrel_app-0.1.0/tests/test_new_sources.py +576 -0
- kestrel_app-0.1.0/tests/test_pipeline_api.py +716 -0
- kestrel_app-0.1.0/tests/test_prep_gap_driven.py +574 -0
- kestrel_app-0.1.0/tests/test_profile_scoping.py +498 -0
- kestrel_app-0.1.0/tests/test_profiles_crud.py +528 -0
- kestrel_app-0.1.0/tests/test_pushover.py +1184 -0
- kestrel_app-0.1.0/tests/test_render_tailored_cvs.py +265 -0
- kestrel_app-0.1.0/tests/test_role_intelligence.py +1015 -0
- kestrel_app-0.1.0/tests/test_round2_residuals.py +517 -0
- kestrel_app-0.1.0/tests/test_scoring.py +1496 -0
- kestrel_app-0.1.0/tests/test_scoring_rescore.py +226 -0
- kestrel_app-0.1.0/tests/test_scrape_resilient.py +483 -0
- kestrel_app-0.1.0/tests/test_scraper.py +199 -0
- kestrel_app-0.1.0/tests/test_search_filter.py +744 -0
- kestrel_app-0.1.0/tests/test_skills_api.py +856 -0
- kestrel_app-0.1.0/tests/test_skills_parsing.py +1412 -0
- kestrel_app-0.1.0/tests/test_star_stories.py +1214 -0
- kestrel_app-0.1.0/tests/test_ticktick.py +1403 -0
- kestrel_app-0.1.0/tests/test_timingsapp.py +1140 -0
- kestrel_app-0.1.0/tests/test_ut_cross_flow_fixes.py +565 -0
- kestrel_app-0.1.0/tests/test_ut_prep_salary_fixes.py +696 -0
- 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,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
|
+
[](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')
|