Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# Imports 

2 

3from configparser import ConfigParser 

4from datetime import datetime 

5from importlib import import_module 

6import logging 

7import os 

8from superdjango.interfaces.cli.constants import OUTPUT_FORMAT 

9from superdjango.interfaces.cli.library import Table 

10from .constants import DEFAULT_INTERVAL, FREQUENCIES, FREQUENCY_CHOICES, STATUS 

11 

12log = logging.getLogger(__name__) 

13 

14# Exports 

15 

16__all__ = ( 

17 "factory", 

18 "Job", 

19 "Result", 

20 "Schedule", 

21) 

22 

23# Functions 

24 

25 

26def factory(path, label=None): 

27 """Initialize a schedule from the given path. 

28 

29 :param path: The path to the ``scheduler.ini`` file. 

30 :type path: str 

31 

32 :param label: The label for the schedule. Defaults to the path with separators changed to dashes. 

33 :type label: str 

34 

35 """ 

36 _label = label or path.replace(os.sep, "-") 

37 if not os.path.exists(path): 

38 log.error("Configuration file does not exist: %s" % path) 

39 return None 

40 

41 log.debug("Loading configuration file: %s" % path) 

42 ini = ConfigParser() 

43 ini.read(path) 

44 

45 jobs = list() 

46 for section in ini.sections(): 

47 error = False 

48 kwargs = dict() 

49 

50 label = section 

51 if ":" in section: 

52 app_name, label = section.split(":") 

53 kwargs['app_name'] = app_name 

54 

55 for key, value in ini.items(section): 

56 if key == "active": 

57 if value.lower() in ("false", "no", "off"): 

58 value = False 

59 else: 

60 value = True 

61 

62 kwargs[key] = value 

63 elif key in ("call", "callback", "do", "run"): 

64 kwargs['callback'] = value 

65 elif key == "every": 

66 tokens = value.split(" ") 

67 try: 

68 frequency = tokens[1] 

69 except IndexError: 

70 frequency = value 

71 

72 if frequency not in FREQUENCY_CHOICES: 

73 log.warning("Unrecognized frequency for %s: %s" % (section, frequency)) 

74 error = True 

75 continue 

76 

77 kwargs['frequency'] = frequency 

78 

79 try: 

80 interval = int(tokens[0]) 

81 # Default interval never triggered because of frequency checking above. 

82 # except IndexError: 

83 # interval = DEFAULT_INTERVAL 

84 except ValueError: 

85 error = True 

86 log.warning("Interval must be given as an integer for: %s" % section) 

87 continue 

88 

89 kwargs['interval'] = interval 

90 elif key == "pk": 

91 kwargs['pk'] = int(value) 

92 else: 

93 kwargs[key] = value 

94 

95 if not error: 

96 job = Job(label, **kwargs) 

97 jobs.append(job) 

98 

99 schedule = Schedule(label=_label) 

100 schedule.jobs = jobs 

101 

102 return schedule 

103 

104 

105# Classes 

106 

107 

108class Job(object): 

109 """A scheduled job.""" 

110 

111 def __init__(self, label, active=True, app_name=None, at=None, callback=None, description=None, 

112 frequency=FREQUENCIES.MINUTE, interval=DEFAULT_INTERVAL, pk=None, **kwargs): 

113 """Initialize a job. 

114 

115 :param label: The name or label for the job. 

116 :type label: str 

117 

118 :param active: Indicates the job is to be executed. 

119 :type active: bool 

120 

121 :param app_name: The app from which the job originates. 

122 :type app_name: str 

123 

124 :param at: The specific time at which job should run. 

125 :type at: str 

126 

127 :param callback: The dotted path to the callback for job execution. May also be provided as a callable. 

128 

129 :param description: Optional, additional description of the job. 

130 :type description: str 

131 

132 :param frequency: The frequency upon which the job runs. 

133 :type frequency: str 

134 

135 :param interval: The interval upon which the job runs. 

136 :type interval: int 

137 

138 :param pk: The primary key of the job record associated with this job. Only used when auto-discovery is enabled. 

139 :type pk: int 

140 

141 ``kwargs`` are passed as parameters to the callback. 

142 

143 """ 

144 self.active = active 

145 self.app_name = app_name 

146 self.at = at 

147 self.callback = callback 

148 self.description = description 

149 self.frequency = frequency 

150 self.interval = interval 

151 self.label = label 

152 self.parameters = kwargs 

153 self.pk = pk 

154 

155 def __repr__(self): 

156 return "<%s %s>" % (self.__class__.__name__, self.label) 

157 

158 @property 

159 def every(self): 

160 """Recombines interval and frequency. 

161 

162 :rtype: str 

163 

164 """ 

165 return "%s %s" % (self.interval, self.frequency) 

166 

167 def get_callback(self): 

168 """Alias for ``_import_callback()``.""" 

169 if callable(self.callback): 

170 return self.callback 

171 

172 return self._import_callback() 

173 

174 def run(self): 

175 """Execute the job. 

176 

177 :rtype: Result 

178 

179 """ 

180 callback = self.get_callback() 

181 if callback is None: 

182 return Result(STATUS.CRITICAL, label=self.label, output="Callback not found: %s" % self.callback) 

183 

184 start_dt = datetime.now() 

185 

186 try: 

187 result = callback(pk=self.pk, **self.parameters) 

188 except Exception as e: 

189 result = Result( 

190 STATUS.FAILURE, 

191 message="The job has failed to properly execute.", 

192 output=str(e) 

193 ) 

194 

195 end_dt = datetime.now() 

196 

197 result.label = self.label 

198 result.start_dt = start_dt 

199 result.end_dt = end_dt 

200 

201 # if self.pk is not None: 

202 # try: 

203 # _job = ScheduledJob.objects.get(pk=self.pk) 

204 # except ScheduledJob.DoesNotExist: 

205 # log.warning("Job record (%s) does not exist for job: %s" % (self.pk, self.label)) 

206 

207 return result 

208 

209 def to_ini(self): 

210 """Export the job to INI format. 

211 

212 :rtype: str 

213 

214 """ 

215 a = list() 

216 

217 if self.app_name: 

218 a.append("[%s:%s]" % (self.app_name, self.label)) 

219 else: 

220 a.append("[%s]" % self.label) 

221 

222 if not self.active: 

223 a.append("active = no") 

224 

225 if self.at: 

226 a.append("at = %s" % self.at) 

227 

228 if self.callback: 

229 if callable(self.callback): 

230 if self.app_name: 

231 a.append("call = %s.scheduler.%s" % (self.app_name, self.callback.__name__)) 

232 else: 

233 a.append("call = %s" % self.callback) 

234 

235 if self.description: 

236 a.append("description = %s" % self.description) 

237 

238 a.append("every = %s %s" % (self.interval, self.frequency)) 

239 

240 if self.pk: 

241 a.append("pk = %s" % self.pk) 

242 

243 if self.parameters: 

244 for key, value in self.parameters.items(): 

245 a.append("%s = %s" % (key, value)) 

246 

247 a.append("") 

248 

249 return "\n".join(a) 

250 

251 def to_markdown(self): 

252 """Export the job to Markdown format. 

253 

254 :rtype: str 

255 

256 """ 

257 a = list() 

258 a.append("### %s" % self.label) 

259 a.append("") 

260 

261 if self.description: 

262 a.append(self.description) 

263 a.append("") 

264 

265 if self.active: 

266 active = "yes" 

267 else: 

268 active = "no" 

269 

270 a.append("- Active: %s" % active) 

271 

272 a.append("- Callback: %s" % self.callback) 

273 

274 if self.interval: 

275 a.append("- Interval: %s" % self.interval) 

276 

277 if self.frequency: 

278 a.append("- Frequency: %s" % self.frequency) 

279 

280 if self.at: 

281 a.append("- At: %s" % self.at) 

282 

283 a.append("") 

284 

285 return "\n".join(a) 

286 

287 def to_plain(self): 

288 """Export the job to plain text format. 

289 

290 :rtype: str 

291 

292 """ 

293 a = list() 

294 a.append("Job: %s" % self.label) 

295 

296 if self.description: 

297 a.append("Description: %s" % self.description) 

298 

299 if self.active: 

300 active = "yes" 

301 else: 

302 active = "no" 

303 

304 a.append("Active: %s" % active) 

305 

306 a.append("Callback: %s" % self.callback) 

307 

308 if self.interval: 

309 a.append("Interval: %s" % self.interval) 

310 

311 if self.frequency: 

312 a.append("Frequency: %s" % self.frequency) 

313 

314 if self.at: 

315 a.append("At: %s" % self.at) 

316 

317 a.append("-" * 120) 

318 

319 return "\n".join(a) 

320 

321 def to_rst(self): 

322 """Export the job to ReStructuredText format. 

323 

324 :rtype: str 

325 

326 """ 

327 a = list() 

328 a.append(self.label) 

329 a.append("-" * len(self.label)) 

330 a.append("") 

331 

332 if self.description: 

333 a.append(self.description) 

334 a.append("") 

335 

336 if self.active: 

337 active = "yes" 

338 else: 

339 active = "no" 

340 

341 a.append("- Active: %s" % active) 

342 

343 a.append("- Callback: %s" % self.callback) 

344 

345 if self.interval: 

346 a.append("- Interval: %s" % self.interval) 

347 

348 if self.frequency: 

349 a.append("- Frequency: %s" % self.frequency) 

350 

351 if self.at: 

352 a.append("- At: %s" % self.at) 

353 

354 a.append("") 

355 

356 return "\n".join(a) 

357 

358 def _import_callback(self): 

359 """Import the callback for the job. 

360 

361 :returns: A callable or ``None`` if the callback could not be imported. 

362 

363 """ 

364 tokens = self.callback.split(".") 

365 callback = tokens.pop(-1) 

366 

367 target = ".".join(tokens) 

368 

369 try: 

370 module = import_module(target) 

371 try: 

372 return getattr(module, callback) 

373 except AttributeError: 

374 log.error("Callback does not exist in %s module: %s" % (target, callback)) 

375 return None 

376 except ImportError as e: 

377 log.error("Failed to import callback %s for %s: %s" % (self.callback, self.label, e)) 

378 return None 

379 

380 

381class Result(object): 

382 """The result of a scheduled job.""" 

383 

384 def __init__(self, status, end_dt=None, label=None, message=None, output=None, start_dt=None): 

385 """Initialize a result. 

386 

387 :param status: The status of the execution. 

388 :type status: str 

389 

390 :param end_dt: The date and time the job completed. Defaults to now. 

391 :type end_dt: datetime 

392 

393 :param label: The job label. 

394 :type label: str 

395 

396 :param message: The human-friendly message. 

397 :type message: str 

398 

399 :param output: The output, if any, produced by the job. 

400 :type output: str 

401 

402 :param start_dt: The date and time the job started. Defaults to now. 

403 :type start_dt: datetime 

404 

405 """ 

406 self.label = label 

407 self.message = message 

408 self.output = output 

409 self.status = status 

410 

411 current_dt = datetime.now() 

412 self.end_dt = end_dt or current_dt 

413 self.start_dt = start_dt or current_dt 

414 

415 @property 

416 def elapsed_time(self): 

417 """The amount of time that passed to execute the job. 

418 

419 :rtype: timedelta 

420 

421 """ 

422 return self.end_dt - self.start_dt 

423 

424 @property 

425 def failure(self): 

426 """Indicates the job failed to properly execute. 

427 

428 :rtype: bool 

429 

430 """ 

431 return self.status is not STATUS.SUCCESS 

432 

433 @property 

434 def success(self): 

435 """Indicates the job was successfully executed. 

436 

437 :rtype: bool 

438 

439 """ 

440 return self.status == STATUS.SUCCESS 

441 

442 

443class Schedule(object): 

444 """A collection of scheduled jobs.""" 

445 

446 def __init__(self, label=None): 

447 self.jobs = list() 

448 self.label = label 

449 

450 # def __init__(self, path, label=None): 

451 # """Initialize a schedule. 

452 # 

453 # :param path: The path to the ``schedule.ini`` file. 

454 # :type path: str 

455 # 

456 # :param label: The label for the schedule. Defaults to the path with separators changed to dashes. 

457 # :type label: str 

458 # 

459 # """ 

460 # self.is_loaded = False 

461 # self.jobs = list() 

462 # self.label = label or path.replace(os.sep, "-") 

463 # self.path = path 

464 

465 def __iter__(self): 

466 return iter(self.jobs) 

467 

468 def __len__(self): 

469 return len(self.jobs) 

470 

471 # def load(self): 

472 # """Load the schedule. 

473 # 

474 # :rtype: bool 

475 # 

476 # """ 

477 # if not os.path.exists(self.path): 

478 # log.critical("Configuration file does not exist: %s" % self.path) 

479 # return False 

480 # 

481 # log.debug("Loading configuration file: %s" % self.path) 

482 # ini = ConfigParser() 

483 # ini.read(self.path) 

484 # 

485 # for section in ini.sections(): 

486 # error = False 

487 # kwargs = dict() 

488 # for key, value in ini.items(section): 

489 # if key in ("call", "callback", "do", "run"): 

490 # kwargs['callback'] = value 

491 # elif key == "active": 

492 # if value.lower() in ("false", "no", "off"): 

493 # value = False 

494 # else: 

495 # value = True 

496 # 

497 # kwargs[key] = value 

498 # elif key == "every": 

499 # tokens = value.split(" ") 

500 # try: 

501 # frequency = tokens[1] 

502 # interval = int(tokens[0]) 

503 # except IndexError: 

504 # frequency = value 

505 # interval = DEFAULT_INTERVAL 

506 # 

507 # if frequency not in FREQUENCIES: 

508 # log.warning("Unrecognized frequency for %s: %s" % (section, frequency)) 

509 # error = True 

510 # continue 

511 # 

512 # kwargs['frequency'] = frequency 

513 # kwargs['interval'] = interval 

514 # else: 

515 # kwargs[key] = value 

516 # 

517 # if not error: 

518 # job = Job(section, **kwargs) 

519 # self.jobs.append(job) 

520 # 

521 # self.is_loaded = len(self.jobs) > 0 

522 # 

523 # return self.is_loaded 

524 

525 def to_ini(self): 

526 """Export all jobs to INI format. 

527 

528 :rtype: str 

529 

530 """ 

531 a = list() 

532 a.append("; %s" % self.label) 

533 a.append("") 

534 for job in self.jobs: 

535 a.append(job.to_ini()) 

536 

537 return "\n".join(a) 

538 

539 def to_markdown(self, tabular=False): 

540 """Export the schedule as Markdown. 

541 

542 :param tabular: Indicates the expected output is in table form. 

543 :type tabular: bool 

544 

545 :rtype: str 

546 

547 """ 

548 a = list() 

549 a.append("## %s" % self.label) 

550 a.append("") 

551 

552 if tabular: 

553 table = self._get_table(OUTPUT_FORMAT.MARKDOWN) 

554 a.append(str(table)) 

555 else: 

556 for job in self.jobs: 

557 a.append(job.to_markdown()) 

558 

559 a.append("") 

560 

561 return "\n".join(a) 

562 

563 def to_plain(self, tabular=False): 

564 """Export the schedule as plain text. 

565 

566 :param tabular: Indicates the expected output is in table form. 

567 :type tabular: bool 

568 

569 :rtype: str 

570 

571 """ 

572 a = list() 

573 a.append(self.label) 

574 a.append("") 

575 

576 if tabular: 

577 table = self._get_table(OUTPUT_FORMAT.SIMPLE) 

578 a.append(str(table)) 

579 else: 

580 for job in self.jobs: 

581 a.append(job.to_plain()) 

582 

583 a.append("") 

584 

585 return "\n".join(a) 

586 

587 def to_rst(self, tabular=False): 

588 """Export the schedule as ReStructuredText. 

589 

590 :param tabular: Indicates the expected output is in table form. 

591 :type tabular: bool 

592 

593 :rtype: str 

594 

595 """ 

596 a = list() 

597 a.append(self.label) 

598 a.append("=" * len(self.label)) 

599 a.append("") 

600 

601 if tabular: 

602 table = self._get_table(OUTPUT_FORMAT.RST) 

603 a.append(str(table)) 

604 else: 

605 for job in self.jobs: 

606 a.append(job.to_rst()) 

607 

608 a.append("") 

609 

610 return "\n".join(a) 

611 

612 def _get_table(self, formatting): 

613 """Get the table. 

614 

615 :param formatting: The expected formatting of the table. 

616 :type formatting: str 

617 

618 :rtype: superdjango.interfaces.cli.library.Table 

619 

620 """ 

621 if formatting in ("md", "markdown"): 

622 formatting = "pipe" 

623 

624 headings = [ 

625 "Label", 

626 "Interval", 

627 "Frequency", 

628 "Active", 

629 "Callback", 

630 ] 

631 table = Table(headings, formatting=formatting) 

632 for job in self.jobs: 

633 table.add([ 

634 job.label, 

635 job.interval, 

636 job.frequency, 

637 job.active, 

638 job.callback, 

639 ]) 

640 

641 return table