Esempio n. 1
0
    def __init__(
        self,
        wechaty: Wechaty,
        endpoint: EndPoint,
        scheduler_options: Optional[Union[AsyncIOScheduler,
                                          WechatySchedulerOptions]] = None):
        self._plugins: Dict[str, WechatyPlugin] = OrderedDict()
        self._wechaty: Wechaty = wechaty
        self._plugin_status: Dict[str, PluginStatus] = {}

        self.app: Quart = cors(Quart('Wechaty Server', static_folder=None))

        self.endpoint: Tuple[str, int] = endpoint

        if scheduler_options is None:
            scheduler_options = WechatySchedulerOptions()

        if isinstance(scheduler_options, WechatySchedulerOptions):
            scheduler = AsyncIOScheduler()

            if isinstance(scheduler_options.job_store, str):
                scheduler_options.job_store = SQLAlchemyJobStore(
                    scheduler_options.job_store)

            scheduler.add_jobstore(scheduler_options.job_store,
                                   scheduler_options.job_store_alias)
        self.scheduler: AsyncIOScheduler = scheduler
Esempio n. 2
0
class TaskScheduler(object):
    def __init__(self, client: MongoClient) -> None:
        super().__init__()
        self.store = MongoDBJobStore(client=client)
        self.scheduler = AsyncIOScheduler()
        self.scheduler.add_jobstore(self.store)
        self.jobs: list = []
        self.jobLengthMax = 0

    def addJob(self,
               job: Callable,
               year: str,
               month: str,
               dayOfWeek: str,
               day: str,
               hour: str,
               minute: str,
               second: str,
               target: str,
               args: List = None) -> Job:
        trigger = CronTrigger(year=year,
                              month=month,
                              day_of_week=dayOfWeek,
                              day=day,
                              hour=hour,
                              minute=minute,
                              second=second)
        try:
            nextLength = self.jobLengthMax + 1
            job = self.scheduler.add_job(job,
                                         args=args,
                                         trigger=trigger,
                                         id=f"{target}-{nextLength}")
            self.jobLengthMax = nextLength
            return job
        except Exception as e:
            print(e)

    def removeJob(self, id: str) -> None:
        self.scheduler.remove_job(id)

    def setupJobs(self) -> None:
        jobs = self.scheduler.get_jobs()
        for i in range(len(jobs)):
            id: str = jobs[i].id
            idSplit = id.split("-")
            if not len(idSplit) > 1:
                continue
            if self.jobLengthMax < int(idSplit[1]):
                self.jobLengthMax = int(idSplit[1])
            self.jobs.append({"target": idSplit[0], "numId": idSplit[1]})

    def getJobs(self) -> list:
        return self.scheduler.get_jobs()

    def start(self) -> None:
        self.scheduler.start()
        self.setupJobs()
Esempio n. 3
0
File: fifo.py Progetto: glx33/Fox-V3
class FIFO(commands.Cog):
    """
    Simple Scheduling Cog

    Named after the simplest scheduling algorithm: First In First Out
    """
    def __init__(self, bot: Red):
        super().__init__()
        self.bot = bot
        self.config = Config.get_conf(self,
                                      identifier=70737079,
                                      force_registration=True)

        default_global = {"jobs": []}
        default_guild = {"tasks": {}}

        self.config.register_global(**default_global)
        self.config.register_guild(**default_guild)

        self.scheduler: Optional[AsyncIOScheduler] = None
        self.jobstore = None

        self.tz_cog = None

    async def red_delete_data_for_user(self, **kwargs):
        """Nothing to delete"""
        return

    def cog_unload(self):
        # self.scheduler.remove_all_jobs()
        if self.scheduler is not None:
            self.scheduler.shutdown()

    async def initialize(self):

        job_defaults = {
            "coalesce":
            True,  # Multiple missed triggers within the grace time will only fire once
            "max_instances":
            5,  # This is probably way too high, should likely only be one
            "misfire_grace_time":
            15,  # 15 seconds ain't much, but it's honest work
            "replace_existing": True,  # Very important for persistent data
        }

        # executors = {"default": AsyncIOExecutor()}

        # Default executor is already AsyncIOExecutor
        self.scheduler = AsyncIOScheduler(job_defaults=job_defaults,
                                          logger=schedule_log)

        from .redconfigjobstore import RedConfigJobStore  # Wait to import to prevent cyclic import

        self.jobstore = RedConfigJobStore(self.config, self.bot)
        await self.jobstore.load_from_config()
        self.scheduler.add_jobstore(self.jobstore, "default")

        self.scheduler.start()

    async def _check_parsable_command(self, ctx: commands.Context,
                                      command_to_parse: str):
        message: discord.Message = ctx.message

        message.content = ctx.prefix + command_to_parse
        message.author = ctx.author

        new_ctx: commands.Context = await self.bot.get_context(message)

        return new_ctx.valid

    async def _delete_task(self, task: Task):
        job: Union[Job, None] = await self._get_job(task)
        if job is not None:
            job.remove()

        await task.delete_self()

    async def _process_task(self, task: Task):
        # None of this is necessar, we have `replace_existing` already
        # job: Union[Job, None] = await self._get_job(task)
        # if job is not None:
        #     combined_trigger_ = await task.get_combined_trigger()
        #     if combined_trigger_ is None:
        #         job.remove()
        #     else:
        #         job.reschedule(combined_trigger_)
        #     return job
        return await self._add_job(task)

    async def _get_job(self, task: Task) -> Job:
        return self.scheduler.get_job(
            _assemble_job_id(task.name, task.guild_id))

    async def _add_job(self, task: Task):
        combined_trigger_ = await task.get_combined_trigger()
        if combined_trigger_ is None:
            return None

        return self.scheduler.add_job(
            _execute_task,
            kwargs=task.__getstate__(),
            id=_assemble_job_id(task.name, task.guild_id),
            trigger=combined_trigger_,
            name=task.name,
            replace_existing=True,
        )

    async def _resume_job(self, task: Task):
        job: Union[Job, None] = await self._get_job(task)
        if job is not None:
            job.resume()
        else:
            job = await self._process_task(task)
        return job

    async def _pause_job(self, task: Task):
        try:
            return self.scheduler.pause_job(
                job_id=_assemble_job_id(task.name, task.guild_id))
        except JobLookupError:
            return False

    async def _remove_job(self, task: Task):
        try:
            self.scheduler.remove_job(
                job_id=_assemble_job_id(task.name, task.guild_id))
        except JobLookupError:
            pass

    async def _get_tz(
            self, user: Union[discord.User,
                              discord.Member]) -> Union[None, tzinfo]:
        if self.tz_cog is None:
            self.tz_cog = self.bot.get_cog("Timezone")
            if self.tz_cog is None:
                self.tz_cog = False  # only try once to get the timezone cog

        if not self.tz_cog:
            return None
        try:
            usertime = await self.tz_cog.config.user(user).usertime()
        except AttributeError:
            return None

        if usertime:
            return await TimezoneConverter().convert(None, usertime)
        else:
            return None

    @checks.is_owner()
    @commands.guild_only()
    @commands.command()
    async def fifoclear(self, ctx: commands.Context):
        """Debug command to clear all current fifo data"""
        self.scheduler.remove_all_jobs()
        await self.config.guild(ctx.guild).tasks.clear()
        await self.config.jobs.clear()
        # await self.config.jobs_index.clear()
        await ctx.tick()

    @checks.is_owner()  # Will be reduced when I figure out permissions later
    @commands.guild_only()
    @commands.group()
    async def fifo(self, ctx: commands.Context):
        """
        Base command for handling scheduling of tasks
        """
        if ctx.invoked_subcommand is None:
            pass

    @fifo.command(name="wakeup")
    async def fifo_wakeup(self, ctx: commands.Context):
        """Debug command to fix missed executions.

        If you see a negative "Next run time" when adding a trigger, this may help resolve it.
        Check the logs when using this command.
        """

        self.scheduler.wakeup()
        await ctx.tick()

    @fifo.command(name="checktask", aliases=["checkjob", "check"])
    async def fifo_checktask(self, ctx: commands.Context, task_name: str):
        """Returns the next 10 scheduled executions of the task"""
        task = Task(task_name, ctx.guild.id, self.config, bot=self.bot)
        await task.load_from_config()

        if task.data is None:
            await ctx.maybe_send_embed(
                f"Task by the name of {task_name} is not found in this guild")
            return

        job = await self._get_job(task)
        if job is None:
            await ctx.maybe_send_embed("No job scheduled for this task")
            return
        now = datetime.now(job.next_run_time.tzinfo)

        times = [
            humanize_timedelta(timedelta=x - now)
            for x in itertools.islice(_get_run_times(job), 10)
        ]
        await ctx.maybe_send_embed("\n\n".join(times))

    @fifo.command(name="set")
    async def fifo_set(
        self,
        ctx: commands.Context,
        task_name: str,
        author_or_channel: Union[discord.Member, discord.TextChannel],
    ):
        """
        Sets a different author or in a different channel for execution of a task.
        """
        task = Task(task_name, ctx.guild.id, self.config, bot=self.bot)
        await task.load_from_config()

        if task.data is None:
            await ctx.maybe_send_embed(
                f"Task by the name of {task_name} is not found in this guild")
            return

        if isinstance(author_or_channel, discord.Member):
            if task.author_id == author_or_channel.id:
                await ctx.maybe_send_embed("Already executing as that member")
                return

            await task.set_author(author_or_channel)  # also saves
        elif isinstance(author_or_channel, discord.TextChannel):
            if task.channel_id == author_or_channel.id:
                await ctx.maybe_send_embed("Already executing in that channel")
                return

            await task.set_channel(author_or_channel)
        else:
            await ctx.maybe_send_embed("Unsupported result")
            return

        await ctx.tick()

    @fifo.command(name="resume")
    async def fifo_resume(self,
                          ctx: commands.Context,
                          task_name: Optional[str] = None):
        """
        Provide a task name to resume execution of a task.

        Otherwise resumes execution of all tasks on all guilds
        If the task isn't currently scheduled, will schedule it
        """
        if task_name is None:
            if self.scheduler.state == STATE_PAUSED:
                self.scheduler.resume()
                await ctx.maybe_send_embed(
                    "All task execution for all guilds has been resumed")
            else:
                await ctx.maybe_send_embed(
                    "Task execution is not paused, can't resume")
        else:
            task = Task(task_name, ctx.guild.id, self.config, bot=self.bot)
            await task.load_from_config()

            if task.data is None:
                await ctx.maybe_send_embed(
                    f"Task by the name of {task_name} is not found in this guild"
                )
                return

            if await self._resume_job(task):
                await ctx.maybe_send_embed(
                    f"Execution of {task_name=} has been resumed")
            else:
                await ctx.maybe_send_embed(f"Failed to resume {task_name=}")

    @fifo.command(name="pause")
    async def fifo_pause(self,
                         ctx: commands.Context,
                         task_name: Optional[str] = None):
        """
        Provide a task name to pause execution of a task

        Otherwise pauses execution of all tasks on all guilds
        """
        if task_name is None:
            if self.scheduler.state == STATE_RUNNING:
                self.scheduler.pause()
                await ctx.maybe_send_embed(
                    "All task execution for all guilds has been paused")
            else:
                await ctx.maybe_send_embed(
                    "Task execution is not running, can't pause")
        else:
            task = Task(task_name, ctx.guild.id, self.config, bot=self.bot)
            await task.load_from_config()

            if task.data is None:
                await ctx.maybe_send_embed(
                    f"Task by the name of {task_name} is not found in this guild"
                )
                return

            if await self._pause_job(task):
                await ctx.maybe_send_embed(
                    f"Execution of {task_name=} has been paused")
            else:
                await ctx.maybe_send_embed(f"Failed to pause {task_name=}")

    @fifo.command(name="details")
    async def fifo_details(self, ctx: commands.Context, task_name: str):
        """
        Provide all the details on the specified task name
        """
        task = Task(task_name, ctx.guild.id, self.config, bot=self.bot)
        await task.load_from_config()

        if task.data is None:
            await ctx.maybe_send_embed(
                f"Task by the name of {task_name} is not found in this guild")
            return

        embed = discord.Embed(title=f"Task: {task_name}")

        embed.add_field(name="Task command",
                        value=f"{ctx.prefix}{task.get_command_str()}",
                        inline=False)

        guild: discord.Guild = self.bot.get_guild(task.guild_id)

        if guild is not None:
            author: discord.Member = guild.get_member(task.author_id)
            channel: discord.TextChannel = guild.get_channel(task.channel_id)
            embed.add_field(name="Server", value=guild.name)
            if author is not None:
                embed.add_field(name="Author", value=author.mention)
            if channel is not None:
                embed.add_field(name="Channel", value=channel.mention)

        else:
            embed.add_field(name="Server",
                            value="Server not found",
                            inline=False)
        triggers, expired_triggers = await task.get_triggers()

        trigger_str = "\n".join(str(t) for t in triggers)
        expired_str = "\n".join(str(t) for t in expired_triggers)
        if trigger_str:
            embed.add_field(name="Triggers", value=trigger_str, inline=False)
        if expired_str:
            embed.add_field(name="Expired Triggers",
                            value=expired_str,
                            inline=False)

        job = await self._get_job(task)
        if job and job.next_run_time:
            embed.timestamp = job.next_run_time

        await ctx.send(embed=embed)

    @fifo.command(name="list")
    async def fifo_list(self, ctx: commands.Context, all_guilds: bool = False):
        """
        Lists all current tasks and their triggers.

        Do `[p]fifo list True` to see tasks from all guilds
        """
        if all_guilds:
            pass  # TODO: All guilds
        else:
            out = ""
            all_tasks = await self.config.guild(ctx.guild).tasks()
            for task_name, task_data in all_tasks.items():
                out += f"{task_name}: {task_data}\n\n"

            if out:
                if len(out) > 2000:
                    for page in pagify(out):
                        await ctx.maybe_send_embed(page)
                else:
                    await ctx.maybe_send_embed(out)
            else:
                await ctx.maybe_send_embed("No tasks to list")

    @fifo.command(name="printschedule")
    async def fifo_printschedule(self, ctx: commands.Context):
        """
        Print the current schedule of execution.

        Useful for debugging.
        """
        cp = CapturePrint()
        self.scheduler.print_jobs(out=cp)

        out = cp.string

        if out:
            if len(out) > 2000:
                for page in pagify(out):
                    await ctx.maybe_send_embed(page)
            else:
                await ctx.maybe_send_embed(out)
        else:
            await ctx.maybe_send_embed("Failed to get schedule from scheduler")

    @fifo.command(name="add")
    async def fifo_add(self, ctx: commands.Context, task_name: str, *,
                       command_to_execute: str):
        """
        Add a new task to this guild's task list
        """
        if (await self.config.guild(ctx.guild
                                    ).tasks.get_raw(task_name,
                                                    default=None)) is not None:
            await ctx.maybe_send_embed(f"Task already exists with {task_name=}"
                                       )
            return

        if "_" in task_name:  # See _disassemble_job_id
            await ctx.maybe_send_embed("Task name cannot contain underscores")
            return

        if not await self._check_parsable_command(ctx, command_to_execute):
            await ctx.maybe_send_embed(
                "Failed to parse command. Make sure not to include the prefix")
            return

        task = Task(task_name, ctx.guild.id, self.config, ctx.author.id,
                    ctx.channel.id, self.bot)
        await task.set_commmand_str(command_to_execute)
        await task.save_all()
        await ctx.tick()

    @fifo.command(name="delete")
    async def fifo_delete(self, ctx: commands.Context, task_name: str):
        """
        Deletes a task from this guild's task list
        """
        task = Task(task_name, ctx.guild.id, self.config, bot=self.bot)
        await task.load_from_config()

        if task.data is None:
            await ctx.maybe_send_embed(
                f"Task by the name of {task_name} is not found in this guild")
            return

        await self._delete_task(task)
        await ctx.maybe_send_embed(
            f"Task[{task_name}] has been deleted from this guild")

    @fifo.command(name="cleartriggers", aliases=["cleartrigger"])
    async def fifo_cleartriggers(self, ctx: commands.Context, task_name: str):
        """
        Removes all triggers from specified task

        Useful to start over with new trigger
        """

        task = Task(task_name, ctx.guild.id, self.config, bot=self.bot)
        await task.load_from_config()

        if task.data is None:
            await ctx.maybe_send_embed(
                f"Task by the name of {task_name} is not found in this guild")
            return

        await task.clear_triggers()
        await self._remove_job(task)
        await ctx.tick()

    @fifo.group(name="addtrigger", aliases=["trigger"])
    async def fifo_trigger(self, ctx: commands.Context):
        """
        Add a new trigger for a task from the current guild.
        """
        if ctx.invoked_subcommand is None:
            pass

    @fifo_trigger.command(name="interval")
    async def fifo_trigger_interval(self, ctx: commands.Context,
                                    task_name: str, *,
                                    interval_str: TimedeltaConverter):
        """
        Add an interval trigger to the specified task
        """

        task = Task(task_name, ctx.guild.id, self.config, bot=self.bot)
        await task.load_from_config()  # Will set the channel and author

        if task.data is None:
            await ctx.maybe_send_embed(
                f"Task by the name of {task_name} is not found in this guild")
            return

        result = await task.add_trigger("interval", interval_str)
        if not result:
            await ctx.maybe_send_embed(
                "Failed to add an interval trigger to this task, see console for logs"
            )
            return
        await task.save_data()
        job: Job = await self._process_task(task)
        delta_from_now: timedelta = job.next_run_time - datetime.now(
            job.next_run_time.tzinfo)
        await ctx.maybe_send_embed(
            f"Task `{task_name}` added interval of {interval_str} to its scheduled runtimes\n\n"
            f"Next run time: {job.next_run_time} ({delta_from_now.total_seconds()} seconds)"
        )

    @fifo_trigger.command(name="relative")
    async def fifo_trigger_relative(self, ctx: commands.Context,
                                    task_name: str, *,
                                    time_from_now: TimedeltaConverter):
        """
        Add a "run once" trigger at a time relative from now to the specified task
        """

        task = Task(task_name, ctx.guild.id, self.config, bot=self.bot)
        await task.load_from_config()

        if task.data is None:
            await ctx.maybe_send_embed(
                f"Task by the name of {task_name} is not found in this guild")
            return

        time_to_run = datetime.now(pytz.utc) + time_from_now

        result = await task.add_trigger("date", time_to_run,
                                        time_to_run.tzinfo)
        if not result:
            await ctx.maybe_send_embed(
                "Failed to add a date trigger to this task, see console for logs"
            )
            return

        await task.save_data()
        job: Job = await self._process_task(task)
        delta_from_now: timedelta = job.next_run_time - datetime.now(
            job.next_run_time.tzinfo)
        await ctx.maybe_send_embed(
            f"Task `{task_name}` added {time_to_run} to its scheduled runtimes\n"
            f"Next run time: {job.next_run_time} ({delta_from_now.total_seconds()} seconds)"
        )

    @fifo_trigger.command(name="date")
    async def fifo_trigger_date(self, ctx: commands.Context, task_name: str, *,
                                datetime_str: DatetimeConverter):
        """
        Add a "run once" datetime trigger to the specified task
        """

        task = Task(task_name, ctx.guild.id, self.config, bot=self.bot)
        await task.load_from_config()

        if task.data is None:
            await ctx.maybe_send_embed(
                f"Task by the name of {task_name} is not found in this guild")
            return

        maybe_tz = await self._get_tz(ctx.author)

        result = await task.add_trigger("date", datetime_str, maybe_tz)
        if not result:
            await ctx.maybe_send_embed(
                "Failed to add a date trigger to this task, see console for logs"
            )
            return

        await task.save_data()
        job: Job = await self._process_task(task)
        delta_from_now: timedelta = job.next_run_time - datetime.now(
            job.next_run_time.tzinfo)
        await ctx.maybe_send_embed(
            f"Task `{task_name}` added {datetime_str} to its scheduled runtimes\n"
            f"Next run time: {job.next_run_time} ({delta_from_now.total_seconds()} seconds)"
        )

    @fifo_trigger.command(name="cron")
    async def fifo_trigger_cron(
        self,
        ctx: commands.Context,
        task_name: str,
        optional_tz: Optional[TimezoneConverter] = None,
        *,
        cron_str: CronConverter,
    ):
        """
        Add a cron "time of day" trigger to the specified task

        See https://crontab.guru/ for help generating the cron_str
        """
        task = Task(task_name, ctx.guild.id, self.config, bot=self.bot)
        await task.load_from_config()

        if task.data is None:
            await ctx.maybe_send_embed(
                f"Task by the name of {task_name} is not found in this guild")
            return

        if optional_tz is None:
            optional_tz = await self._get_tz(ctx.author)  # might still be None

        result = await task.add_trigger("cron", cron_str, optional_tz)
        if not result:
            await ctx.maybe_send_embed(
                "Failed to add a cron trigger to this task, see console for logs"
            )
            return

        await task.save_data()
        job: Job = await self._process_task(task)
        delta_from_now: timedelta = job.next_run_time - datetime.now(
            job.next_run_time.tzinfo)
        await ctx.maybe_send_embed(
            f"Task `{task_name}` added cron_str to its scheduled runtimes\n"
            f"Next run time: {job.next_run_time} ({delta_from_now.total_seconds()} seconds)"
        )
class Reminders:
    """Reminders functionality for the guild bot"""
    def __init__(self, reminders_database_path: str):
        self.reminders_database_path = reminders_database_path
        self.scheduler : AsyncIOScheduler = None

    def start(self, event_loop):
        """Start the reminder service."""
        self.scheduler = AsyncIOScheduler(event_loop=event_loop, timezone='UTC')
        self.scheduler.add_jobstore(SQLAlchemyJobStore(url='sqlite:///' + self.reminders_database_path))
        self.scheduler.start()

    def stop(self):
        """Stop the reminder service immediately."""
        self.scheduler.shutdown(wait=False)
        del self.scheduler

    def createOrResetPeriodicStatusUpdateCallback(self, callback: callable):
        """Create or reset the status update callback for the entire bot."""
        self.scheduler.add_job(callback, trigger='interval', minutes=1, id='periodic-status', name='periodic-status',
            coalesce=True, max_instances=1, replace_existing=True)

    def addDailyReminder(self, owner_name: str, owner_id: str, callback: callable, callback_args: List[str]):
        """Add a daily reminder that is fired within a short time window after daily reset based on WotV world time (midnight at UTC -8).

        args:
        owner_name                  name of the owner, used in the description of the task.
        ower_id                     unique ID of the owner, used to construct IDs for the reminders.
        callback                    the callback function (must be a callable) to be invoked for the reminder.
        callback_args               positional arguments to be passed to the callback.

        The reminder will have the name "<owner_name>#daily", i.e. if the owner_name is "bob" then the ID of the reminder is "bob#daily"
        """
        job_id = owner_id + '#daily'
        job_desc = '#daily reminder for ' + owner_name + ' (id=' + owner_id + ')'
        self.scheduler.add_job(callback, id=job_id, name=job_desc, coalesce=True, max_instances=1, replace_existing=True, args=callback_args,
            trigger='cron', # Cron scheduler to get easy daily scheduling
            hour=8,    # World time is at UTC-8 so execute at midnight World Time
            minute=5,  # ... but actually spread it out a bit over 5 minutes by anchoring at 5 minutes past the hour and...
            jitter=300) # Jittering the time by as much as 300 seconds (5 minutes) in any direciton.

    def getDailyReminder(self, owner_id: str) -> apscheduler.job.Job:
        """Return the specified ownwer's daily reminder job."""
        return self.scheduler.get_job(owner_id + '#daily')

    def hasDailyReminder(self, owner_id: str) -> bool:
        """Return true if the specified user has a daily reminder configured."""
        return self.getDailyReminder(owner_id) is not None

    def cancelDailyReminder(self, owner_id: str):
        """Cancel any daily reminder configured for the specified owner."""
        job: apscheduler.job.Job = self.scheduler.get_job(owner_id + '#daily')
        if job:
            job.remove()

    def addWhimsyReminder(self, owner_name: str, owner_id: str, nrg_reminder_callback: callable, nrg_reminder_args: List[str], spawn_reminder_callback: callable,
        spawn_reminder_args: List[str], nrg_time_ms_override: int = None, spawn_time_ms_override: int = None):
        """Add a whimsy shop reminder. Actually a pair of reminders, one for NRG spending and one for whimsy spawning.

        The first reminder is set for 30 minutes after now, and reminds the user that they can now start spending NRG.
        The second reminder is set for 60 minutes after now, and reminds the user that they can now spawn a new Whimsy shop.

        args:
        owner_name                  name of the owner, used in the description of the task.
        ower_id                     unique ID of the owner, used to construct IDs for the reminders.
        nrg_reminder_callback       the callback function (must be a callable) to be invoked for the nrg reminder.
        nrg_reminder_args           positional arguments to be passed to the nrg_reminder_callback.
        spawn_reminder_callback     the callback function (must be a callable) to be invoked for the whimsy spawn reminder.
        spawn_reminder_args         positional arguments to be passed to the spawn_reminder_callback.
        nrg_time_ms_override        if specified, overrides the amount of time before the nrg reminder fires from 30 minutes to the specified number of ms
        spawn_time_ms_override      if specified, overrides the amount of time before the spawn reminder fires from 60 minutes to the specified number of ms

        The nrg reminder will have the name "<owner_name>#whimsy-nrg", i.e. if the owner_name is "bob" then the ID of the reminder is "bob#whimsy-nrg"
        The spawn reminder will have the name "<owner_name>#whimsy-spawn", i.e. if the owner_name is "bob" then the ID of the reminder is "bob#whimsy-spawn"
        """
        nrg_job_id = owner_id + '#whimsy-nrg'
        nrg_job_desc = '#whimsy-nrg reminder for ' + owner_name + ' (id=' + owner_id + ')'
        now = datetime.datetime.now(tz=utc)
        nrg_execute_at = now + datetime.timedelta(minutes=30)
        if nrg_time_ms_override:
            nrg_execute_at = now + datetime.timedelta(milliseconds=nrg_time_ms_override)
        spawn_job_id = owner_id + '#whimsy-spawn'
        spawn_job_desc = '#whimsy-spawn reminder for ' + owner_name + ' (id=' + owner_id + ')'
        spawn_execute_at = now + datetime.timedelta(hours=1)
        if spawn_time_ms_override:
            spawn_execute_at = now + datetime.timedelta(milliseconds=spawn_time_ms_override)
        self.scheduler.add_job(nrg_reminder_callback, trigger='date', run_date=nrg_execute_at, args=nrg_reminder_args, kwargs=None,
            id=nrg_job_id, name=nrg_job_desc, misfire_grace_time=30*60, coalesce=True, max_instances=1, replace_existing=True)
        self.scheduler.add_job(spawn_reminder_callback, trigger='date', run_date=spawn_execute_at, args=spawn_reminder_args, kwargs=None,
            id=spawn_job_id, name=spawn_job_desc, misfire_grace_time=30*60, coalesce=True, max_instances=1, replace_existing=True)

    def getWhimsyReminders(self, owner_id: str) -> Dict[str, apscheduler.job.Job]:
        """Fetch any whimsy reminders outstanding for the specified owner id.

        The returned dictionary contains 2 entries:
            'nrg':  <the NRG reminder, or None if there is no such reminder or the reminder has expired.>
            'spawn': <the spawn reminder, or None if there is no such reminder or the reminder has expired.>
        """
        return {
            'nrg': self.scheduler.get_job(owner_id + '#whimsy-nrg'),
            'spawn': self.scheduler.get_job(owner_id + '#whimsy-spawn'),
        }

    def hasPendingWhimsyNrgReminder(self, owner_id: str) -> bool:
        """Return true if the specified user has a pending whimsy shop NRG reminder."""
        scheduled: Dict[str, apscheduler.job.Job] = self.getWhimsyReminders(owner_id)
        return scheduled and 'nrg' in scheduled and scheduled['nrg'] and scheduled['nrg'].next_run_time and scheduled['nrg'].next_run_time > datetime.datetime.now(tz=utc)

    def hasPendingWhimsySpawnReminder(self, owner_id: str) -> bool:
        """Return true if the specified user has a pending whimsy shop spawn reminder."""
        scheduled: Dict[str, apscheduler.job.Job] = self.getWhimsyReminders(owner_id)
        return scheduled and 'spawn' in scheduled and scheduled['spawn'] and scheduled['spawn'].next_run_time and scheduled['spawn'].next_run_time > datetime.datetime.now(tz=utc)

    def timeTillWhimsyNrgReminder(self, owner_id: str) -> int:
        """If the specified user has a whimsy-shop NRG reminder in the future, return the number of seconds until that reminder fires; else return None."""
        scheduled: Dict[str, apscheduler.job.Job] = self.getWhimsyReminders(owner_id)
        if scheduled and 'nrg' in scheduled and scheduled['nrg'] and scheduled['nrg'].next_run_time and scheduled['nrg'].next_run_time > datetime.datetime.now(tz=utc):
            next_run_time: datetime.datetime = scheduled['nrg'].next_run_time
            return (next_run_time - datetime.datetime.now(tz=utc)).total_seconds()
        return None

    def timeTillWhimsySpawnReminder(self, owner_id: str) -> int:
        """If the specified user has a whimsy-shop spawn reminder in the future, return the number of seconds until that reminder fires; else return None."""
        scheduled: Dict[str, apscheduler.job.Job] = self.getWhimsyReminders(owner_id)
        if scheduled and 'spawn' in scheduled and scheduled['spawn'] and scheduled['spawn'].next_run_time and scheduled['spawn'].next_run_time > datetime.datetime.now(tz=utc):
            next_run_time: datetime.datetime = scheduled['spawn'].next_run_time
            return (next_run_time - datetime.datetime.now(tz=utc)).total_seconds()
        return None

    def cancelWhimsyReminders(self, owner_id: str):
        """Cancels any and all oustanding whimsy reminders for the specified owner."""
        job: apscheduler.job.Job = None
        job = self.scheduler.get_job(owner_id + '#whimsy-nrg')
        if job:
            job.remove()
        job = self.scheduler.get_job(owner_id + '#whimsy-spawn')
        if job:
            job.remove()
Esempio n. 5
0
    EVENT_JOB_MODIFIED = 2**11
    EVENT_JOB_EXECUTED = 2**12
    EVENT_JOB_ERROR = 2**13
    EVENT_JOB_MISSED = 2**14
    EVENT_JOB_SUBMITTED = 2**15
    EVENT_JOB_MAX_INSTANCES = 2**16


job_defaults = {
    "coalesce": AfishSettings.sched_coalesce,
    "max_instances": AfishSettings.sched_max_instances,
}

AfishScheduler = AsyncIOScheduler(job_defaults=job_defaults,
                                  timezone=AfishSettings.sched_timezone)
AfishScheduler.add_jobstore("sqlalchemy", url=AfishSettings.afish_pg_conn)
AfishScheduler.start()


# EVENT HANDLERS #
def jobExecHandler(event: JobExecutionEvent):
    try:
        db = SessionLocal()
        job = AfishScheduler.get_job(event.job_id)
        output = str(event.exception) if event.exception else event.retval
        execution = Execution(
            eid=str(uuid.uuid4()),
            job_id=event.job_id,
            name=job.name,
            module=job.kwargs["module"],
            project=job.kwargs["project"],
Esempio n. 6
0
"""
Represents our shared scheduler and distributed locks
The scheduler is backed by redis, meaning that jobs can be restored after a restart
"""
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from redlock import RedLockFactory

__all__ = ["redlocks", "scheduler"]

scheduler = AsyncIOScheduler()
scheduler.add_jobstore("redis")

redlocks = RedLockFactory([{"host": "127.0.0.1"}])

scheduler.start()
Esempio n. 7
0
    format="[{time:YYYY-MM-DD at HH:mm:ss}] {level}: {name} : {message}",
    level=logging.ERROR,
    colorize=False)

logging.getLogger('aiogram').setLevel(logging.WARNING)

loop = asyncio.get_event_loop()
bot = Bot(telegram.BOT_TOKEN, loop=loop, parse_mode=types.ParseMode.HTML)

dp = Dispatcher(bot, storage=MemoryStorage())

# additional helpers
scheduler = AsyncIOScheduler(timezone=consts.default_timezone,
                             coalesce=True,
                             misfire_grace_time=10000)
scheduler.add_jobstore(
    RedisJobStore(db=1, host=database.REDIS_HOST, port=database.REDIS_PORT))
scheduler.start()

inline_timepicker = InlineTimepicker()


@dp.message_handler(state='*', commands=['cancel'])
@dp.message_handler(lambda msg: msg.text.lower() == 'cancel', state='*')
async def cancel_handler(msg: types.Message, state: FSMContext):
    await state.finish()
    await bot.send_message(msg.from_user.id, _("cancel"))


@dp.message_handler(commands=['start'], state='*')
async def start_command_handler(msg: types.Message):
    await bot.send_message(msg.chat.id, _("start_cmd_text"))
Esempio n. 8
0
# -*- coding: utf-8 -*-
"""
Created on Sat Apr 25 17:54:00 2020

@author: Lukas
"""
###############################################################################
# Imports
###############################################################################
import asyncio
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore

###############################################################################
# Start the scheduler
###############################################################################
scheduler = AsyncIOScheduler(event_loop=asyncio.get_event_loop())
#jobstore = SQLAlchemyJobStore(url='postgresql+asyncpg://localhost:5432/team_utils')
jobstore = SQLAlchemyJobStore(url='sqlite:///jobs.db')
scheduler.add_jobstore(jobstore)
scheduler.start()

Esempio n. 9
0
from datetime import datetime

docker_client = docker.from_env()
config = configparser.ConfigParser()
config.read('config.ini')
scheduler = AsyncIOScheduler()
scheduler.add_executor('processpool')
jobstore = False
if config.has_section('POSTGRES') and config['POSTGRES']['DB'] != '':
    postgres_user = config['POSTGRES']['USER']
    postgres_pass = config['POSTGRES']['PASSWORD']
    postgres_db = config['POSTGRES']['DB']
    postgres_host = config['POSTGRES']['HOST']
    url = 'postgresql://{}:{}@{}/{}'.format(postgres_user, postgres_pass,
                                            postgres_host, postgres_db)
    scheduler.add_jobstore('sqlalchemy', url=url)
    jobstore = True
#logging.basicConfig(level=logging.DEBUG)
#logging.getLogger('apscheduler').setLevel(logging.DEBUG)
scheduler.start()


def schedule_run(data):
    #data = request.get_json()
    response = {}
    env = {}
    command = []
    renku = False
    container = data['container']
    response['container'] = container
    print(container)
Esempio n. 10
0
class Application(object):
    def __init__(self, options):
        self.cli_options = self.get_argparser().parse_args(options)
        log.debug("Parsed CLI options: %s", self.cli_options)
        self.timelapse_config_list = TimelapseConfig.parse_configs_from_file(
            self.cli_options.config)
        self.scheduler = AsyncIOScheduler()
        self.active_cameras_sn = set()

    @staticmethod
    def get_argparser():
        parser = argparse.ArgumentParser()
        parser.add_argument('-v',
                            '--verbose',
                            action='store_true',
                            default=False,
                            help='Use more verbose output.')
        parser.add_argument('-c',
                            '--config',
                            action='store',
                            default=None,
                            help='Path to configuration YAML file to use.')
        parser.add_argument('-i',
                            '--max-instances',
                            action='store',
                            type=int,
                            default=2,
                            help='Maximum number of jobs taking picture '
                            'from a camera running at the same time. '
                            'You may need to increase this in case '
                            'that uploading pictures to e.g. Dropbox '
                            'takes too long. The default is "2".')
        return parser

    def _scheduler_add_job(self, config, camera):
        try:
            self.scheduler.add_jobstore(MemoryJobStore(),
                                        alias=camera.serial_number)
        except ValueError as e:
            raise e
        # TODO: It would probably make sense to make the number of max_instances configurable
        self.scheduler.add_job(self.take_picture_job,
                               TimelapseConfigTrigger(config),
                               args=(config, camera, asyncio.get_event_loop()),
                               jobstore=camera.serial_number,
                               max_instances=self.cli_options.max_instances)
        self.active_cameras_sn.add(camera.serial_number)

    def _scheduler_remove_jobstore(self, jobstore):
        log.debug("Removing jobs for camera sn: %s", jobstore)
        self.scheduler.remove_jobstore(jobstore)
        self.active_cameras_sn.remove(jobstore)

    def refresh_timelapses_job(self):
        refresh_period = 5
        loop = asyncio.get_event_loop()

        available_cameras = CameraDevice.get_available_cameras()
        if len(available_cameras) == 0:
            for removed_camera_sn in self.active_cameras_sn:
                log.debug("Removing jobs for camera sn: %s", removed_camera_sn)
                self.scheduler.remove_jobstore(removed_camera_sn)
            self.active_cameras_sn.clear()
            self.scheduler.remove_all_jobs()
            loop.call_later(refresh_period, self.refresh_timelapses_job)
            return

        active_cameras_map = {c.serial_number: c for c in available_cameras}
        new_active_cameras_sn = [c.serial_number for c in available_cameras]
        # remove jobs and job stores for every removed camera
        removed_cameras_sn = self.active_cameras_sn - set(
            new_active_cameras_sn)
        for removed_camera_sn in removed_cameras_sn:
            self._scheduler_remove_jobstore(removed_camera_sn)

        new_cameras_sn = set(new_active_cameras_sn) - self.active_cameras_sn
        # Go through all configuration and add timelapse jobs for any new cameras that fit them
        for config in self.timelapse_config_list:
            camera_sn = config.camera_sn
            # the config is bound to specific device
            if camera_sn:
                if camera_sn in new_cameras_sn:
                    camera_device = active_cameras_map[camera_sn]
                    self._scheduler_add_job(config, camera_device)
                    log.debug("Added timelapse job for camera sn: %s",
                              camera_sn)
            # configuration is not bound to specific device
            else:
                for camera_sn in new_cameras_sn:
                    camera_device = active_cameras_map[camera_sn]
                    self._scheduler_add_job(config, camera_device)
                    log.debug("Added timelapse job for camera sn: %s",
                              camera_sn)

        loop.call_later(refresh_period, self.refresh_timelapses_job)

    def take_picture_job(self, config, camera, eventloop):
        log.info("Taking picture in %s ...", threading.current_thread())
        tmp_store_dir = tempfile.mkdtemp()
        try:
            picture = camera.take_picture()
            tmp_store_location = os.path.join(tmp_store_dir,
                                              os.path.basename(picture))
            camera.download_picture(picture, tmp_store_location,
                                    config.keep_on_camera)
        except CameraDeviceError as err:
            # there is some problem with the Camera, remove its whole jobstore
            log.warning("Error occurred while taking picture on %s(%s)",
                        camera.name, camera.serial_number)
            log.debug(err)
            shutil.rmtree(tmp_store_dir)
            self._scheduler_remove_jobstore(camera.serial_number)
        else:
            log.info("Temporarily stored taken picture in %s",
                     tmp_store_location)
            # TODO: it may make sense to use camera_sn in the store path if the configuration is not bound to a specific camera, thus there can be multiple cameras storing pictures into the same folder
            eventloop.run_in_executor(None, self.store_tmp_file_in_datastore,
                                      config, tmp_store_location)

    @staticmethod
    def store_tmp_file_in_datastore(config, tmp_file):
        datastores = config.datastore

        for datastore in datastores:
            datastore_type = datastore[TimelapseConfig.DATASTORE_TYPE]

            try:
                if datastore_type == TimelapseConfig.DATASTORE_TYPE_DROPBOX:
                    ds = DropboxDataStore(
                        datastore[TimelapseConfig.DATASTORE_DROPBOX_TOKEN],
                        datastore[TimelapseConfig.DATASTORE_STORE_PATH],
                        datastore.get(
                            TimelapseConfig.DATASTORE_DROPBOX_TIMEOUT, None))
                elif datastore_type == TimelapseConfig.DATASTORE_TYPE_FILESYSTEM:
                    ds = FilesystemDataStore(
                        datastore[TimelapseConfig.DATASTORE_STORE_PATH])
                else:
                    raise NotImplementedError("Unexpected datastore type '%s'",
                                              datastore_type)
            except DatastoreError as err:
                log.error(
                    "Failed to initialize datastore '%s' due to error: %s",
                    datastore_type, err)
                continue

            log.debug("Storing temporary file '%s' using data store '%s'",
                      tmp_file, ds)
            try:
                ds.store_file(tmp_file, False)
            except DataSaveError as err:
                log.warning(
                    "Failed to store file '%s' using datastore '%s' due to error: %s",
                    tmp_file, datastore_type, err)
                continue
        shutil.rmtree(os.path.dirname(tmp_file))

    def exception_handler_job(self, loop, context):
        log.critical(
            "Unhandled exception happened in one of the callbacks, terminating the application!"
        )
        log.exception(context['exception'])
        self.stop()

    def stop(self):
        log.info("Shutting down the application")
        self.scheduler.remove_all_jobs()
        self.scheduler.shutdown(wait=False)
        loop = asyncio.get_event_loop()
        loop.stop()

    def run(self):
        self.scheduler.start()
        loop = asyncio.get_event_loop()
        loop.set_exception_handler(self.exception_handler_job)
        loop.call_soon(self.refresh_timelapses_job)
        loop.run_forever()
        loop.close()