|
5 | 5 | import time |
6 | 6 | import traceback |
7 | 7 | from pathlib import Path |
| 8 | +from threading import Lock |
8 | 9 |
|
9 | 10 | import dotenv |
| 11 | +from pynput import keyboard as pynput_kb |
10 | 12 |
|
11 | | -from config.app_config import CHECK_LAST_SEARCH_TIME |
| 13 | +from config.app_config import RESTART_EVERY_DAY |
12 | 14 | from config.constants import BROWSER_STORAGE_STATE, RESUME_DIR, SEARCH_CONFIG_FILE |
13 | 15 | from config.logger_config import logger |
14 | 16 | from src.job_manager.authenticator import LinkedInAuthenticator |
|
36 | 38 | RESUME_TEXT_FILE = Path(RESUME_DIR) / "resume_text.txt" |
37 | 39 | READY_MADE_RESUME = Path(RESUME_DIR) / "resume.pdf" |
38 | 40 |
|
| 41 | +# Global pause state for keyboard control |
| 42 | +paused = False |
| 43 | +pause_lock = Lock() |
| 44 | +ctrl_pressed = False |
| 45 | + |
39 | 46 |
|
40 | 47 | class ConfigError(Exception): |
41 | 48 | pass |
@@ -101,6 +108,49 @@ def validate_resume_structured(resume_structured_file: Path) -> dict: |
101 | 108 | raise ConfigError(f"Structured resume validation error: {str(e)}") |
102 | 109 |
|
103 | 110 |
|
| 111 | +def on_press(key): |
| 112 | + """Handle key press events""" |
| 113 | + global paused, ctrl_pressed |
| 114 | + try: |
| 115 | + # Track Ctrl key state |
| 116 | + if key in (pynput_kb.Key.ctrl_l, pynput_kb.Key.ctrl_r): |
| 117 | + ctrl_pressed = True |
| 118 | + # Check for 'x' key when Ctrl is pressed |
| 119 | + elif hasattr(key, "char") and key.char == "x" and ctrl_pressed: |
| 120 | + with pause_lock: |
| 121 | + paused = not paused |
| 122 | + if paused: |
| 123 | + logger.warning("⏸️ PAUSED - Press Ctrl+X to continue") |
| 124 | + else: |
| 125 | + logger.info("▶️ RESUMED") |
| 126 | + except AttributeError: |
| 127 | + pass |
| 128 | + |
| 129 | + |
| 130 | +def on_release(key): |
| 131 | + """Handle key release events""" |
| 132 | + global ctrl_pressed |
| 133 | + # Reset Ctrl key state |
| 134 | + if key in (pynput_kb.Key.ctrl_l, pynput_kb.Key.ctrl_r): |
| 135 | + ctrl_pressed = False |
| 136 | + |
| 137 | + |
| 138 | +def start_keyboard_listener(): |
| 139 | + """Start keyboard listener in background thread""" |
| 140 | + listener = pynput_kb.Listener(on_press=on_press, on_release=on_release) |
| 141 | + listener.daemon = True |
| 142 | + listener.start() |
| 143 | + logger.info("Keyboard listener started - Press Ctrl+X to pause/resume") |
| 144 | + |
| 145 | + |
| 146 | +async def check_pause(): |
| 147 | + """Check if execution is paused and wait if needed""" |
| 148 | + global paused |
| 149 | + if paused: |
| 150 | + while paused: |
| 151 | + await asyncio.sleep(0.5) |
| 152 | + |
| 153 | + |
104 | 154 | async def create_and_run_bot( |
105 | 155 | search_config: dict, |
106 | 156 | secrets: dict, |
@@ -170,9 +220,10 @@ async def create_and_run_bot( |
170 | 220 | # Set bot facade |
171 | 221 | bot = BotFacade(resume_anonymizer, search_component, apply_component, llm_agent_component) |
172 | 222 | bot.set_parameters(search_config) |
| 223 | + bot.set_pause_checker(check_pause) |
173 | 224 |
|
174 | 225 | # Check if the last search was less than a day ago |
175 | | - if CHECK_LAST_SEARCH_TIME and not apply_component.check_the_last_search_time(): |
| 226 | + if RESTART_EVERY_DAY and not apply_component.check_the_last_search_time(): |
176 | 227 | logger.warning( |
177 | 228 | "Last search was less than a day ago, finishing work. If you want to restart the search, delete the file data/output/last_run.yaml file" |
178 | 229 | ) |
@@ -206,44 +257,50 @@ async def create_and_run_bot( |
206 | 257 |
|
207 | 258 |
|
208 | 259 | def main() -> None: |
209 | | - try: |
210 | | - # create output folder if it doesn't exist |
211 | | - data = Path("data") |
212 | | - output_folder = data / "output" |
213 | | - output_folder.mkdir(exist_ok=True) |
214 | | - |
215 | | - # validate config files |
216 | | - config_validator = ConfigValidator() |
217 | | - secrets = config_validator.validate_secrets() |
218 | | - search_config = config_validator.validate_search_config(SEARCH_CONFIG_FILE) |
219 | | - resume_text = config_validator.validate_resume_text(RESUME_TEXT_FILE) |
220 | | - resume_structured = config_validator.validate_resume_structured(RESUME_STRUCTURED_FILE) |
221 | | - |
222 | | - logger.info("Starting LinkedIn Job Applier...") |
223 | | - logger.info(f"Search config loaded with {len(search_config)} parameters") |
224 | | - |
225 | | - # Run LinkedIn bot (async) |
226 | | - asyncio.run(create_and_run_bot(search_config, secrets, resume_text, resume_structured)) |
227 | | - logger.info("LinkedIn bot completed successfully") |
228 | | - |
229 | | - # Wait 1 hour total before next run |
230 | | - if CHECK_LAST_SEARCH_TIME: |
231 | | - time.sleep(3600) |
232 | | - |
233 | | - except ConfigError as ce: |
234 | | - logger.error(f"Configuration error: {str(ce)}") |
235 | | - except FileNotFoundError as fnf: |
236 | | - tb_str = traceback.format_exc() |
237 | | - logger.error(f"File not found: {str(fnf)}\n{tb_str}") |
238 | | - except RuntimeError as re: |
239 | | - tb_str = traceback.format_exc() |
240 | | - logger.error(f"Runtime error: {str(re)}\n{tb_str}") |
241 | | - except Exception as e: |
242 | | - tb_str = traceback.format_exc() |
243 | | - logger.error(f"Unknown error: {str(e)}\n{tb_str}") |
244 | | - finally: |
245 | | - logger.info("Program completed") |
246 | | - # time.sleep(600) # Commented out for testing |
| 260 | + # Start keyboard listener for pause/resume functionality |
| 261 | + start_keyboard_listener() |
| 262 | + |
| 263 | + while True: |
| 264 | + try: |
| 265 | + # create output folder if it doesn't exist |
| 266 | + data = Path("data") |
| 267 | + output_folder = data / "output" |
| 268 | + output_folder.mkdir(exist_ok=True) |
| 269 | + |
| 270 | + # validate config files |
| 271 | + config_validator = ConfigValidator() |
| 272 | + secrets = config_validator.validate_secrets() |
| 273 | + search_config = config_validator.validate_search_config(SEARCH_CONFIG_FILE) |
| 274 | + resume_text = config_validator.validate_resume_text(RESUME_TEXT_FILE) |
| 275 | + resume_structured = config_validator.validate_resume_structured(RESUME_STRUCTURED_FILE) |
| 276 | + |
| 277 | + logger.info("Starting LinkedIn Job Applier...") |
| 278 | + logger.info(f"Search config loaded with {len(search_config)} parameters") |
| 279 | + |
| 280 | + # Run LinkedIn bot (async) |
| 281 | + asyncio.run(create_and_run_bot(search_config, secrets, resume_text, resume_structured)) |
| 282 | + logger.info("LinkedIn bot completed successfully") |
| 283 | + |
| 284 | + except ConfigError as ce: |
| 285 | + logger.error(f"Configuration error: {str(ce)}") |
| 286 | + except FileNotFoundError as fnf: |
| 287 | + tb_str = traceback.format_exc() |
| 288 | + logger.error(f"File not found: {str(fnf)}\n{tb_str}") |
| 289 | + except RuntimeError as re: |
| 290 | + tb_str = traceback.format_exc() |
| 291 | + logger.error(f"Runtime error: {str(re)}\n{tb_str}") |
| 292 | + except Exception as e: |
| 293 | + tb_str = traceback.format_exc() |
| 294 | + logger.error(f"Unknown error: {str(e)}\n{tb_str}") |
| 295 | + finally: |
| 296 | + logger.info("Program completed") |
| 297 | + # Wait 1 hour total before next run |
| 298 | + if RESTART_EVERY_DAY: |
| 299 | + logger.info("Waiting 1 hour before next run") |
| 300 | + time.sleep(3600) |
| 301 | + else: |
| 302 | + logger.info("Exiting program") |
| 303 | + break |
247 | 304 |
|
248 | 305 |
|
249 | 306 | if __name__ == "__main__": |
|
0 commit comments