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
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
12log = logging.getLogger(__name__)
14# Exports
16__all__ = (
17 "factory",
18 "Job",
19 "Result",
20 "Schedule",
21)
23# Functions
26def factory(path, label=None):
27 """Initialize a schedule from the given path.
29 :param path: The path to the ``scheduler.ini`` file.
30 :type path: str
32 :param label: The label for the schedule. Defaults to the path with separators changed to dashes.
33 :type label: str
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
41 log.debug("Loading configuration file: %s" % path)
42 ini = ConfigParser()
43 ini.read(path)
45 jobs = list()
46 for section in ini.sections():
47 error = False
48 kwargs = dict()
50 label = section
51 if ":" in section:
52 app_name, label = section.split(":")
53 kwargs['app_name'] = app_name
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
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
72 if frequency not in FREQUENCY_CHOICES:
73 log.warning("Unrecognized frequency for %s: %s" % (section, frequency))
74 error = True
75 continue
77 kwargs['frequency'] = frequency
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
89 kwargs['interval'] = interval
90 elif key == "pk":
91 kwargs['pk'] = int(value)
92 else:
93 kwargs[key] = value
95 if not error:
96 job = Job(label, **kwargs)
97 jobs.append(job)
99 schedule = Schedule(label=_label)
100 schedule.jobs = jobs
102 return schedule
105# Classes
108class Job(object):
109 """A scheduled job."""
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.
115 :param label: The name or label for the job.
116 :type label: str
118 :param active: Indicates the job is to be executed.
119 :type active: bool
121 :param app_name: The app from which the job originates.
122 :type app_name: str
124 :param at: The specific time at which job should run.
125 :type at: str
127 :param callback: The dotted path to the callback for job execution. May also be provided as a callable.
129 :param description: Optional, additional description of the job.
130 :type description: str
132 :param frequency: The frequency upon which the job runs.
133 :type frequency: str
135 :param interval: The interval upon which the job runs.
136 :type interval: int
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
141 ``kwargs`` are passed as parameters to the callback.
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
155 def __repr__(self):
156 return "<%s %s>" % (self.__class__.__name__, self.label)
158 @property
159 def every(self):
160 """Recombines interval and frequency.
162 :rtype: str
164 """
165 return "%s %s" % (self.interval, self.frequency)
167 def get_callback(self):
168 """Alias for ``_import_callback()``."""
169 if callable(self.callback):
170 return self.callback
172 return self._import_callback()
174 def run(self):
175 """Execute the job.
177 :rtype: Result
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)
184 start_dt = datetime.now()
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 )
195 end_dt = datetime.now()
197 result.label = self.label
198 result.start_dt = start_dt
199 result.end_dt = end_dt
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))
207 return result
209 def to_ini(self):
210 """Export the job to INI format.
212 :rtype: str
214 """
215 a = list()
217 if self.app_name:
218 a.append("[%s:%s]" % (self.app_name, self.label))
219 else:
220 a.append("[%s]" % self.label)
222 if not self.active:
223 a.append("active = no")
225 if self.at:
226 a.append("at = %s" % self.at)
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)
235 if self.description:
236 a.append("description = %s" % self.description)
238 a.append("every = %s %s" % (self.interval, self.frequency))
240 if self.pk:
241 a.append("pk = %s" % self.pk)
243 if self.parameters:
244 for key, value in self.parameters.items():
245 a.append("%s = %s" % (key, value))
247 a.append("")
249 return "\n".join(a)
251 def to_markdown(self):
252 """Export the job to Markdown format.
254 :rtype: str
256 """
257 a = list()
258 a.append("### %s" % self.label)
259 a.append("")
261 if self.description:
262 a.append(self.description)
263 a.append("")
265 if self.active:
266 active = "yes"
267 else:
268 active = "no"
270 a.append("- Active: %s" % active)
272 a.append("- Callback: %s" % self.callback)
274 if self.interval:
275 a.append("- Interval: %s" % self.interval)
277 if self.frequency:
278 a.append("- Frequency: %s" % self.frequency)
280 if self.at:
281 a.append("- At: %s" % self.at)
283 a.append("")
285 return "\n".join(a)
287 def to_plain(self):
288 """Export the job to plain text format.
290 :rtype: str
292 """
293 a = list()
294 a.append("Job: %s" % self.label)
296 if self.description:
297 a.append("Description: %s" % self.description)
299 if self.active:
300 active = "yes"
301 else:
302 active = "no"
304 a.append("Active: %s" % active)
306 a.append("Callback: %s" % self.callback)
308 if self.interval:
309 a.append("Interval: %s" % self.interval)
311 if self.frequency:
312 a.append("Frequency: %s" % self.frequency)
314 if self.at:
315 a.append("At: %s" % self.at)
317 a.append("-" * 120)
319 return "\n".join(a)
321 def to_rst(self):
322 """Export the job to ReStructuredText format.
324 :rtype: str
326 """
327 a = list()
328 a.append(self.label)
329 a.append("-" * len(self.label))
330 a.append("")
332 if self.description:
333 a.append(self.description)
334 a.append("")
336 if self.active:
337 active = "yes"
338 else:
339 active = "no"
341 a.append("- Active: %s" % active)
343 a.append("- Callback: %s" % self.callback)
345 if self.interval:
346 a.append("- Interval: %s" % self.interval)
348 if self.frequency:
349 a.append("- Frequency: %s" % self.frequency)
351 if self.at:
352 a.append("- At: %s" % self.at)
354 a.append("")
356 return "\n".join(a)
358 def _import_callback(self):
359 """Import the callback for the job.
361 :returns: A callable or ``None`` if the callback could not be imported.
363 """
364 tokens = self.callback.split(".")
365 callback = tokens.pop(-1)
367 target = ".".join(tokens)
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
381class Result(object):
382 """The result of a scheduled job."""
384 def __init__(self, status, end_dt=None, label=None, message=None, output=None, start_dt=None):
385 """Initialize a result.
387 :param status: The status of the execution.
388 :type status: str
390 :param end_dt: The date and time the job completed. Defaults to now.
391 :type end_dt: datetime
393 :param label: The job label.
394 :type label: str
396 :param message: The human-friendly message.
397 :type message: str
399 :param output: The output, if any, produced by the job.
400 :type output: str
402 :param start_dt: The date and time the job started. Defaults to now.
403 :type start_dt: datetime
405 """
406 self.label = label
407 self.message = message
408 self.output = output
409 self.status = status
411 current_dt = datetime.now()
412 self.end_dt = end_dt or current_dt
413 self.start_dt = start_dt or current_dt
415 @property
416 def elapsed_time(self):
417 """The amount of time that passed to execute the job.
419 :rtype: timedelta
421 """
422 return self.end_dt - self.start_dt
424 @property
425 def failure(self):
426 """Indicates the job failed to properly execute.
428 :rtype: bool
430 """
431 return self.status is not STATUS.SUCCESS
433 @property
434 def success(self):
435 """Indicates the job was successfully executed.
437 :rtype: bool
439 """
440 return self.status == STATUS.SUCCESS
443class Schedule(object):
444 """A collection of scheduled jobs."""
446 def __init__(self, label=None):
447 self.jobs = list()
448 self.label = label
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
465 def __iter__(self):
466 return iter(self.jobs)
468 def __len__(self):
469 return len(self.jobs)
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
525 def to_ini(self):
526 """Export all jobs to INI format.
528 :rtype: str
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())
537 return "\n".join(a)
539 def to_markdown(self, tabular=False):
540 """Export the schedule as Markdown.
542 :param tabular: Indicates the expected output is in table form.
543 :type tabular: bool
545 :rtype: str
547 """
548 a = list()
549 a.append("## %s" % self.label)
550 a.append("")
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())
559 a.append("")
561 return "\n".join(a)
563 def to_plain(self, tabular=False):
564 """Export the schedule as plain text.
566 :param tabular: Indicates the expected output is in table form.
567 :type tabular: bool
569 :rtype: str
571 """
572 a = list()
573 a.append(self.label)
574 a.append("")
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())
583 a.append("")
585 return "\n".join(a)
587 def to_rst(self, tabular=False):
588 """Export the schedule as ReStructuredText.
590 :param tabular: Indicates the expected output is in table form.
591 :type tabular: bool
593 :rtype: str
595 """
596 a = list()
597 a.append(self.label)
598 a.append("=" * len(self.label))
599 a.append("")
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())
608 a.append("")
610 return "\n".join(a)
612 def _get_table(self, formatting):
613 """Get the table.
615 :param formatting: The expected formatting of the table.
616 :type formatting: str
618 :rtype: superdjango.interfaces.cli.library.Table
620 """
621 if formatting in ("md", "markdown"):
622 formatting = "pipe"
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 ])
641 return table