Full Code of agronholm/apscheduler for AI

master ff65a444f830 cached
99 files
589.5 KB
132.2k tokens
712 symbols
1 requests
Download .txt
Showing preview only (620K chars total). Download the full file or copy to clipboard to get everything.
Repository: agronholm/apscheduler
Branch: master
Commit: ff65a444f830
Files: 99
Total size: 589.5 KB

Directory structure:
gitextract_2so_8z_h/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yaml
│   │   ├── config.yml
│   │   └── features_request.yaml
│   ├── dependabot.yml
│   ├── pull_request_template.md
│   └── workflows/
│       ├── publish.yml
│       └── test.yml
├── .gitignore
├── .mailmap
├── .pre-commit-config.yaml
├── .readthedocs.yml
├── LICENSE.txt
├── README.rst
├── docker-compose.yml
├── docs/
│   ├── api.rst
│   ├── conf.py
│   ├── contributing.rst
│   ├── extending.rst
│   ├── faq.rst
│   ├── index.rst
│   ├── integrations.rst
│   ├── migration.rst
│   ├── userguide.rst
│   └── versionhistory.rst
├── examples/
│   ├── README.rst
│   ├── gui/
│   │   └── qt_executor.py
│   ├── separate_worker/
│   │   ├── async_scheduler.py
│   │   ├── async_worker.py
│   │   ├── example_tasks.py
│   │   ├── sync_scheduler.py
│   │   └── sync_worker.py
│   ├── standalone/
│   │   ├── async_memory.py
│   │   ├── async_mysql.py
│   │   ├── async_postgres.py
│   │   └── sync_memory.py
│   └── web/
│       ├── asgi_fastapi.py
│       ├── asgi_noframework.py
│       ├── asgi_starlette.py
│       ├── wsgi_flask.py
│       └── wsgi_noframework.py
├── pyproject.toml
├── src/
│   └── apscheduler/
│       ├── __init__.py
│       ├── _context.py
│       ├── _converters.py
│       ├── _decorators.py
│       ├── _enums.py
│       ├── _events.py
│       ├── _exceptions.py
│       ├── _marshalling.py
│       ├── _retry.py
│       ├── _schedulers/
│       │   ├── __init__.py
│       │   ├── async_.py
│       │   └── sync.py
│       ├── _structures.py
│       ├── _utils.py
│       ├── _validators.py
│       ├── abc.py
│       ├── datastores/
│       │   ├── __init__.py
│       │   ├── base.py
│       │   ├── memory.py
│       │   ├── mongodb.py
│       │   └── sqlalchemy.py
│       ├── eventbrokers/
│       │   ├── __init__.py
│       │   ├── asyncpg.py
│       │   ├── base.py
│       │   ├── local.py
│       │   ├── mqtt.py
│       │   ├── psycopg.py
│       │   └── redis.py
│       ├── executors/
│       │   ├── __init__.py
│       │   ├── async_.py
│       │   ├── qt.py
│       │   ├── subprocess.py
│       │   └── thread.py
│       ├── py.typed
│       ├── serializers/
│       │   ├── __init__.py
│       │   ├── cbor.py
│       │   ├── json.py
│       │   └── pickle.py
│       └── triggers/
│           ├── __init__.py
│           ├── calendarinterval.py
│           ├── combining.py
│           ├── cron/
│           │   ├── __init__.py
│           │   ├── expressions.py
│           │   └── fields.py
│           ├── date.py
│           └── interval.py
├── tests/
│   ├── conftest.py
│   ├── test_datastores.py
│   ├── test_eventbrokers.py
│   ├── test_marshalling.py
│   ├── test_schedulers.py
│   ├── test_serializers.py
│   └── triggers/
│       ├── test_calendarinterval.py
│       ├── test_combining.py
│       ├── test_cron.py
│       ├── test_date.py
│       └── test_interval.py
└── tools/
    └── dockerize

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yaml
================================================
name: Bug Report
description: File a bug report
labels: ["bug"]
body:
  - type: markdown
    attributes:
      value: >
        If you observed a crash in the library, or saw unexpected behavior in it, report
        your findings here. If you're not reasonably sure that it's not the intended
        behavior, you should ask through one of the support venues listed on the
        previous page.
  - type: checkboxes
    attributes:
      label: Things to check first
      options:
        - label: >
            I have checked that my issue does not already have a solution in the
            [FAQ](https://apscheduler.readthedocs.io/en/master/faq.html)
          required: true
        - label: >
            I have searched the existing issues and didn't find my bug already reported
            there
          required: true
        - label: >
            I have checked that my bug is still present in the latest release
          required: true
  - type: input
    id: version
    attributes:
      label: Version
      description: What version of APScheduler were you running?
    validations:
      required: true
  - type: textarea
    id: what-happened
    attributes:
      label: What happened?
      description: >
        Unless you are reporting a crash, tell us what you expected to happen instead.
    validations:
      required: true
  - type: textarea
    id: mwe
    attributes:
      label: How can we reproduce the bug?
      description: >
        In order to investigate the bug, we need to be able to reproduce it on our own.
        Please create a
        [minimum workable example](https://stackoverflow.com/help/minimal-reproducible-example)
        that demonstrates the problem. List any third party libraries required for this,
        but avoid using them unless absolutely necessary.
    validations:
      required: true


================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
  - name: Stack Overflow
    url: https://stackoverflow.com/questions/tagged/apscheduler
    about: The preferred site for asking questions
  - name: GitHub Discussions
    url: https://github.com/agronholm/apscheduler/discussions/categories/q-a
    about: An alternative for StackOverflow (if you don't want to register there)
  - name: Support chat on Gitter
    url: https://gitter.im/apscheduler/Lobby
    about: Technical support chat


================================================
FILE: .github/ISSUE_TEMPLATE/features_request.yaml
================================================
name: Feature request
description: Suggest a new feature
labels: ["enhancement"]
body:
  - type: markdown
    attributes:
      value: >
        If you have thought of a new feature that would increase the usefulness of this
        project, please use this form to send us your idea.
  - type: checkboxes
    attributes:
      label: Things to check first
      options:
        - label: >
            I have searched the existing issues and didn't find my feature already
            requested there
          required: true
  - type: textarea
    id: feature
    attributes:
      label: Feature description
      description: >
        Describe the feature in detail. The more specific the description you can give,
        the easier it should be to implement this feature.
    validations:
      required: true
  - type: textarea
    id: usecase
    attributes:
      label: Use case
      description: >
        Explain why you need this feature, and why you think it would be useful to
        others too.
    validations:
      required: true


================================================
FILE: .github/dependabot.yml
================================================
# Keep GitHub Actions up to date with GitHub's Dependabot...
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
version: 2
updates:
  - package-ecosystem: github-actions
    directory: /
    groups:
      github-actions:
        patterns:
          - "*"  # Group all Actions updates into a single larger pull request
    schedule:
      interval: monthly


================================================
FILE: .github/pull_request_template.md
================================================
<!-- Thank you for your contribution! -->
## Changes

Fixes #. <!-- Provide issue number if exists -->

<!-- Please give a short brief about these changes. -->

## Checklist

If this is a user-facing code change, like a bugfix or a new feature, please ensure that
you've fulfilled the following conditions (where applicable):

- [ ] You've added tests (in `tests/`) added which would fail without your patch
- [ ] You've updated the documentation (in `docs/`, in case of behavior changes or new
features)
- [ ] You've added a new changelog entry (in `docs/versionhistory.rst`).

If this is a trivial change, like a typo fix or a code reformatting, then you can ignore
these instructions.

### Updating the changelog

If there are no entries after the last release, use `**UNRELEASED**` as the version.
If, say, your patch fixes issue #999, the entry should look like this:

`* Fix big bad boo-boo in the async scheduler
  (`#123 <https://github.com/agronholm/apscheduler/issues/123>`_; PR by @yourgithubaccount)

If there's no issue linked, just link to your pull request instead by updating the
changelog after you've created the PR.


================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish packages to PyPI

on:
  push:
    tags:
      - "[0-9]+.[0-9]+.[0-9]+"
      - "[0-9]+.[0-9]+.[0-9]+.post[0-9]+"
      - "[0-9]+.[0-9]+.[0-9]+[a-b][0-9]+"
      - "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"

jobs:
  build:
    name: Build the source tarball and the wheel
    runs-on: ubuntu-latest
    environment: release
    steps:
    - uses: actions/checkout@v6
    - name: Set up Python
      uses: actions/setup-python@v6
      with:
        python-version: 3.x
    - name: Install dependencies
      run: pip install build
    - name: Create packages
      run: python -m build
    - name: Archive packages
      uses: actions/upload-artifact@v6
      with:
        name: dist
        path: dist

  publish:
    name: Publish build artifacts to the PyPI
    needs: build
    runs-on: ubuntu-latest
    environment: release
    permissions:
      id-token: write
    steps:
    - name: Retrieve packages
      uses: actions/download-artifact@v7
      with:
        name: dist
        path: dist
    - name: Upload packages
      uses: pypa/gh-action-pypi-publish@release/v1

  release:
    name: Create a GitHub release
    needs: build
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
    - uses: actions/checkout@v6
    - id: changelog
      uses: agronholm/release-notes@v1
      with:
        path: docs/versionhistory.rst
    - uses: ncipollo/release-action@v1
      with:
        body: ${{ steps.changelog.outputs.changelog }}


================================================
FILE: .github/workflows/test.yml
================================================
name: test suite

on:
  push:
    branches: [master]
  pull_request:

jobs:
  test-linux:
    strategy:
      fail-fast: false
      matrix:
        python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v6
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v6
      with:
        python-version: ${{ matrix.python-version }}
        allow-prereleases: true
        cache: pip
        cache-dependency-path: pyproject.toml
    - name: Start external services
      run: docker compose up -d
    - name: Ensure pip >= v25.1
      run: python -m pip install "pip >= 25.1"
    - name: Install the project and its dependencies
      run: pip install --group test -e .
    - name: Test with pytest
      run: coverage run -m pytest -v
    - name: Generate coverage report
      run: coverage xml
    - name: Upload Coverage
      uses: coverallsapp/github-action@v2
      with:
        parallel: true
        file: coverage.xml

  test-pypy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v6
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v6
      with:
        python-version: pypy-3.11
        cache: pip
        cache-dependency-path: pyproject.toml
    - name: Start external services
      run: docker compose up -d
    - name: Ensure pip >= v25.1
      run: python -m pip install "pip >= 25.1"
    - name: Install the project and its dependencies
      run: pip install --group test -e .
    - name: Test with pytest
      run: pytest -v

  test-others:
    strategy:
      fail-fast: false
      matrix:
        os: [macos-latest, windows-latest]
        python-version: ["3.10", "3.14"]
    runs-on: ${{ matrix.os }}
    steps:
    - uses: actions/checkout@v6
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v6
      with:
        python-version: ${{ matrix.python-version }}
        allow-prereleases: true
        cache: pip
        cache-dependency-path: pyproject.toml
    - name: Ensure pip >= v25.1
      run: python -m pip install "pip >= 25.1"
    - name: Install the project and its dependencies
      run: pip install --group test -e .
    - name: Test with pytest
      run: coverage run -m pytest -v -m "not external_service"
    - name: Generate coverage report
      run: coverage xml
    - name: Upload Coverage
      uses: coverallsapp/github-action@v2
      with:
        parallel: true
        file: coverage.xml

  coveralls:
    name: Finish Coveralls
    needs:
      - test-linux
      - test-others
    runs-on: ubuntu-latest
    steps:
    - name: Finished
      uses: coverallsapp/github-action@v2
      with:
        parallel-finished: true


================================================
FILE: .gitignore
================================================
.project
.pydevproject
.idea/
.coverage
.cache/
.mypy_cache
.pytest_cache/
.tox/
.eggs/
*.egg-info/
*.pyc
dist/
docs/_build/
build/
virtualenv/
venv*/
example.sqlite


================================================
FILE: .mailmap
================================================
Alex Grönholm <alex.gronholm@nextday.fi> agronholm <devnull@localhost>
Alex Grönholm <alex.gronholm@nextday.fi> demigod <devnull@localhost>


================================================
FILE: .pre-commit-config.yaml
================================================
# This is the configuration file for pre-commit (https://pre-commit.com/).
# To use:
# * Install pre-commit (https://pre-commit.com/#installation)
# * Copy this file as ".pre-commit-config.yaml"
# * Run "pre-commit install".
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v6.0.0
    hooks:
      - id: check-added-large-files
      - id: check-case-conflict
      - id: check-merge-conflict
      - id: check-symlinks
      - id: check-toml
      - id: check-yaml
      - id: debug-statements
      - id: end-of-file-fixer
      - id: mixed-line-ending
        args: [ "--fix=lf" ]
      - id: trailing-whitespace

  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.14.10
    hooks:
      - id: ruff-check
        args: [--fix, --show-fixes]
      - id: ruff-format

  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: v1.19.1
    hooks:
      - id: mypy
        additional_dependencies:
          - attrs == 23.2.0
          - pymongo == 4.8.0
          - redis == 5.0.7
          - sqlalchemy == 2.0.31
          - tzlocal == 5.2
        exclude: docs/conf.py
        stages: [manual]

  - repo: https://github.com/codespell-project/codespell
    rev: v2.4.1
    hooks:
      - id: codespell

  - repo: https://github.com/pre-commit/pygrep-hooks
    rev: v1.10.0
    hooks:
    - id: rst-backticks
    - id: rst-directive-colons
    - id: rst-inline-touching-normal

ci:
  autoupdate_schedule: quarterly


================================================
FILE: .readthedocs.yml
================================================
version: 2

build:
  os: ubuntu-22.04
  tools:
    python: "3.12"
  jobs:
    install:
      - python -m pip install --no-cache-dir "pip >= 25.1"
      - python -m pip install --upgrade --upgrade-strategy only-if-needed --no-cache-dir --group doc .

sphinx:
  configuration: docs/conf.py
  fail_on_warning: true


================================================
FILE: LICENSE.txt
================================================
The MIT License (MIT)

Copyright (c) 2009 Alex Grönholm

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.rst
================================================
.. image:: https://github.com/agronholm/apscheduler/actions/workflows/test.yml/badge.svg
  :target: https://github.com/agronholm/apscheduler/actions/workflows/test.yml
  :alt: Build Status
.. image:: https://coveralls.io/repos/github/agronholm/apscheduler/badge.svg?branch=master
  :target: https://coveralls.io/github/agronholm/apscheduler?branch=master
  :alt: Code Coverage
.. image:: https://readthedocs.org/projects/apscheduler/badge/?version=latest
  :target: https://apscheduler.readthedocs.io/en/master/?badge=latest
  :alt: Documentation

.. warning:: The v4.0 series is provided as a **pre-release** and may change in a
   backwards incompatible fashion without any migration pathway, so do NOT use this
   release in production!

Advanced Python Scheduler (APScheduler) is a task scheduler and task queue system for
Python. It can be used solely as a job queuing system if you have no need for task
scheduling. It scales both up and down, and is suitable for both trivial, single-process
use cases as well as large deployments spanning multiple nodes. Multiple schedulers and
workers can be deployed to use a shared data store to provide both a degree of high
availability and horizontal scaling.

APScheduler comes in both synchronous and asynchronous flavors, making it a good fit for
both traditional, thread-based applications, and asynchronous (asyncio or Trio_)
applications. Documentation and examples are provided for integrating with either WSGI_
or ASGI_ compatible web applications.

Support is provided for persistent storage of schedules and jobs. This means that they
can be shared among multiple scheduler/worker instances and will survive process and
node restarts.

The built-in persistent data store back-ends are:

* PostgreSQL
* MySQL and derivatives
* SQLite
* MongoDB

The built-in event brokers (needed in scenarios with multiple schedulers and/or
workers):

* PostgreSQL
* Redis
* MQTT

The built-in scheduling mechanisms (*triggers*) are:

* Cron-style scheduling
* Interval-based scheduling (runs tasks on even intervals)
* Calendar-based scheduling (runs tasks on intervals of X years/months/weeks/days,
  always at the same time of day)
* One-off scheduling (runs a task once, at a specific date/time)

Different scheduling mechanisms can even be combined with so-called *combining triggers*
(see the documentation_ for details).

You can also implement your custom scheduling logic by building your own trigger class.
These will be treated no differently than the built-in ones.

Other notable features include:

* You can limit the maximum number of simultaneous jobs for a given task (function)
* You can limit the amount of time a job is allowed to start late
* Jitter (adjustable, random delays added to the run time of each scheduled job)

.. _Trio: https://pypi.org/project/trio/
.. _WSGI: https://wsgi.readthedocs.io/en/latest/what.html
.. _ASGI: https://asgi.readthedocs.io/en/latest/index.html
.. _documentation: https://apscheduler.readthedocs.io/en/master/

Documentation
=============

Documentation can be found
`here <https://apscheduler.readthedocs.io/en/master/?badge=latest>`_.

Source
======

The source can be browsed at `Github <https://github.com/agronholm/apscheduler>`_.

Reporting bugs
==============

A `bug tracker <https://github.com/agronholm/apscheduler/issues>`_ is provided by
GitHub.

Getting help
============

If you have problems or other questions, you can either:

* Ask in the `apscheduler <https://gitter.im/apscheduler/Lobby>`_ room on Gitter
* Post a question on `GitHub discussions`_, or
* Post a question on StackOverflow_ and add the ``apscheduler`` tag

.. _GitHub discussions: https://github.com/agronholm/apscheduler/discussions/categories/q-a
.. _StackOverflow: http://stackoverflow.com/questions/tagged/apscheduler


================================================
FILE: docker-compose.yml
================================================
services:
  postgresql:
    image: postgres
    ports:
      - 127.0.0.1:5432:5432
    environment:
      POSTGRES_DB: testdb
      POSTGRES_PASSWORD: secret

  mysql:
    image: mysql
    ports:
      - 127.0.0.1:3306:3306
    environment:
      MYSQL_DATABASE: testdb
      MYSQL_ROOT_PASSWORD: secret

  mongodb:
    image: mongo
    ports:
      - 127.0.0.1:27017:27017

  emqx:
    image: emqx/emqx
    ports:
      - 127.0.0.1:1883:1883

  redis:
    image: redis
    ports:
      - 127.0.0.1:6379:6379


================================================
FILE: docs/api.rst
================================================
API reference
=============

Data structures
---------------

.. autoclass:: apscheduler.Task
.. autoclass:: apscheduler.TaskDefaults
.. autoclass:: apscheduler.Schedule
.. autoclass:: apscheduler.ScheduleResult
.. autoclass:: apscheduler.Job
.. autoclass:: apscheduler.JobResult

Decorators
----------

.. autodecorator:: apscheduler.task

Schedulers
----------

.. autoclass:: apscheduler.Scheduler
.. autoclass:: apscheduler.AsyncScheduler

Job executors
-------------

.. autoclass:: apscheduler.abc.JobExecutor
.. autoclass:: apscheduler.executors.async_.AsyncJobExecutor
.. autoclass:: apscheduler.executors.subprocess.ProcessPoolJobExecutor
.. autoclass:: apscheduler.executors.qt.QtJobExecutor
.. autoclass:: apscheduler.executors.thread.ThreadPoolJobExecutor

Data stores
-----------

.. autoclass:: apscheduler.abc.DataStore
.. autoclass:: apscheduler.datastores.memory.MemoryDataStore
.. autoclass:: apscheduler.datastores.sqlalchemy.SQLAlchemyDataStore
.. autoclass:: apscheduler.datastores.mongodb.MongoDBDataStore

Event brokers
-------------

.. autoclass:: apscheduler.abc.EventBroker
.. autoclass:: apscheduler.abc.Subscription
.. autoclass:: apscheduler.eventbrokers.local.LocalEventBroker
.. autoclass:: apscheduler.eventbrokers.asyncpg.AsyncpgEventBroker
.. autoclass:: apscheduler.eventbrokers.psycopg.PsycopgEventBroker
.. autoclass:: apscheduler.eventbrokers.mqtt.MQTTEventBroker
.. autoclass:: apscheduler.eventbrokers.redis.RedisEventBroker

Serializers
-----------

.. autoclass:: apscheduler.abc.Serializer
.. autoclass:: apscheduler.serializers.cbor.CBORSerializer
.. autoclass:: apscheduler.serializers.json.JSONSerializer
.. autoclass:: apscheduler.serializers.pickle.PickleSerializer

Triggers
--------

.. autoclass:: apscheduler.abc.Trigger
   :special-members: __getstate__, __setstate__

.. autoclass:: apscheduler.triggers.date.DateTrigger
.. autoclass:: apscheduler.triggers.interval.IntervalTrigger
.. autoclass:: apscheduler.triggers.calendarinterval.CalendarIntervalTrigger
.. autoclass:: apscheduler.triggers.combining.AndTrigger
.. autoclass:: apscheduler.triggers.combining.OrTrigger
.. autoclass:: apscheduler.triggers.cron.CronTrigger

Events
------

.. autoclass:: apscheduler.Event
.. autoclass:: apscheduler.DataStoreEvent
.. autoclass:: apscheduler.TaskAdded
.. autoclass:: apscheduler.TaskUpdated
.. autoclass:: apscheduler.TaskRemoved
.. autoclass:: apscheduler.ScheduleAdded
.. autoclass:: apscheduler.ScheduleUpdated
.. autoclass:: apscheduler.ScheduleRemoved
.. autoclass:: apscheduler.JobAdded
.. autoclass:: apscheduler.JobRemoved
.. autoclass:: apscheduler.ScheduleDeserializationFailed
.. autoclass:: apscheduler.JobDeserializationFailed
.. autoclass:: apscheduler.SchedulerEvent
.. autoclass:: apscheduler.SchedulerStarted
.. autoclass:: apscheduler.SchedulerStopped
.. autoclass:: apscheduler.JobAcquired
.. autoclass:: apscheduler.JobReleased

Enumerated types
----------------

.. autoclass:: apscheduler.SchedulerRole()
    :show-inheritance:

.. autoclass:: apscheduler.RunState()
    :show-inheritance:

.. autoclass:: apscheduler.JobOutcome()
    :show-inheritance:

.. autoclass:: apscheduler.ConflictPolicy()
    :show-inheritance:

.. autoclass:: apscheduler.CoalescePolicy()
    :show-inheritance:

Context variables
-----------------

See the :mod:`contextvars` module for information on how to work with context variables.

.. data:: apscheduler.current_scheduler
   :type: ~contextvars.ContextVar[Scheduler]

   The current scheduler.

.. data:: apscheduler.current_async_scheduler
   :type: ~contextvars.ContextVar[AsyncScheduler]

   The current asynchronous scheduler.

.. data:: apscheduler.current_job
   :type: ~contextvars.ContextVar[Job]

   The job being currently run (available when running the job's target callable).

Exceptions
----------

.. autoexception:: apscheduler.TaskLookupError
.. autoexception:: apscheduler.ScheduleLookupError
.. autoexception:: apscheduler.JobLookupError
.. autoexception:: apscheduler.CallableLookupError
.. autoexception:: apscheduler.JobResultNotReady
.. autoexception:: apscheduler.JobCancelled
.. autoexception:: apscheduler.JobDeadlineMissed
.. autoexception:: apscheduler.ConflictingIdError
.. autoexception:: apscheduler.SerializationError
.. autoexception:: apscheduler.DeserializationError
.. autoexception:: apscheduler.MaxIterationsReached

Support classes for retrying failures
-------------------------------------

.. autoclass:: apscheduler.RetrySettings
.. autoclass:: apscheduler.RetryMixin

Support classes for unset options
---------------------------------

.. data:: apscheduler.unset

    Sentinel value for unset option values.

.. autoclass:: apscheduler.UnsetValue


================================================
FILE: docs/conf.py
================================================
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html

from __future__ import annotations

# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))


# -- Project information -----------------------------------------------------

project = "APScheduler"
copyright = "Alex Grönholm"
author = "Alex Grönholm"


# -- General configuration ---------------------------------------------------

# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
    "sphinx.ext.autodoc",
    "sphinx.ext.intersphinx",
    "sphinx_tabs.tabs",
    "sphinx_autodoc_typehints",
    "sphinx_rtd_theme",
]

# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []

autodoc_default_options = {"members": True}
autodoc_mock_imports = [
    "asyncpg",
    "bson",
    "cbor2",
    "paho",
    "pymongo",
    "psycopg",
    "redis",
    "sqlalchemy",
    "PyQt6",
]
autodoc_type_aliases = {
    "datetime": "datetime.datetime",
    "UUID": "uuid.UUID",
    "AsyncEngine": "sqlalchemy.ext.asyncio.AsyncEngine",
    "RetrySettings": "apscheduler.RetrySettings",
    "Serializer": "apscheduler.abc.Serializer",
}
nitpick_ignore = [("py:class", "datetime")]

# -- Options for HTML output -------------------------------------------------

# The theme to use for HTML and HTML Help pages.  See the documentation for
# a list of builtin themes.
#
html_theme = "sphinx_rtd_theme"

intersphinx_mapping = {
    "python": ("https://docs.python.org/3/", None),
    "anyio": ("https://anyio.readthedocs.io/en/latest/", None),
    "asyncpg": ("https://magicstack.github.io/asyncpg/current/", None),
    "cbor2": ("https://cbor2.readthedocs.io/en/latest/", None),
    "psycopg": ("https://www.psycopg.org/psycopg3/docs/", None),
    "pymongo": ("https://pymongo.readthedocs.io/en/stable", None),
    "sqlalchemy": ("https://docs.sqlalchemy.org/en/20/", None),
    "tenacity": ("https://tenacity.readthedocs.io/en/latest/", None),
}


================================================
FILE: docs/contributing.rst
================================================
Contributing to APScheduler
===========================

.. highlight:: bash

If you wish to contribute a fix or feature to APScheduler, please follow the following
guidelines.

When you make a pull request against the main APScheduler codebase, Github runs the test
suite against your modified code. Before making a pull request, you should ensure that
the modified code passes tests and code quality checks locally.

Running the test suite
----------------------

The test suite has dependencies on several external services, such as database servers.
To make this easy for the developer, a `docker compose`_ configuration is provided.
To use it, you need Docker_ (or a suitable replacement). On Linux, unless you're using
Docker Desktop, you may need to also install the compose (v2) plugin (named
``docker-compose-plugin``, or similar) separately.

Once you have the necessary tools installed, you can start the services with this
command::

    docker compose up -d

You can run the test suite two ways: either with tox_, or by running pytest_ directly.

To run tox_ against all supported (of those present on your system) Python versions::

    tox

Tox will handle the installation of dependencies in separate virtual environments.

To pass arguments to the underlying pytest_ command, you can add them after ``--``, like
this::

    tox -- -k somekeyword

To use pytest directly, you can set up a virtual environment and install the project in
development mode along with its test dependencies (virtualenv activation demonstrated
for Linux and macOS; on Windows you need ``venv\Scripts\activate`` instead)::

    python -m venv venv
    source venv/bin/activate
    pip install --group test -e .

Now you can just run pytest_::

    pytest

Building the documentation
--------------------------

To build the documentation, run ``tox -e docs``. This will place the documentation in
``build/sphinx/html`` where you can open ``index.html`` to view the formatted
documentation.

APScheduler uses ReadTheDocs_ to automatically build the documentation so the above
procedure is only necessary if you are modifying the documentation and wish to check the
results before committing.

APScheduler uses pre-commit_ to perform several code style/quality checks. It is
recommended to activate pre-commit_ on your local clone of the repository (using
``pre-commit install``) to ensure that your changes will pass the same checks on GitHub.

Making a pull request on Github
-------------------------------

To get your changes merged to the main codebase, you need a Github account.

#. Fork the repository (if you don't have your own fork of it yet) by navigating to the
   `main APScheduler repository`_ and clicking on "Fork" near the top right corner.
#. Clone the forked repository to your local machine with
   ``git clone git@github.com/yourusername/apscheduler``.
#. Create a branch for your pull request, like ``git checkout -b myfixname``
#. Make the desired changes to the code base.
#. Commit your changes locally. If your changes close an existing issue, add the text
   ``Fixes #XXX.`` or ``Closes #XXX.`` to the commit message (where XXX is the issue
   number).
#. Push the changeset(s) to your forked repository (``git push``)
#. Navigate to Pull requests page on the original repository (not your fork) and click
   "New pull request"
#. Click on the text "compare across forks".
#. Select your own fork as the head repository and then select the correct branch name.
#. Click on "Create pull request".

If you have trouble, consult the `pull request making guide`_ on opensource.com.

.. _Docker: https://docs.docker.com/desktop/#download-and-install
.. _docker compose: https://docs.docker.com/compose/
.. _tox: https://tox.readthedocs.io/en/latest/install.html
.. _pre-commit: https://pre-commit.com/#installation
.. _pytest: https://pypi.org/project/pytest/
.. _ReadTheDocs: https://readthedocs.org/
.. _main APScheduler repository: https://github.com/agronholm/apscheduler
.. _pull request making guide: https://opensource.com/article/19/7/create-pull-request-github


================================================
FILE: docs/extending.rst
================================================
#####################
Extending APScheduler
#####################

.. py:currentmodule:: apscheduler

This document is meant to explain how to develop your custom triggers and data stores.

Custom triggers
---------------

The built-in triggers cover the needs of the majority of all users, particularly so when
combined using :class:`~triggers.combining.AndTrigger` and
:class:`~triggers.combining.OrTrigger`. However, some users may need specialized
scheduling logic. This can be accomplished by creating your own custom trigger class.

To implement your scheduling logic, create a new class that inherits from the
:class:`~abc.Trigger` interface class::

    from __future__ import annotations

    from apscheduler.abc import Trigger

    class MyCustomTrigger(Trigger):
        def next() -> datetime | None:
            ... # Your custom logic here

        def __getstate__():
            ... # Return the serializable state here

        def __setstate__(state):
            ... # Restore the state from the return value of __getstate__()

Requirements and constraints for trigger classes:

* :meth:`~abc.Trigger.next` must always either return a timezone aware
  :class:`~datetime.datetime` object or :data:`None` if a new run time cannot be
  calculated
* :meth:`~abc.Trigger.next` must never return the same :class:`~datetime.datetime`
  twice and never one that is earlier than the previously returned one
* :meth:`~abc.Trigger.__setstate__` must accept the return value of
  :meth:`~abc.Trigger.__getstate__` and restore the trigger to the functionally same
  state as the original
* :meth:`~abc.Trigger.__getstate__` may only return an object containing types
  serializable by :class:`~abc.Serializer`

Triggers are stateful objects. The :meth:`~abc.Trigger.next` method is where you
determine the next run time based on the current state of the trigger. The trigger's
internal state needs to be updated before returning to ensure that the trigger won't
return the same datetime on the next call. The trigger code does **not** need to be
thread-safe.

Custom job executors
--------------------

.. py:currentmodule:: apscheduler

If you need the ability to use third party frameworks or services to handle the
actual execution of jobs, you will need a custom job executor.

A job executor needs to inherit from :class:`~abc.JobExecutor`. This interface contains
one abstract method you're required to implement: :meth:`~abc.JobExecutor.run_job`.
This method is called with two arguments:

#. ``func``: the callable you're supposed to call
#. ``job``: the :class:`Job` instance

The :meth:`~abc.JobExecutor.run_job` implementation needs to call ``func`` with the
positional and keyword arguments attached to the job (``job.args`` and ``job.kwargs``,
respectively). The return value of the callable must be returned from the method.

Here's an example of a simple job executor that runs a (synchronous) callable in a
thread::

    from contextlib import AsyncExitStack
    from functools import partial

    from anyio import to_thread
    from apscheduler import Job
    from apscheduler.abc import JobExecutor

    class ThreadJobExecutor(JobExecutor):
        async def run_job(self, func: Callable[..., Any], job: Job) -> Any:
            wrapped = partial(func, *job.args, **job.kwargs)
            return await to_thread.run_sync(wrapped)

If you need to initialize some underlying services, you can override the
:meth:`~abc.JobExecutor.start` method. For example, the executor above could be improved
to take a maximum number of threads and create an AnyIO
:class:`~anyio.CapacityLimiter`::

    from contextlib import AsyncExitStack
    from functools import partial

    from anyio import CapacityLimiter, to_thread
    from apscheduler import Job
    from apscheduler.abc import JobExecutor

    class ThreadJobExecutor(JobExecutor):
        _limiter: CapacityLimiter

        def __init__(self, max_threads: int):
            self.max_threads = max_threads

        async def start(self, exit_stack: AsyncExitStack) -> None:
            self._limiter = CapacityLimiter(self.max_workers)

        async def run_job(self, func: Callable[..., Any], job: Job) -> Any:
            wrapped = partial(func, *job.args, **job.kwargs)
            return await to_thread.run_sync(wrapped, limiter=self._limiter)

Custom data stores
------------------

If you want to make use of some external service to store the scheduler data, and it's
not covered by a built-in data store implementation, you may want to create a custom
data store class.

A data store implementation needs to inherit from :class:`~abc.DataStore` and implement
several abstract methods:

* :meth:`~abc.DataStore.start`
* :meth:`~abc.DataStore.add_task`
* :meth:`~abc.DataStore.remove_task`
* :meth:`~abc.DataStore.get_task`
* :meth:`~abc.DataStore.get_tasks`
* :meth:`~abc.DataStore.add_schedule`
* :meth:`~abc.DataStore.remove_schedules`
* :meth:`~abc.DataStore.get_schedules`
* :meth:`~abc.DataStore.acquire_schedules`
* :meth:`~abc.DataStore.release_schedules`
* :meth:`~abc.DataStore.get_next_schedule_run_time`
* :meth:`~abc.DataStore.add_job`
* :meth:`~abc.DataStore.get_jobs`
* :meth:`~abc.DataStore.acquire_jobs`
* :meth:`~abc.DataStore.release_job`
* :meth:`~abc.DataStore.get_job_result`
* :meth:`~abc.DataStore.extend_acquired_schedule_leases`
* :meth:`~abc.DataStore.extend_acquired_job_leases`
* :meth:`~abc.DataStore.cleanup`

The :meth:`~abc.DataStore.start` method is where your implementation can perform any
initialization, including starting any background tasks. This method is called with two
arguments:

#. ``exit_stack``: an :class:`~contextlib.AsyncExitStack` object that can be used to
   work with context managers
#. ``event_broker``: the event broker that the store should be using to send events to
   other components of the system (including other schedulers)

The data store class needs to inherit from :class:`~abc.DataStore`::

    from contextlib import AsyncExitStack

    from apscheduler.abc import DataStore, EventBroker

    class MyCustomDataStore(DataStore):
        _event_broker: EventBroker

        async def start(self, exit_stack: AsyncExitStack, event_broker: EventBroker) -> None:
            # Save the event broker in a member attribute and initialize the store
            self._event_broker = event_broker

        # See the interface class for the rest of the abstract methods

Handling temporary failures
+++++++++++++++++++++++++++

If you plan to make your data store implementation public, it is strongly recommended
that you make an effort to ensure that the implementation can tolerate the loss of
connectivity to the backing store. The Tenacity_ library is used for this purpose by the
built-in stores to retry operations in case of a disconnection. If you use it to retry
operations when exceptions are raised, it is important to only do that in cases of
*temporary* errors, like connectivity loss, and not in cases like authentication
failure, missing database and so forth. See the built-in data store implementations and
Tenacity_ documentation for more information on how to pick the exceptions on which to
retry the operations.

.. _Tenacity: https://pypi.org/project/tenacity/


================================================
FILE: docs/faq.rst
================================================
##########################
Frequently Asked Questions
##########################

Is there a graphical user interface for APScheduler?
====================================================

No graphical interface is provided by the library itself. However, there are some third
party implementations, but APScheduler developers are not responsible for them. Here is
a potentially incomplete list:

* django_apscheduler_
* apschedulerweb_
* `Nextdoor scheduler`_

.. warning:: As of this writing, these third party offerings have not been updated to
    work with APScheduler 4.

.. _django_apscheduler: https://pypi.org/project/django-apscheduler/
.. _Flask-APScheduler: https://pypi.org/project/flask-apscheduler/
.. _aiohttp: https://pypi.org/project/aiohttp/
.. _apschedulerweb: https://github.com/marwinxxii/apschedulerweb
.. _Nextdoor scheduler: https://github.com/Nextdoor/ndscheduler


================================================
FILE: docs/index.rst
================================================
Advanced Python Scheduler
=========================

.. include:: ../README.rst
   :end-before: Documentation


Table of Contents
=================

.. toctree::
  :maxdepth: 1

  userguide
  integrations
  versionhistory
  migration
  contributing
  extending
  faq
  api


================================================
FILE: docs/integrations.rst
================================================
Integrating with application frameworks
=======================================

.. py:currentmodule:: apscheduler

WSGI
----

To integrate APScheduler with web frameworks using WSGI_ (Web Server Gateway Interface),
you need to use the synchronous scheduler and start it as a side effect of importing the
module that contains your application instance::

    from apscheduler import Scheduler


    def app(environ, start_response):
        """Trivial example of a WSGI application."""
        response_body = b"Hello, World!"
        response_headers = [
            ("Content-Type", "text/plain"),
            ("Content-Length", str(len(response_body))),
        ]
        start_response(200, response_headers)
        return [response_body]


    scheduler = Scheduler()
    scheduler.start_in_background()

Assuming you saved this as ``example.py``, you can now start the application with uWSGI_
with:

.. code-block:: bash

    uwsgi --enable-threads --http :8080 --wsgi-file example.py

The ``--enable-threads`` (or ``-T``) option is necessary because uWSGI disables threads
by default which then prevents the scheduler from working. See the
`uWSGI documentation <uWSGI-threads>`_ for more details.

.. note::
    The :meth:`Scheduler.start_in_background` method installs an
    :mod:`atexit` hook that shuts down the scheduler gracefully when the worker process
    exits.

.. _WSGI: https://wsgi.readthedocs.io/en/latest/what.html
.. _uWSGI: https://www.fullstackpython.com/uwsgi.html
.. _uWSGI-threads: https://uwsgi-docs.readthedocs.io/en/latest/WSGIquickstart.html#a-note-on-python-threads

ASGI
----

To integrate APScheduler with web frameworks using ASGI_ (Asynchronous Server Gateway
Interface), you need to use the asynchronous scheduler and tie its lifespan to the
lifespan of the application by wrapping it in middleware, as follows::

    from apscheduler import AsyncScheduler


    async def app(scope, receive, send):
        """Trivial example of an ASGI application."""
        if scope["type"] == "http":
            await receive()
            await send(
                {
                    "type": "http.response.start",
                    "status": 200,
                    "headers": [
                        [b"content-type", b"text/plain"],
                    ],
                }
            )
            await send(
                {
                    "type": "http.response.body",
                    "body": b"Hello, world!",
                    "more_body": False,
                }
            )
        elif scope["type"] == "lifespan":
            while True:
                message = await receive()
                if message["type"] == "lifespan.startup":
                    await send({"type": "lifespan.startup.complete"})
                elif message["type"] == "lifespan.shutdown":
                    await send({"type": "lifespan.shutdown.complete"})
                    return


    async def scheduler_middleware(scope, receive, send):
        if scope['type'] == 'lifespan':
            async with AsyncScheduler() as scheduler:
                await app(scope, receive, send)
        else:
            await app(scope, receive, send)

Assuming you saved this as ``example.py``, you can then run this with Hypercorn_:

.. code-block:: bash

    hypercorn example:scheduler_middleware

or with Uvicorn_:

.. code-block:: bash

    uvicorn example:scheduler_middleware

.. _ASGI: https://asgi.readthedocs.io/en/latest/index.html
.. _Hypercorn: https://gitlab.com/pgjones/hypercorn/
.. _Uvicorn: https://www.uvicorn.org/


================================================
FILE: docs/migration.rst
================================================
###############################################
Migrating from previous versions of APScheduler
###############################################

.. py:currentmodule:: apscheduler

From v3.x to v4.0
=================

APScheduler 4.0 has undergone a partial rewrite since the 3.x series.

There is currently no way to automatically import schedules from a persistent 3.x job
store, but this shortcoming will be rectified before the final v4.0 release.

Terminology and architectural design changes
--------------------------------------------

The concept of a *job* has been split into :class:`Task`, :class:`Schedule` and
:class:`Job`. See the documentation of each class (and read the tutorial) to understand
their roles.

**Data stores**, previously called *job stores*, have been redesigned to work with
multiple running schedulers and workers, both for purposes of scalability and fault
tolerance. Many data store implementations were dropped because they were either too
burdensome to support, or the backing services were not sophisticated enough to handle
the increased requirements.

**Event brokers** are a new component in v4.0. They relay events between schedulers and
workers, enabling them to work together with a shared data store. External (as opposed
to local) event broker services are required in multi-node or multi-process deployment
scenarios.

**Triggers** are now stateful. This change was found to be necessary to properly support
combining triggers (:class:`~.triggers.combining.AndTrigger` and
:class:`~.triggers.combining.OrTrigger`), as they needed to keep track of the next run
times of all the triggers contained within. This change also enables some more
sophisticated custom trigger implementations.

**Time zone** support has been revamped to use :mod:`zoneinfo` (or `backports.zoneinfo`_
on Python versions earlier than 3.9) zones instead of pytz zones. You should not use
pytz with APScheduler anymore.

`Entry points`_ are no longer used or supported, as they were more trouble than they
were worth, particularly with packagers like py2exe or PyInstaller which by default did
not package distribution metadata. Thus, triggers and data stores have to be explicitly
instantiated.

.. _backports.zoneinfo: https://pypi.org/project/backports.zoneinfo/
.. _Entry points: https://packaging.python.org/en/latest/specifications/entry-points/

Scheduler changes
-----------------

The ``add_job()`` method is now :meth:`~Scheduler.add_schedule`. The scheduler still has
a method named :meth:`~Scheduler.add_job`, but this is meant for making one-off runs of
a task. Previously you would have had to call ``add_job()`` with a
:class:`~triggers.date.DateTrigger` using the current time as the run time.

The two most commonly used schedulers, ``BlockingScheduler`` and
``BackgroundScheduler``, have often caused confusion among users and have thus been
combined into :class:`~Scheduler`. This new unified scheduler class has two methods that
replace the ``start()`` method used previously: :meth:`~Scheduler.run_until_stopped` and
:meth:`~Scheduler.start_in_background`. The former should be used if you previously used
``BlockingScheduler``, and the latter if you used ``BackgroundScheduler``.

The asyncio scheduler has been replaced with a more generic :class:`AsyncScheduler`,
which is based on AnyIO_ and thus also supports Trio_ in addition to :mod:`asyncio`.
The API of the async scheduler differs somewhat from its synchronous counterpart. In
particular, it **requires** itself to be used as an async context manager – whereas with
the synchronous scheduler, use as a context manager is recommended but not required.

All other scheduler implementations have been dropped because they were either too
burdensome to support, or did not seem necessary anymore. Some of the dropped
implementations (particularly Qt) are likely to be re-added before v4.0 final.

Schedulers no longer support multiple data stores. If you need this capability, you
should run multiple schedulers instead.

Configuring and running the scheduler has been radically simplified. The ``configure()``
method is gone, and all configuration is now passed as keyword arguments to the
scheduler class.

.. _AnyIO: https://pypi.org/project/anyio/
.. _Trio: https://pypi.org/project/trio/

Trigger changes
---------------

As the scheduler is no longer used to create triggers, any supplied datetimes will be
assumed to be in the local time zone. If you wish to change the local time zone, you
should set the ``TZ`` environment variable to either the name of the desired timezone
(e.g. ``Europe/Helsinki``) or to a path of a time zone file. See the tzlocal_
documentation for more information.

**Jitter** support has been moved from individual triggers to the schedule level.
This not only simplified trigger design, but also enabled the scheduler to provide
information about the randomized jitter and the original run time to the user.

:class:`~triggers.cron.CronTrigger` was changed to respect the standard order of
weekdays, so that Sunday is now 0 and Saturday is 6. If you used numbered weekdays
before, you must change your trigger configuration to match. If in doubt, use
abbreviated weekday names (e.g. ``sun``, ``fri``) instead.

:class:`~triggers.interval.IntervalTrigger` was changed to start immediately, instead
of waiting for the first interval to pass. If you have workarounds in place to "fix"
the previous behavior, you should remove them.

.. _tzlocal: https://pypi.org/project/tzlocal/

From v3.0 to v3.2
=================

Prior to v3.1, the scheduler inadvertently exposed the ability to fetch and manipulate
jobs before the scheduler had been started. The scheduler now requires you to call
``scheduler.start()`` before attempting to access any of the jobs in the job stores. To
ensure that no old jobs are mistakenly executed, you can start the scheduler in paused
mode (``scheduler.start(paused=True)``) (introduced in v3.2) to avoid any premature job
processing.


From v2.x to v3.0
=================

The 3.0 series is API incompatible with previous releases due to a design overhaul.

Scheduler changes
-----------------

* The concept of "standalone mode" is gone. For ``standalone=True``, use
  ``BlockingScheduler`` instead, and for ``standalone=False``, use
  ``BackgroundScheduler``. BackgroundScheduler matches the old default semantics.
* Job defaults (like ``misfire_grace_time`` and ``coalesce``) must now be passed in a
  dictionary as the ``job_defaults`` option to ``BaseScheduler.configure()``. When
  supplying an ini-style configuration as the first argument, they will need a
  corresponding ``job_defaults.`` prefix.
* The configuration key prefix for job stores was changed from ``jobstore.`` to
  ``jobstores.`` to match the dict-style configuration better.
* The ``max_runs`` option has been dropped since the run counter could not be reliably
  preserved when replacing a job with another one with the same ID. To make up for this,
  the ``end_date`` option was added to cron and interval triggers.
* The old thread pool is gone, replaced by ``ThreadPoolExecutor``.
  This means that the old ``threadpool`` options are no longer valid.
* The trigger-specific scheduling methods have been removed entirely from the scheduler.
  Use the generic ``BaseScheduler.add_job()`` method or the
  ``@BaseScheduler.scheduled_job`` decorator instead. The signatures of these methods
  were changed significantly.
* The ``shutdown_threadpool`` and ``close_jobstores`` options have been removed from the
  ``BaseScheduler.shutdown()`` method.
  Executors and job stores are now always shut down on scheduler shutdown.
* ``Scheduler.unschedule_job()`` and ``Scheduler.unschedule_func()`` have been replaced
  by ``BaseScheduler.remove_job()``. You can also unschedule a job by using the job
  handle returned from ``BaseScheduler.add_job()``.

Job store changes
-----------------

The job store system was completely overhauled for both efficiency and forwards
compatibility. Unfortunately, this means that the old data is not compatible with the
new job stores. If you need to migrate existing data from APScheduler 2.x to 3.x,
contact the APScheduler author.

The Shelve job store had to be dropped because it could not support the new job store
design. Use SQLAlchemyJobStore with SQLite instead.

Trigger changes
---------------

From 3.0 onwards, triggers now require a pytz timezone. This is normally provided by the
scheduler, but if you were instantiating triggers manually before, then one must be
supplied as the ``timezone`` argument.

The only other backwards incompatible change was that ``get_next_fire_time()`` takes two
arguments now: the previous fire time and the current datetime.


From v1.x to 2.0
================

There have been some API changes since the 1.x series. This document
explains the changes made to v2.0 that are incompatible with the v1.x API.

API changes
-----------

* The behavior of cron scheduling with regards to default values for omitted
  fields has been made more intuitive -- omitted fields lower than the least
  significant explicitly defined field will default to their minimum values
  except for the week number and weekday fields
* SchedulerShutdownError has been removed -- jobs are now added tentatively
  and scheduled for real when/if the scheduler is restarted
* Scheduler.is_job_active() has been removed -- use
  ``job in scheduler.get_jobs()`` instead
* dump_jobs() is now print_jobs() and prints directly to the given file or
  sys.stdout if none is given
* The ``repeat`` parameter was removed from
  ``Scheduler.add_interval_job()`` and ``@Scheduler.interval_schedule`` in favor of the
  universal ``max_runs`` option
* ``Scheduler.unschedule_func()`` now raises a :exc:`KeyError` if the given function is
  not scheduled
* The semantics of ``Scheduler.shutdown()`` have changed – the method no longer accepts
  a numeric argument, but two booleans


Configuration changes
---------------------

* The scheduler can no longer be reconfigured while it's running


================================================
FILE: docs/userguide.rst
================================================
##########
User guide
##########

.. py:currentmodule:: apscheduler

Installation
============

The preferred installation method is by using
`pip <http://pypi.python.org/pypi/pip/>`_::

    $ pip install apscheduler

If you don't have pip installed, you need to
`install that first <https://pip.pypa.io/en/stable/installation/>`_.

Interfacing with certain external services may need extra dependencies which are
installable as extras:

* ``asyncpg``: for the AsyncPG event broker
* ``cbor``: for the CBOR serializer
* ``mongodb``: for the MongoDB data store
* ``mqtt``: for the MQTT event broker
* ``psycopg``: for the Psycopg event broker
* ``redis``: for the Redis event broker
* ``sqlalchemy``: for the SQLAlchemy data store

Using the extras instead of adding the corresponding libraries separately helps ensure
that you will have compatible versions of the dependent libraries going forward.

You can install any number of these extras with APScheduler by providing them as a comma
separated list inside the brackets, like this::

    pip install apscheduler[psycopg,sqlalchemy]

Code examples
=============

The source distribution contains the :file:`examples` directory where you can find many
working examples for using APScheduler in different ways. The examples can also be
`browsed online
<https://github.com/agronholm/apscheduler/tree/master/examples/?at=master>`_.


Introduction
============

The core concept of APScheduler is to give the user the ability to queue Python code to
be executed, either as soon as possible, later at a given time, or on a recurring
schedule.

The *scheduler* is the user-facing interface of the system. When it's running, it does
two things concurrently. The first is processing *schedules*. From its *data store*,
it fetches :ref:`schedules <schedule>` due to be run. For each such schedule, it then
uses the schedule's trigger_ to calculate run times up to the present. The scheduler
then creates one or more jobs (controllable by configuration) based on these run times
and adds them to the data store.

The second role of the scheduler is running :ref:`jobs <job>`. The scheduler asks the
`data store`_ for jobs, and then starts running those jobs. If the data store signals
that it has new jobs, the scheduler will try to acquire those jobs if it is capable of
accommodating more. When a scheduler completes a job, it will then also ask the data
store for as many more jobs as it can handle.

By default, schedulers operate in both of these roles, but can be configured to only
process schedules or run jobs if deemed necessary. It may even be desirable to use the
scheduler only as an interface to an external data store while leaving schedule and job
processing to other scheduler instances running elsewhere.

Basic concepts / glossary
=========================

These are the basic components and concepts of APScheduler which will be referenced
later in this guide.

.. _callable:

A *callable* is any object that returns ``True`` from :func:`callable`. These are:

* A free function (``def something(...): ...``)
* An instance method (``class Foo: ... def something(self, ...): ...``)
* A class method (``class Foo: ... @classmethod ... def something(cls, ...): ...``)
* A static method (``class Foo: ... @staticmethod ... def something(...): ...``)
* A lambda (``lambda a, b: a + b``)
* An instance of a class that contains a method named ``__call__``)

.. _task:

A *task* encapsulates a callable_ and a number of configuration parameters. They are
often implicitly defined as a side effect of the user creating a new schedule against a
callable_, but can also be :ref:`explicitly defined beforehand <configuring-tasks>`.

Tasks have three different roles:

#. They provide the target callable to be run when a job is started
#. They provide a key (task ID) on which to limit the maximum number of concurrent jobs,
   even between different schedules
#. They provide a template from which certain parameters, like job executor and misfire
   grace time, are copied to schedules and jobs derived from the task

.. _trigger:

A trigger_ contains the logic and state used to calculate when a scheduled task_ should
be run.

.. _schedule:

A *schedule* combines a task_ with a trigger_, plus a number of configuration
parameters.

.. _job:

A *job* is request for a task_ to be run. It can be created automatically from a
schedule when a scheduler processes it, or it can be directly created by the user if
they directly request a task_ to be run.

.. _data store:

A *data store* is used to store :ref:`schedules <schedule>` and :ref:`jobs <job>`, and
to keep track of :ref:`tasks <task>`.

.. _job executor:

A *job executor* runs the job_, by calling the function associated with the job's task.
An executor could directly call the callable_, or do it in another thread, subprocess or
even some external service.

.. _event broker:

An *event broker* delivers published events to all interested parties. It facilitates
the cooperation between schedulers by notifying them of new or updated
:ref:`schedules <schedule>` and :ref:`jobs <job>`.

.. _scheduler:

A *scheduler* is the main interface of this library. It houses both a `data store`_ and
an `event broker`_, plus one or more :ref:`job executors <job executor>`. It contains
methods users can use to work with tasks, schedules and jobs. Behind the scenes, it also
processes due schedules, spawning jobs and updating the next run times. It also
processes available jobs, making the appropriate :ref:`job executors <job executor>` to
run them, and then sending back the results to the `data store`_.

Running the scheduler
=====================

The scheduler_ comes in two flavors: synchronous and asynchronous. The synchronous
scheduler actually runs an asynchronous scheduler behind the scenes in a dedicated
thread, so if your app runs on :mod:`asyncio` or Trio_, you should prefer the
asynchronous scheduler.

The scheduler can run either in the foreground, blocking on a call to
:meth:`~Scheduler.run_until_stopped`, or in the background where it does its work while
letting the rest of the program run.

If the only intent of your program is to run scheduled tasks, then you should start the
scheduler with :meth:`~Scheduler.run_until_stopped`. But if you need to do other things
too, then you should call :meth:`~Scheduler.start_in_background` before running the rest
of the program.

In almost all cases, the scheduler should be used as a context manager. This initializes
the underlying `data store`_ and `event broker`_, allowing you to use the scheduler for
manipulating :ref:`tasks <task>`, :ref:`schedules <schedule>` and jobs prior to starting
the processing of schedules and jobs. Exiting the context manager will shut down the
scheduler and its underlying services. This mode of operation is mandatory for the
asynchronous scheduler when running it in the background, but it is preferred for the
synchronous scheduler too.

As a special consideration (for use with WSGI_ based web frameworks), the synchronous
scheduler can be run in the background without being used as a context manager. In this
scenario, the scheduler adds an :mod:`atexit` hook that will perform an orderly shutdown
of the scheduler before the process terminates.

.. _WSGI: https://wsgi.readthedocs.io/en/latest/what.html
.. _Trio: https://trio.readthedocs.io/en/stable/

.. warning:: If you start the scheduler in the background and let the script finish
   execution, the scheduler will automatically shut down as well.

.. tabs::

   .. code-tab:: python Synchronous (run in foreground)

      from apscheduler import Scheduler

      with Scheduler() as scheduler:
          # Add schedules, configure tasks here
          scheduler.run_until_stopped()

   .. code-tab:: python Synchronous (background thread; preferred method)

      from apscheduler import Scheduler

      with Scheduler() as scheduler:
          # Add schedules, configure tasks here
          scheduler.start_in_background()

   .. code-tab:: python Synchronous (background thread; WSGI alternative)

      from apscheduler import Scheduler

      scheduler = Scheduler()
      # Add schedules, configure tasks here
      scheduler.start_in_background()

   .. code-tab:: python Asynchronous (run in foreground)

      import asyncio

      from apscheduler import AsyncScheduler

      async def main():
          async with AsyncScheduler() as scheduler:
              # Add schedules, configure tasks here
              await scheduler.run_until_stopped()

     asyncio.run(main())

   .. code-tab:: python Asynchronous (background task)

      import asyncio

      from apscheduler import AsyncScheduler

      async def main():
          async with AsyncScheduler() as scheduler:
              # Add schedules, configure tasks here
              await scheduler.start_in_background()

     asyncio.run(main())

.. _configuring-tasks:

Configuring tasks
=================

In order to add :ref:`schedules <schedule>` or :ref:`jobs <job>` to the `data store`_,
you need to have a task_ that defines which callable_ will be called when each job_ is
run.

In most cases, you don't need to go through this step, and instead have a task_
implicitly created for you by the methods that add schedules or jobs.

Explicitly configuring a task is generally only necessary in the following cases:

* You need to have more than one task with the same callable
* You need to set any of the task settings to non-default values
* You need to add schedules/jobs targeting lambdas, nested functions or instances of
  unserializable classes

There are two ways to explicitly configure tasks:

#. Call the :meth:`~Scheduler.configure_task` scheduler method
#. Decorate your target function with :func:`@task <task>`

.. seealso:: :ref:`settings_inheritance`

Limiting the number of concurrently executing instances of a job
----------------------------------------------------------------

**Option**: ``max_running_jobs``

It is possible to control the maximum number of concurrently running jobs for a
particular task. By default, only one job is allowed to be run for every task.
This means that if the job is about to be run but there is another job for the same task
still running, the later job is terminated with the outcome of
:attr:`~JobOutcome.missed_start_deadline`.

To allow more jobs to be concurrently running for a task, pass the desired maximum
number as the ``max_running_jobs`` keyword argument to :meth:`~Scheduler.add_schedule`.

.. _controlling-how-much-a-job-can-be-started-late:

Controlling how much a job can be started late
----------------------------------------------

**Option**: ``misfire_grace_time``

This option applies to scheduled jobs. Some tasks are time sensitive, and should not be
run at all if they fail to be started on time (like, for example, if the scheduler(s)
were down while they were supposed to be running the scheduled jobs). When a scheduler
acquires jobs, the data store discards any jobs that have passed their start deadlines
(scheduled time + ``misfire_grace_time``). Such jobs are released with the outcome of
:attr:`~JobOutcome.missed_start_deadline`.

Adding custom metadata
----------------------

**Option**: ``metadata``

This option allows adding custom, JSON compatible metadata to tasks, schedules and jobs.
Here, "JSON compatible" means the following restrictions:

* The top-level metadata object must be a :class:`dict`
* All :class:`dict` keys must be strings
* Values can be :class:`int`, :class:`float`, :class:`str`, :class:`bool` or
  :data:`None`

.. note:: Top level metadata keys are merged with any explicitly passed values, in such
    a way that explicitly passed values override any values from the task level.

.. _settings_inheritance:

Inheritance of settings
-----------------------

When tasks are configured, or schedules or jobs created, they will inherit the settings
of any "parent" object according to the following rules:

* Task configuration parameters are resolving according to the following, descending
  priority order:

  #. Parameters passed directly to :meth:`~AsyncScheduler.configure_task`
  #. Parameters bound to the target function via :func:`@task <task>`
  #. The scheduler's task defaults
* Schedules inherit settings from the their respective tasks
* Jobs created from schedules inherit the settings from their parent schedules
* Jobs created directly inherit the settings from their parent tasks

The ``metadata`` parameter works a bit differently. Top level keys will be merged in
such a way that keys on a more explicit configuration level keys will overwrite keys
from a more generic level.

If any parameter is unset, it will be looked up on the next level. Here is an example
that illustrates the lookup order::

    from apscheduler import Scheduler, TaskDefaults, task

    @task(max_running_jobs=3, metadata={"foo": ["taskfunc"]})
    def mytaskfunc():
        print("running stuff")

    task_defaults = TaskDefaults(
        misfire_grace_time=15,
        job_executor="processpool",
        metadata={"global": 3, "foo": ["bar"]}
    )
    with Scheduler(task_defaults=task_defaults) as scheduler:
        scheduler.configure_task(
            "sometask",
            func=mytaskfunc,
            job_executor="threadpool",
            metadata={"direct": True}
        )

The resulting task will have the following parameters:

* ``id``: ``'sometask'`` (from the :meth:`~AsyncScheduler.configure_task` call)
* ``job_executor``: ``'threadpool'`` (from the :meth:`~AsyncScheduler.configure_task`
  call, where it overrides the scheduler-level default)
* ``max_running_jobs``: 3 (from the decorator)
* ``misfire_grace_time``: 15 (from the scheduler-level default)
* ``metadata``: ``{"global": 3, "foo": ["taskfunc"], "direct": True}``

Scheduling tasks
================

To create a schedule for running a task, you need, at the minimum:

* A preconfigured task_, OR a callable_ to be run
* A trigger_

If you've configured a task (as per the previous section), you can pass the task object
or its ID to :meth:`Scheduler.add_schedule`. As a shortcut, you can pass a callable_
instead, in which case a task will be automatically created for you if necessary.

If the callable you're trying to schedule is either a lambda or a nested function, then
you need to explicitly create a task beforehand, as it is not possible to create a
reference (``package.module:varname``) to these types of callables.

The trigger determines the scheduling logic for your schedule. In other words, it is
used to calculate the datetimes on which the task will be run. APScheduler comes with a
number of built-in trigger classes:

* :class:`~triggers.date.DateTrigger`:
  use when you want to run the task just once at a certain point of time
* :class:`~triggers.interval.IntervalTrigger`:
  use when you want to run the task at fixed intervals of time
* :class:`~triggers.cron.CronTrigger`:
  use when you want to run the task periodically at certain time(s) of day
* :class:`~triggers.calendarinterval.CalendarIntervalTrigger`:
  use when you want to run the task on calendar-based intervals, at a specific time of
  day

Combining multiple triggers
---------------------------

Occasionally, you may find yourself in a situation where your scheduling needs are too
complex to be handled with any of the built-in triggers directly.

One examples of such a need would be when you want the task to run at 10:00 from Monday
to Friday, but also at 11:00 from Saturday to Sunday.
A single :class:`~triggers.cron.CronTrigger` would not be able to handle
this case, but an :class:`~triggers.combining.OrTrigger` containing two cron
triggers can::

    from apscheduler.triggers.combining import OrTrigger
    from apscheduler.triggers.cron import CronTrigger

    trigger = OrTrigger(
        CronTrigger(day_of_week="mon-fri", hour=10),
        CronTrigger(day_of_week="sat-sun", hour=11),
    )

On the first run, :class:`~triggers.combining.OrTrigger` generates the next
run times from both cron triggers and saves them internally. It then returns the
earliest one. On the next run, it generates a new run time from the trigger that
produced the earliest run time on the previous run, and then again returns the earliest
of the two run times. This goes on until all the triggers have been exhausted, if ever.

Another example would be a case where you want the task to be run every 2 months at
10:00, but not on weekends (Saturday or Sunday)::

    from apscheduler.triggers.calendarinterval import CalendarIntervalTrigger
    from apscheduler.triggers.combining import AndTrigger
    from apscheduler.triggers.cron import CronTrigger

    trigger = AndTrigger(
        CalendarIntervalTrigger(months=2, hour=10),
        CronTrigger(day_of_week="mon-fri", hour=10),
    )

On the first run, :class:`~triggers.combining.AndTrigger` generates the next
run times from both the
:class:`~triggers.calendarinterval.CalendarIntervalTrigger` and
:class:`~triggers.cron.CronTrigger`. If the run times coincide, it will
return that run time. Otherwise, it will calculate a new run time from the trigger that
produced the earliest run time. It will keep doing this until a match is found, one of
the triggers has been exhausted or the maximum number of iterations (1000 by default) is
reached.

If this trigger is created on 2022-06-07 at 09:00:00, its first run times would be:

* 2022-06-07 10:00:00
* 2022-10-07 10:00:00
* 2022-12-07 10:00:00

Notably, 2022-08-07 is skipped because it falls on a Sunday.

Removing schedules
------------------

To remove a previously added schedule, call
:meth:`~Scheduler.remove_schedule`. Pass the identifier of
the schedule you want to remove as an argument. This is the ID you got from
:meth:`~Scheduler.add_schedule`.

Note that removing a schedule does not cancel any jobs derived from it, but does prevent
further jobs from being created from that schedule.

Pausing schedules
-----------------

To pause a schedule, call :meth:`~Scheduler.pause_schedule`. Pass the identifier of the
schedule you want to pause as an argument. This is the ID you got from
:meth:`~Scheduler.add_schedule`.

Pausing a schedule prevents any new jobs from being created from it, but does not cancel
any jobs that have already been created from that schedule.

The schedule can be unpaused by calling :meth:`~Scheduler.unpause_schedule` with the
identifier of the schedule you want to unpause.

By default the schedule will retain the next fire time it had when it was paused, which
may result in the schedule being considered to have misfired when it is unpaused,
resulting in whatever misfire behavior it has configured
(see :ref:`controlling-how-much-a-job-can-be-started-late` for more details).

The ``resume_from`` parameter can be used to specify the time from which the schedule
should be resumed. This can be used to avoid the misfire behavior mentioned above. It
can be either a datetime object, or the string ``"now"`` as a convenient shorthand for
the current datetime. If this parameter is provided, the schedules trigger will be
repeatedly advanced to determine a next fire time that is at or after the specified time
to resume from.

Controlling how jobs are queued from schedules
----------------------------------------------

In most cases, when a scheduler processes a schedule, it queues a new job using the
run time currently marked for the schedule. Then it updates the next run time using the
schedule's trigger and releases the schedule back to the data store. But sometimes a
situation occurs where the schedule did not get processed often or quickly enough, and
one or more next run times produced by the trigger are actually in the past.

In a situation like that, the scheduler needs to decide what to do: to queue a job for
every run time produced, or to *coalesce* them all into a single job, effectively just
kicking off a single job. To control this, pass the ``coalesce`` argument to
:meth:`~Scheduler.add_schedule`.

The possible values are:

* :data:`~CoalescePolicy.latest`: queue exactly one job, using the
  **latest** run time as the designated run time
* :data:`~CoalescePolicy.earliest`: queue exactly one job, using the
  **earliest** run time as the designated run time
* :data:`~CoalescePolicy.all`: queue one job for **each** of the calculated
  run times

The biggest difference between the first two options is how the designated run time, and
by extension, the starting deadline for the job is selected. With the first option,
the job is less likely to be skipped due to being started late since the latest of all
the collected run times is used for the deadline calculation.

As explained in the previous section, the starting
deadline is *misfire grace time*
affects the newly queued job.

Running tasks without scheduling
================================

In some cases, you want to run tasks directly, without involving schedules:

* You're only interested in using the scheduler system as a job queue
* You're interested in the job's return value

To queue a job and wait for its completion and get the result, the easiest way is to
use :meth:`~Scheduler.run_job`. If you prefer to just launch a job and not wait for its
result, use :meth:`~Scheduler.add_job` instead. If you want to get the results later,
you need to pass an appropriate ``result_expiration_time`` parameter to
:meth:`~Scheduler.add_job` so that the result is saved. Then, you can call
:meth:`~Scheduler.get_job_result` with the job ID you got from
:meth:`~Scheduler.add_job` to retrieve the result.

Context variables
=================

Schedulers provide certain `context variables`_ available to the tasks being run:

* The current (synchronous) scheduler: :data:`~current_scheduler`
* The current asynchronous scheduler: :data:`~current_async_scheduler`
* Information about the job being currently run: :data:`~current_job`

Here's an example::

    from apscheduler import current_job

    def my_task_function():
        job_info = current_job.get().id
        print(
            f"This is job {job_info.id} and was spawned from schedule "
            f"{job_info.schedule_id}"
        )

.. _context variables: :mod:`contextvars`

.. _scheduler-events:

Subscribing to events
=====================

Schedulers have the ability to notify listeners when some event occurs in the scheduler
system. Examples of such events would be schedulers or workers starting up or shutting
down, or schedules or jobs being created or removed from the data store.

To listen to events, you need a callable_ that takes a single positional argument
which is the event object. Then, you need to decide which events you're interested in:

.. tabs::

    .. code-tab:: python Synchronous

        from apscheduler import Event, JobAcquired, JobReleased

        def listener(event: Event) -> None:
            print(f"Received {event.__class__.__name__}")

        scheduler.subscribe(listener, {JobAcquired, JobReleased})

    .. code-tab:: python Asynchronous

        from apscheduler import Event, JobAcquired, JobReleased

        async def listener(event: Event) -> None:
            print(f"Received {event.__class__.__name__}")

        scheduler.subscribe(listener, {JobAcquired, JobReleased})

This example subscribes to the :class:`~JobAcquired` and
:class:`~JobReleased` event types. The callback will receive an event of
either type, and prints the name of the class of the received event.

Asynchronous schedulers and workers support both synchronous and asynchronous callbacks,
but their synchronous counterparts only support synchronous callbacks.

When **distributed** event brokers (that is, other than the default one) are being used,
events other than the ones relating to the life cycles of schedulers and workers, will
be sent to all schedulers and workers connected to that event broker.

Clean-up of expired jobs, job results and schedules
===================================================

Each scheduler runs the data store's :meth:`~.abc.DataStore.cleanup` method
periodically, configurable via the ``cleanup_interval`` scheduler parameter. This
ensures that the data store doesn't get filled with unused data over time.

Deployment
==========

Using persistent data stores
----------------------------

The default data store, :class:`~datastores.memory.MemoryDataStore`, stores
data only in memory so all the schedules and jobs that were added to it will be erased
if the process crashes.

When you need your schedules and jobs to survive the application shutting down, you need
to use a *persistent data store*. Such data stores do have additional considerations,
compared to the memory data store:

* Task arguments must be *serializable*
* You must either trust the data store, or use an alternate *serializer*
* A *conflict policy* and an *explicit identifier* must be defined for schedules that
  are added at application startup

These requirements warrant some explanation. The first point means that since persisting
data means saving it externally, either in a file or sending to a database server, all
the objects involved are converted to bytestrings. This process is called
*serialization*. By default, this is done using :mod:`pickle`, which guarantees the best
compatibility but is notorious for being vulnerable to simple injection attacks. This
brings us to the second point. If you cannot be sure that nobody can maliciously alter
the externally stored serialized data, it would be best to use another serializer. The
built-in alternatives are:

* :class:`~serializers.cbor.CBORSerializer`
* :class:`~serializers.json.JSONSerializer`

The former requires the cbor2_ library, but supports a wider variety of types natively.
The latter has no dependencies but has very limited support for different types.

The third point relates to situations where you're essentially adding the same schedule
to the data store over and over again. If you don't specify a static identifier for
the schedules added at the start of the application, you will end up with an increasing
number of redundant schedules doing the same thing, which is probably not what you want.
To that end, you will need to come up with some identifying name which will ensure that
the same schedule will not be added over and over again (as data stores are required to
enforce the uniqueness of schedule identifiers). You'll also need to decide what to do
if the schedule already exists in the data store (that is, when the application is
started the second time) by passing the ``conflict_policy`` argument. Usually you want
the :data:`~ConflictPolicy.replace` option, which replaces the existing
schedule with the new one.

.. seealso:: You can find practical examples of persistent data stores in the
    :file:`examples/standalone` directory (``async_postgres.py`` and
    ``async_mysql.py``).

.. _cbor2: https://pypi.org/project/cbor2/

Using multiple schedulers
-------------------------

There are several situations in which you would want to run several schedulers against
the same data store at once:

* Running a server application (usually a web app) with multiple worker processes
* You need fault tolerance (scheduling will continue even if a node or process running
  a scheduler goes down)

When you have multiple schedulers running at once, they need to be able to coordinate
their efforts so that the schedules don't get processed more than once and the
schedulers know when to wake up even if another scheduler added the next due schedule to
the data store. To this end, a shared *event broker* must be configured.

.. seealso:: You can find practical examples of data store sharing in the
    :file:`examples/web` directory.

Using a scheduler without running it
------------------------------------

Some deployment scenarios may warrant the use of a scheduler for only interfacing with
an external data store, for things like configuring tasks, adding schedules or queuing
jobs. One such practical use case is a web application that needs to run heavy
computations elsewhere so they don't cause performance issues with the web application
itself.

You can then run one or more schedulers against the same data store and event broker
elsewhere where they don't disturb the web application. These schedulers will do all the
heavy lifting like processing schedules and running jobs.

.. seealso:: A practical example of this separation of concerns can be found in the
    :file:`examples/separate_worker` directory.

Explicitly assigning an identity to the scheduler
-------------------------------------------------

If you're running one or more schedulers against a persistent data store in a production
setting, it'd be wise to assign each scheduler a custom identity. The reason for this is
twofold:

#. It helps you figure out which jobs are being run where
#. It allows crashed jobs to cleared out quicker, as other schedulers aren't allowed to
   clean them up until the jobs' timeouts expire

The best choice would be something that the environment guarantees to be unique among
all the scheduler instances but stays the same when the scheduler instance is restarted.
For example, on Kubernetes, this would be the name of the pod where the scheduler is
running, assuming of course that there is only one scheduler running in each pod against
the same data store.

Of course, if you're only ever running one scheduler against a persistent data store,
you can just use a static scheduler ID.

If no ID is explicitly given, the scheduler generates an ID by concatenating the
following:

* the current host name
* the current process ID
* the ID of the scheduler instance

.. _troubleshooting:

Troubleshooting
===============

If something isn't working as expected, it will be helpful to increase the logging level
of the ``apscheduler`` logger to the ``DEBUG`` level.

If you do not yet have logging enabled in the first place, you can do this::

    import logging

    logging.basicConfig()
    logging.getLogger('apscheduler').setLevel(logging.DEBUG)

This should provide lots of useful information about what's going on inside the
scheduler and/or worker.

Also make sure that you check the :doc:`faq` section to see if your problem already has
a solution.

Reporting bugs
==============

A `bug tracker <https://github.com/agronholm/apscheduler/issues>`_ is provided by
GitHub.

Getting help
============

If you have problems or other questions, you can either:

* Ask in the `apscheduler <https://gitter.im/apscheduler/Lobby>`_ room on Gitter
* Post a question on `GitHub discussions`_, or
* Post a question on StackOverflow_ and add the ``apscheduler`` tag

.. _GitHub discussions: https://github.com/agronholm/apscheduler/discussions/categories/q-a
.. _StackOverflow: http://stackoverflow.com/questions/tagged/apscheduler


================================================
FILE: docs/versionhistory.rst
================================================
Version history
===============

To find out how to migrate your application from a previous version of
APScheduler, see the :doc:`migration section <migration>`.

**UNRELEASED**

- **BREAKING** Switched the MongoDB data store to use the asynchronous API in
  ``pymongo`` and bumped the minimum ``pymongo`` version to v4.13.0
- Dropped support for Python 3.9
- Fixed an issue where ``CronTrigger`` does not convert ``start_time`` to ``self.timezone``
  (`#1061 <https://github.com/agronholm/apscheduler/issues/1061>`_; PR by @jonasitzmann)
- Fixed an issue where ``CronTrigger.next()`` returned a non-existing date on a DST change
  (`#1059 <https://github.com/agronholm/apscheduler/issues/1059>`_; PR by @jonasitzmann)
- Fixed jobs that were being run when the scheduler was gracefully stopped being left in
  an acquired state (`#946 <https://github.com/agronholm/apscheduler/issues/946>`_)

**4.0.0a6**

- **BREAKING** Refactored ``AsyncpgEventBroker`` to directly accept a connection string,
  thus eliminating the need for the ``AsyncpgEventBroker.from_dsn()`` class method
- **BREAKING** Added the ``extend_acquired_schedule_leases()`` data store method to
  prevent other schedulers from acquiring schedules already being processed by a
  scheduler, if that's taking unexpectedly long for some reason
- **BREAKING** Added the ``extend_acquired_job_leases()`` data store method to prevent
  jobs from being cleaned up as if they had been abandoned
  (`#864 <https://github.com/agronholm/apscheduler/issues/864>`_)
- **BREAKING** Changed the ``cleanup()`` data store method to also be responsible for
  releasing jobs whose leases have expired (so the schedulers responsible for them have
  probably died)
- **BREAKING** Changed most attributes in ``Task`` and ``Schedule`` classes to be
  read-only
- **BREAKING** Refactored the ``release_schedules()`` data store method to take a
  sequence of ``ScheduleResult`` instances instead of a sequence of schedules, to enable
  the memory data store to handle schedule updates more efficiently
- **BREAKING** Replaced the data store ``lock_expiration_delay`` parameter with a new
  scheduler-level parameter, ``lease_duration`` which is then used to call the various
  data store methods
- **BREAKING** Added the ``job_result_expiration_time`` field to the ``Schedule`` class,
  to allow the job results from scheduled jobs to stay around for some time
  (`#927 <https://github.com/agronholm/apscheduler/issues/927>`_)
- **BREAKING** Added an index for the ``created_at`` job field, so acquiring jobs would
  be faster when there are a lot of them
- **BREAKING** Removed the ``job_executor`` and ``max_running_jobs`` parameters from
  ``add_schedule()`` and ``add_run_job()`` (explicitly configure the task using
  ``configure_task()`` or by using the new ``@task`` decorator
- **BREAKING** Replaced the ``default_job_executor`` scheduler parameter with a more
  comprehensive ``task_defaults`` parameter
- Added the ``@task`` decorator for specifying task configuration parameters bound to a
  function
- **BREAKING** Changed tasks to only function as job templates as well as buckets to
  limit maximum concurrent job execution
- **BREAKING** Changed the ``timezone`` argument to ``CronTrigger.from_crontab()`` into
  a keyword-only argument
- **BREAKING** Added the ``metadata`` field to tasks, schedules and jobs
- **BREAKING** Added logic to store ``last_fire_time`` in datastore implementations
  (PR by @hlobit)
- **BREAKING** Added the ``reap_abandoned_jobs()`` abstract method to ``DataStore``
  which the scheduler calls before processing any jobs in order to immediately mark jobs
  left in an acquired state when the scheduler crashed
- Added the ``start_time`` and ``end_time`` arguments to ``CronTrigger.from_crontab()``
  (`#676 <https://github.com/agronholm/apscheduler/issues/676>`_)
- Added the ``psycopg`` event broker
- Added useful indexes and removed useless ones in ``SQLAlchemyDatastore`` and
  ``MongoDBDataStore``
- Changed the ``lock_expiration_delay`` parameter of built-in data stores to accept a
  ``timedelta`` as well as ``int`` or ``float``
- Fixed serialization error with ``CronTrigger`` when pausing a schedule
  (`#864 <https://github.com/agronholm/apscheduler/issues/864>`_)
- Fixed ``TypeError: object NoneType can't be used in 'await' expression`` at teardown
  of ``SQLAlchemyDataStore`` when it was passed a URL that implicitly created a
  synchronous engine
- Fixed serializers raising their own exceptions instead of ``SerializationError`` and
  ``DeserializationError`` as appropriate
- Fixed ``repr()`` outputs of schedulers, data stores and event brokers to be much more
  useful and reasonable
- Fixed race condition in ``MongoDBDataStore`` that allowed multiple schedulers to
  acquire the same schedules at once
- Changed ``SQLAlchemyDataStore`` to automatically create the explicitly specified
  schema if it's missing (PR by @zhu0629)
- Fixed an issue with ``CronTrigger`` infinitely looping to get next date when DST ends
  (`#980 <https://github.com/agronholm/apscheduler/issues/980>`_; PR by @hlobit)
- Skip dispatching extend_acquired_job_leases with no jobs (PR by @JacobHayes)
- Fixed schedulers not immediately processing schedules that the scheduler left in an
  acquired state after a crash
- Fixed the job lease extension task exiting prematurely while the scheduler is starting
  (PR by @JacobHayes)
- Migrated test and documentation dependencies from extras to dependency groups
- Fixed ``add_job()`` overwriting task configuration (PR by @mattewid)

**4.0.0a5**

- **BREAKING** Added the ``cleanup()`` scheduler method and a configuration option
  (``cleanup_interval``). A corresponding abstract method was added to the ``DataStore``
  class. This method purges expired job results and schedules that have exhausted their
  triggers and have no more associated jobs running. Previously, schedules were
  automatically deleted instantly once their triggers could no longer produce any fire
  times.
- **BREAKING** Made publishing ``JobReleased`` events the responsibility of the
  ``DataStore`` implementation, rather than the scheduler, for consistency with the
  ``acquire_jobs()`` method
- **BREAKING** The ``started_at`` field was moved from ``Job`` to ``JobResult``
- **BREAKING** Removed the ``from_url()`` class methods of ``SQLAlchemyDataStore``,
  ``MongoDBDataStore`` and ``RedisEventBroker`` in favor of the ability to pass a
  connection url to the initializer
- Added the ability to pause and unpause schedules (PR by @WillDaSilva)
- Added the ``scheduled_start`` field to the ``JobAcquired`` event
- Added the ``scheduled_start`` and ``started_at`` fields to the ``JobReleased`` event
- Fixed large parts of ``MongoDBDataStore`` still calling blocking functions in the
  event loop thread
- Fixed JSON serialization of triggers that had been used at least once
- Fixed dialect name checks in the SQLAlchemy job store
- Fixed JSON and CBOR serializers unable to serialize enums
- Fixed infinite loop in CalendarIntervalTrigger with UTC timezone (PR by unights)
- Fixed scheduler not resuming job processing when ``max_concurrent_jobs`` had been
  reached and then a job was completed, thus making job processing possible again
  (PR by MohammadAmin Vahedinia)
- Fixed the shutdown procedure of the Redis event broker
- Fixed ``SQLAlchemyDataStore`` not respecting custom schema name when creating enums
- Fixed skipped intervals with overlapping schedules in ``AndTrigger``
  (#911 <https://github.com/agronholm/apscheduler/issues/911>_; PR by Bennett Meares)
- Fixed implicitly created client instances in data stores and event brokers not being
  closed along with the store/broker

**4.0.0a4**

- **BREAKING** Renamed any leftover fields named ``executor`` to ``job_executor``
  (this breaks data store compatibility)
- **BREAKING** Switched to using the timezone aware timestamp column type on Oracle
- **BREAKING** Fixed precision issue with interval columns on MySQL
- **BREAKING** Fixed datetime comparison issues on SQLite and MySQL
- **BREAKING** Worked around datetime microsecond precision issue on MongoDB
- **BREAKING** Renamed the ``worker_id`` field to ``scheduler_id`` in the
  ``JobAcquired`` and ``JobReleased`` events
- **BREAKING** Added the ``task_id`` attribute to the ``ScheduleAdded``,
  ``ScheduleUpdated`` and ``ScheduleRemoved`` events
- **BREAKING** Added the ``finished`` attribute to the ``ScheduleRemoved`` event
- **BREAKING** Added the ``logger`` parameter to ``Datastore.start()`` and
  ``EventBroker.start()`` to make both use the scheduler's assigned logger
- **BREAKING** Made the ``apscheduler.marshalling`` module private
- Added the ``configure_task()`` and ``get_tasks()`` scheduler methods
- Fixed out of order delivery of events delivered using worker threads
- Fixed schedule processing not setting job start deadlines correctly

**4.0.0a3**

- **BREAKING** The scheduler classes were moved to be importable (only) directly from
  the ``apscheduler`` package (``apscheduler.Scheduler`` and
  ``apscheduler.AsyncScheduler``)
- **BREAKING** Removed the "tags" field in schedules and jobs (this will be added back
  when the feature has been fully thought through)
- **BREAKING** Removed the ``JobInfo`` class in favor of just using the ``Job`` class
  (which is now immutable)
- **BREAKING** Workers were merged into schedulers. As the ``Worker`` and
  ``AsyncWorker`` classes have been removed, you now need to pass
  ``role=SchedulerRole.scheduler`` to the scheduler to prevent it from processing due
  jobs. The worker event classes (``WorkerEvent``, ``WorkerStarted``, ``WorkerStopped``)
  have also been removed.
- **BREAKING** The synchronous interfaces for event brokers and data stores have been
  removed. Synchronous libraries can still be used to implement these services through
  the use of ``anyio.to_thread.run_sync()``.
- **BREAKING** The ``current_worker`` context variable has been removed
- **BREAKING** The ``current_scheduler`` context variable is now specified to only
  contain the currently running instance of a **synchronous** scheduler
  (``apscheduler.Scheduler``). The asynchronous scheduler instance can be fetched from
  the new ``current_async_scheduler`` context variable, and will always be available
  when a scheduler is running in the current context, while ``current_scheduler`` is
  only available when the synchronous wrapper is being run.
- **BREAKING** Changed the initialization of data stores and event brokers to use a
  single ``start()`` method that accepts an ``AsyncExitStack`` (and, depending on the
  interface, other arguments too)
- **BREAKING** Added a concept of "job executors". This determines how the task function
  is executed once picked up by a worker. Several data structures and scheduler methods
  have a new field/parameter for this, ``job_executor``. This addition requires database
  schema changes too.
- Dropped support for Python 3.7
- Added support for Python 3.12
- Added the ability to run jobs in worker processes, courtesy of the ``processpool``
  executor
- Added the ability to run jobs in the Qt event loop via the ``qt`` executor
- Added the ``get_jobs()`` scheduler method
- The synchronous scheduler now runs an asyncio event loop in a thread, acting as a
  façade for ``AsyncScheduler``
- Fixed the ``schema`` parameter in ``SQLAlchemyDataStore`` not being applied
- Fixed SQLalchemy 2.0 compatibility

**4.0.0a2**

- **BREAKING** Changed the scheduler API to always require a call to either
  ``run_until_stopped()`` or ``start_in_background()`` to start the scheduler (using it
  as a context manager is no longer enough)
- **BREAKING** Replaced ``from_asyncpg_pool()`` with ``from_dsn()`` in the asyncpg event
  broker
- Added an async Redis event broker
- Added automatic reconnection to the Redis event brokers (sync and async)
- Added automatic reconnection to the asyncpg event broker
- Changed ``from_async_sqla_engine()`` in asyncpg event broker to only copy the
  connection options instead of directly using the engine
- Simplified the MQTT event broker by providing a default ``client`` instance if omitted
- Fixed ``CancelledError`` being reported as a crash on Python 3.7
- Fixed JSON/CBOR serialization of ``JobReleased`` events

**4.0.0a1**

This was a major rewrite/redesign of most parts of the project. See the
:doc:`migration section <migration>` section for details.

.. warning:: The v4.0 series is provided as a **pre-release** and may change in a
   backwards incompatible fashion without any migration pathway, so do NOT use this
   release in production!

- Made persistent data stores shareable between multiple processes and nodes
- Enhanced data stores to be more resilient against temporary connectivity failures
- Refactored executors (now called *workers*) to pull jobs from the data store so they
  can be run independently from schedulers
- Added full async support (:mod:`asyncio` and Trio_) via AnyIO_
- Added type annotations to the code base
- Added the ability to queue jobs directly without scheduling them
- Added alternative serializers (CBOR, JSON)
- Added the ``CalendarInterval`` trigger
- Added the ability to access the current scheduler (under certain circumstances),
  current worker and the currently running job via context-local variables
- Added schedule level support for jitter
- Made triggers stateful
- Added threshold support for ``AndTrigger``
- Migrated from ``pytz`` time zones to standard library ``zoneinfo`` zones
- Allowed a wider range of tzinfo implementations to be used (though ``zoneinfo`` is
  preferred)
- Changed ``IntervalTrigger`` to start immediately instead of first waiting for one
  interval
- Changed ``CronTrigger`` to use Sunday as weekday number 0, as per the crontab standard
- Dropped support for Python 2.X, 3.5 and 3.6
- Dropped support for the Qt, Twisted, Tornado and Gevent schedulers
- Dropped support for the Redis, RethinkDB and Zookeeper job stores

.. _Trio: https://pypi.org/project/trio/
.. _AnyIO: https://github.com/agronholm/anyio

**3.9.1**

* Removed a leftover check for pytz ``localize()`` and ``normalize()`` methods

**3.9.0**

- Added support for PySide6 to the Qt scheduler
- No longer enforce pytz time zones (support for others is experimental in the 3.x series)
- Fixed compatibility with PyMongo 4
- Fixed pytz deprecation warnings
- Fixed RuntimeError when shutting down the scheduler from a scheduled job

**3.8.1**

- Allowed the use of tzlocal v4.0+ in addition to v2.*

**3.8.0**

- Allowed passing through keyword arguments to the underlying stdlib executors in the
  thread/process pool executors (PR by Albert Xu)

**3.7.0**

- Dropped support for Python 3.4
- Added PySide2 support (PR by Abdulla Ibrahim)
- Pinned ``tzlocal`` to a version compatible with pytz
- Ensured that jitter is always non-negative to prevent triggers from firing more often than
  intended
- Changed ``AsyncIOScheduler`` to obtain the event loop in ``start()`` instead of ``__init__()``,
  to prevent situations where the scheduler won't run because it's using a different event loop
  than then one currently running
- Made it possible to create weak references to ``Job`` instances
- Made the schedulers explicitly raise a descriptive ``TypeError`` when serialization is attempted
- Fixed Zookeeper job store using backslashes instead of forward slashes for paths
  on Windows (PR by Laurel-rao)
- Fixed deprecation warnings on the MongoDB job store and increased the minimum PyMongo
  version to 3.0
- Fixed ``BlockingScheduler`` and ``BackgroundScheduler`` shutdown hanging after the user has
  erroneously tried to start it twice
- Fixed memory leak when coroutine jobs raise exceptions (due to reference cycles in tracebacks)
- Fixed inability to schedule wrapped functions with extra arguments when the wrapped function
  cannot accept them but the wrapper can (original PR by Egor Malykh)
- Fixed potential ``where`` clause error in the SQLAlchemy job store when a subclass uses more than
  one search condition
- Fixed a problem where bound methods added as jobs via textual references were called with an
  unwanted extra ``self`` argument (PR by Pengjie Song)
- Fixed ``BrokenPoolError`` in ``ProcessPoolExecutor`` so that it will automatically replace the
  broken pool with a fresh instance

**3.6.3**

- Fixed Python 2.7 accidentally depending on the ``trollius`` package (regression from v3.6.2)

**3.6.2**

- Fixed handling of :func:`~functools.partial` wrapped coroutine functions in ``AsyncIOExecutor``
  and ``TornadoExecutor`` (PR by shipmints)

**3.6.1**

- Fixed OverflowError on Qt scheduler when the wait time is very long
- Fixed methods inherited from base class could not be executed by processpool executor
  (PR by Yang Jian)

**3.6.0**

- Adapted ``RedisJobStore`` to v3.0 of the ``redis`` library
- Adapted ``RethinkDBJobStore`` to v2.4 of the ``rethink`` library
- Fixed ``DeprecationWarnings`` about ``collections.abc`` on Python 3.7 (PR by Roman Levin)

**3.5.3**

- Fixed regression introduced in 3.5.2: Class methods were mistaken for instance methods and thus
  were broken during serialization
- Fixed callable name detection for methods in old style classes

**3.5.2**

- Fixed scheduling of bound methods on persistent job stores (the workaround of scheduling
  ``YourClass.methodname`` along with an explicit ``self`` argument is no longer necessary as this
  is now done automatically for you)
- Added the FAQ section to the docs
- Made ``BaseScheduler.start()`` raise a ``RuntimeError`` if running under uWSGI with threads
  disabled

**3.5.1**

- Fixed ``OverflowError`` on Windows when the wait time is too long
- Fixed ``CronTrigger`` sometimes producing fire times beyond ``end_date`` when jitter is enabled
  (thanks to gilbsgilbs for the tests)
- Fixed ISO 8601 UTC offset information being silently discarded from string formatted datetimes by
  adding support for parsing them

**3.5.0**

- Added the ``engine_options`` option to ``SQLAlchemyJobStore``
- Added the ``jitter`` options to ``IntervalTrigger`` and ``CronTrigger`` (thanks to gilbsgilbs)
- Added combining triggers (``AndTrigger`` and ``OrTrigger``)
- Added better validation for the steps and ranges of different expressions in ``CronTrigger``
- Added support for named months (``jan`` – ``dec``) in ``CronTrigger`` month expressions
- Added support for creating a ``CronTrigger`` from a crontab expression
- Allowed spaces around commas in ``CronTrigger`` fields
- Fixed memory leak due to a cyclic reference when jobs raise exceptions
  (thanks to gilbsgilbs for help on solving this)
- Fixed passing ``wait=True`` to ``AsyncIOScheduler.shutdown()`` (although it doesn't do much)
- Cancel all pending futures when ``AsyncIOExecutor`` is shut down

**3.4.0**

- Dropped support for Python 3.3
- Added the ability to specify the table schema for ``SQLAlchemyJobStore``
  (thanks to Meir Tseitlin)
- Added a workaround for the ``ImportError`` when used with PyInstaller and the likes
  (caused by the missing packaging metadata when APScheduler is packaged with these tools)

**3.3.1**

- Fixed Python 2.7 compatibility in ``TornadoExecutor``

**3.3.0**

- The asyncio and Tornado schedulers can now run jobs targeting coroutine functions
  (requires Python 3.5; only native coroutines (``async def``) are supported)
- The Tornado scheduler now uses TornadoExecutor as its default executor (see above as for why)
- Added ZooKeeper job store (thanks to Jose Ignacio Villar for the patch)
- Fixed job store failure (``get_due_jobs()``) causing the scheduler main loop to exit (it now
  waits a configurable number of seconds before retrying)
- Fixed ``@scheduled_job`` not working when serialization is required (persistent job stores and
  ``ProcessPoolScheduler``)
- Improved import logic in ``ref_to_obj()`` to avoid errors in cases where traversing the path with
  ``getattr()`` would not work (thanks to Jarek Glowacki for the patch)
- Fixed CronTrigger's weekday position expressions failing on Python 3
- Fixed CronTrigger's range expressions sometimes allowing values outside the given range

**3.2.0**

- Added the ability to pause and unpause the scheduler
- Fixed pickling problems with persistent jobs when upgrading from 3.0.x
- Fixed AttributeError when importing apscheduler with setuptools < 11.0
- Fixed some events missing from ``apscheduler.events.__all__`` and
  ``apscheduler.events.EVENTS_ALL``
- Fixed wrong run time being set for date trigger when the timezone isn't the same as the local one
- Fixed builtin ``id()`` erroneously used in MongoDBJobStore's ``JobLookupError()``
- Fixed endless loop with CronTrigger that may occur when the computer's clock resolution is too
   low (thanks to Jinping Bai for the patch)

**3.1.0**

- Added RethinkDB job store (contributed by Allen Sanabria)
- Added method chaining to the ``modify_job()``, ``reschedule_job()``, ``pause_job()`` and
   ``resume_job()`` methods in ``BaseScheduler`` and the corresponding methods in the ``Job`` class
- Added the EVENT_JOB_SUBMITTED event that indicates a job has been submitted to its executor.
- Added the EVENT_JOB_MAX_INSTANCES event that indicates a job's execution was skipped due to its
  maximum number of concurrently running instances being reached

- Added the time zone to the  repr() output of ``CronTrigger`` and ``IntervalTrigger``
- Fixed rare race condition on scheduler ``shutdown()``
- Dropped official support for CPython 2.6 and 3.2 and PyPy3
- Moved the connection logic in database backed job stores to the ``start()`` method
- Migrated to setuptools_scm for versioning
- Deprecated the various version related variables in the ``apscheduler`` module
  (``apscheduler.version_info``, ``apscheduler.version``, ``apscheduler.release``,
  ``apscheduler.__version__``)

**3.0.6**

- Fixed bug in the cron trigger that produced off-by-1-hour datetimes when crossing the daylight
  saving threshold (thanks to Tim Strazny for reporting)

**3.0.5**

- Fixed cron trigger always coalescing missed run times into a single run time
  (contributed by Chao Liu)
- Fixed infinite loop in the cron trigger when an out-of-bounds value was given in an expression
- Fixed debug logging displaying the next wakeup time in the UTC timezone instead of the
  scheduler's configured timezone
- Allowed unicode function references in Python 2

**3.0.4**

- Fixed memory leak in the base executor class (contributed by Stefan Nordhausen)

**3.0.3**

- Fixed compatibility with pymongo 3.0

**3.0.2**

- Fixed ValueError when the target callable has a default keyword argument that wasn't overridden
- Fixed wrong job sort order in some job stores
- Fixed exception when loading all jobs from the redis job store when there are paused jobs in it
- Fixed AttributeError when printing a job list when there were pending jobs
- Added setuptools as an explicit requirement in install requirements

**3.0.1**

- A wider variety of target callables can now be scheduled so that the jobs are still serializable
  (static methods on Python 3.3+, unbound methods on all except Python 3.2)
- Attempting to serialize a non-serializable Job now raises a helpful exception during
  serialization. Thanks to Jeremy Morgan for pointing this out.
- Fixed table creation with SQLAlchemyJobStore on MySQL/InnoDB
- Fixed start date getting set too far in the future with a timezone different from the local one
- Fixed _run_job_error() being called with the incorrect number of arguments in most executors

**3.0.0**

- Added support for timezones (special thanks to Curtis Vogt for help with this one)
- Split the old Scheduler class into BlockingScheduler and BackgroundScheduler and added
  integration for asyncio (PEP 3156), Gevent, Tornado, Twisted and Qt event loops
- Overhauled the job store system for much better scalability
- Added the ability to modify, reschedule, pause and resume jobs
- Dropped the Shelve job store because it could not work with the new job store system
- Dropped the max_runs option and run counting of jobs since it could not be implemented reliably
- Adding jobs is now done exclusively through ``add_job()`` -- the shortcuts to triggers were
  removed
- Added the ``end_date`` parameter to cron and interval triggers
- It is now possible to add a job directly to an executor without scheduling, by omitting the
  trigger argument
- Replaced the thread pool with a pluggable executor system
- Added support for running jobs in subprocesses (via the ``processpool`` executor)
- Switched from nose to py.test for running unit tests

**2.1.0**

- Added Redis job store
- Added a "standalone" mode that runs the scheduler in the calling thread
- Fixed disk synchronization in ShelveJobStore
- Switched to PyPy 1.9 for PyPy compatibility testing
- Dropped Python 2.4 support
- Fixed SQLAlchemy 0.8 compatibility in SQLAlchemyJobStore
- Various documentation improvements

**2.0.3**

- The scheduler now closes the job store that is being removed, and all job stores on shutdown() by
  default
- Added the ``last`` expression in the day field of CronTrigger (thanks rcaselli)
- Raise a TypeError when fields with invalid names are passed to CronTrigger (thanks Christy
  O'Reilly)
- Fixed the persistent.py example by shutting down the scheduler on Ctrl+C
- Added PyPy 1.8 and CPython 3.3 to the test suite
- Dropped PyPy 1.4 - 1.5 and CPython 3.1 from the test suite
- Updated setup.cfg for compatibility with distutils2/packaging
- Examples, documentation sources and unit tests are now packaged in the source distribution

**2.0.2**

- Removed the unique constraint from the "name" column in the SQLAlchemy job store
- Fixed output from Scheduler.print_jobs() which did not previously output a line ending at the end

**2.0.1**

- Fixed cron style jobs getting wrong default values

**2.0.0**

- Added configurable job stores with several persistent back-ends (shelve, SQLAlchemy and MongoDB)
- Added the possibility to listen for job events (execution, error, misfire, finish) on a scheduler
- Added an optional start time for cron-style jobs
- Added optional job execution coalescing for situations where several executions of the job are
  due
- Added an option to limit the maximum number of concurrently executing instances of the job
- Allowed configuration of misfire grace times on a per-job basis
- Allowed jobs to be explicitly named
- All triggers now accept dates in string form (YYYY-mm-dd HH:MM:SS)
- Jobs are now run in a thread pool; you can either supply your own PEP 3148 compliant thread pool
  or let APScheduler create its own
- Maximum run count can be configured for all jobs, not just those using interval-based scheduling
- Fixed a v1.x design flaw that caused jobs to be executed twice when the scheduler thread was
  woken up while still within the allowable range of their previous execution time (issues #5, #7)
- Changed defaults for cron-style jobs to be more intuitive -- it will now default to all
  minimum values for fields lower than the least significant explicitly defined field

**1.3.1**

- Fixed time difference calculation to take into account shifts to and from daylight saving time

**1.3.0**

- Added __repr__() implementations to expressions, fields, triggers, and jobs to help with
  debugging
- Added the dump_jobs method on Scheduler, which gives a helpful listing of all jobs scheduled on
  it
- Fixed positional weekday (3th fri etc.) expressions not working except in some edge cases
  (fixes #2)
- Removed autogenerated API documentation for modules which are not part of the public API, as it
  might confuse some users

.. Note:: Positional weekdays are now used with the **day** field, not
   **weekday**.

**1.2.1**

- Fixed regression: add_cron_job() in Scheduler was creating a CronTrigger with the wrong
  parameters (fixes #1, #3)
- Fixed: if the scheduler is restarted, clear the "stopped" flag to allow jobs to be scheduled
  again

**1.2.0**

- Added the ``week`` option for cron schedules
- Added the ``daemonic`` configuration option
- Fixed a bug in cron expression lists that could cause valid firing times to be missed
- Fixed unscheduling bound methods via unschedule_func()
- Changed CronTrigger constructor argument names to match those in Scheduler

**1.01**

- Fixed a corner case where the combination of hour and day_of_week parameters would cause
  incorrect timing for a cron trigger


================================================
FILE: examples/README.rst
================================================
APScheduler practical examples
==============================

.. highlight:: bash

This directory contains a number of practical examples for running APScheduler in a
variety of configurations.

Prerequisites
-------------

Most examples use one or more external services for data sharing and synchronization.
To start these services, you need Docker_ installed. Each example lists the services
it needs (if any) in the module file, so you can start these services selectively.

On Linux, if you're using the vendor provided system package for Docker instead of
Docker Desktop, you may need to install the compose (v2) plugin (named
``docker-compose-plugin``, or similar) separately.

.. note:: If you're still using the Python-based docker-compose tool (aka compose v1),
          replace ``docker compose`` with ``docker-compose``.

To start all the services, run this command anywhere within the project directory::

    docker compose up -d

To start just a specific service, you can pass its name as an argument::

    docker compose up -d postgresql

To shut down the services and delete all their data::

    docker compose down -v

In addition to having these background services running, you may need to install
specific extra dependencies, like database drivers. Each example module has its required
dependencies listed in the module comment at the top.

.. _Docker: https://docs.docker.com/desktop/#download-and-install

Standalone examples
-------------------

The examples in the ``standalone`` directory demonstrate how to run the scheduler in the
foreground, without anything else going on in the same process.

The directory contains four modules:

- ``async_memory.py``: Basic asynchronous scheduler using the default memory-based data
  store
- ``async_postgres.py``: Basic asynchronous scheduler using the asynchronous SQLAlchemy
  data store with a PostgreSQL back-end
- ``async_mysql.py``: Basic asynchronous scheduler using the asynchronous SQLAlchemy
  data store with a MySQL back-end
- ``sync_mysql.py``: Basic synchronous scheduler using the default memory-based data
  store

Schedulers in web apps
----------------------

The examples in the ``web`` directory demonstrate how to run the scheduler inside a web
application (ASGI_ or WSGI_).

The directory contains five modules:

- ``asgi_noframework.py``: Trivial ASGI_ application, with middleware that starts and
  stops the scheduler as part of the ASGI lifecycle
- ``asgi_fastapi.py``: Trivial FastAPI_ application, with middleware that starts and
  stops the scheduler as part of the ASGI_ lifecycle
- ``asgi_starlette.py``: Trivial Starlette_ application, with middleware that starts and
  stops the scheduler as part of the ASGI_ lifecycle
- ``wsgi_noframework.py``: Trivial WSGI_ application where the scheduler is started in a
  background thread
- ``wsgi_flask.py``: Trivial Flask_ application where the scheduler is started in a
  background thread

.. note:: There is no Django example available yet.

To run any of the ASGI_ examples::

    uvicorn <filename_without_py_extension>:app

To run any of the WSGI_ examples::

    uwsgi -T --http :8000 --wsgi-file <filename>

.. _ASGI: https://asgi.readthedocs.io/en/latest/introduction.html
.. _WSGI: https://wsgi.readthedocs.io/en/latest/what.html
.. _FastAPI: https://fastapi.tiangolo.com/
.. _Starlette: https://www.starlette.io/
.. _Flask: https://flask.palletsprojects.com/

Separate scheduler and worker
-----------------------------

The example in the ``separate_worker`` directory demonstrates the ability to run
schedulers and workers separately. The directory contains three modules:

- ``sync_scheduler.py``: Runs a scheduler (without an internal worker) and adds/updates
  a schedule
- ``sync_worker.py``: Runs a worker only
- ``tasks.py``: Contains the task code (don't try to run this directly; it does nothing)

The reason for the task function being in a separate module is because when you run
either the ``sync_scheduler`` or ``sync_worker`` script, that script is imported as the
``__main__`` module, so if the scheduler schedules ``__main__:tick`` as the task, then
the worker would not be able to find it because its own script would also be named
``__main__``.

To run the example, you need to have both the worker and scheduler scripts running at
the same time. To run the worker::

    python sync_worker.py

To run the scheduler::

    python sync_scheduler.py

You can run multiple schedulers and workers at the same time within this example. If you
run multiple workers, the message might be printed on the console of a different worker
each time the job is run. Running multiple schedulers should have no visible effect, and
as long as at least one scheduler is running, the scheduled task should keep running
periodically on one of the workers.


================================================
FILE: examples/gui/qt_executor.py
================================================
from __future__ import annotations

import sys
from datetime import datetime

from apscheduler import Scheduler
from apscheduler.executors.qt import QtJobExecutor
from apscheduler.triggers.interval import IntervalTrigger

try:
    from PySide6.QtWidgets import QApplication, QLabel, QMainWindow
except ImportError:
    try:
        from PyQt6.QtWidgets import QApplication, QLabel, QMainWindow
    except ImportError:
        raise ImportError("Either PySide6 or PyQt6 is needed to run this") from None


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("APScheduler demo")

        self.clock = QLabel()
        font = self.clock.font()
        font.setPointSize(30)
        self.clock.setFont(font)

        self.update_time()
        self.setCentralWidget(self.clock)

    def update_time(self) -> None:
        now = datetime.now()
        self.clock.setText(f"The time is now {now:%H:%M:%S}")


app = QApplication(sys.argv)
window = MainWindow()
window.show()
with Scheduler() as scheduler:
    scheduler.job_executors["qt"] = QtJobExecutor()
    scheduler.add_schedule(
        window.update_time, IntervalTrigger(seconds=1), job_executor="qt"
    )
    scheduler.start_in_background()
    app.exec()


================================================
FILE: examples/separate_worker/async_scheduler.py
================================================
"""
This is an example demonstrating the use of the scheduler as only an interface to the
scheduling system. This script adds or updates a single schedule and then exits. To see
the schedule acted on, you need to run the corresponding worker script (either
async_worker.py or sync_worker.py).

This script requires the "postgresql" service to be running.
To install prerequisites: pip install sqlalchemy asyncpg
To run: python async_scheduler.py
"""

from __future__ import annotations

import asyncio
import logging

from example_tasks import tick
from sqlalchemy.ext.asyncio import create_async_engine

from apscheduler import AsyncScheduler
from apscheduler.datastores.sqlalchemy import SQLAlchemyDataStore
from apscheduler.eventbrokers.asyncpg import AsyncpgEventBroker
from apscheduler.triggers.interval import IntervalTrigger


async def main():
    engine = create_async_engine(
        "postgresql+asyncpg://postgres:secret@localhost/testdb"
    )
    data_store = SQLAlchemyDataStore(engine)
    event_broker = AsyncpgEventBroker.from_async_sqla_engine(engine)

    # Uncomment the next two lines to use the Redis event broker instead
    # from apscheduler.eventbrokers.redis import RedisEventBroker
    # event_broker = RedisEventBroker.from_url("redis://localhost")

    async with AsyncScheduler(data_store, event_broker) as scheduler:
        await scheduler.add_schedule(tick, IntervalTrigger(seconds=1), id="tick")
        # Note: we don't actually start the scheduler here!


logging.basicConfig(level=logging.INFO)
asyncio.run(main())


================================================
FILE: examples/separate_worker/async_worker.py
================================================
"""
This is an example demonstrating how to run a scheduler to process schedules added by
another scheduler elsewhere. Prior to starting this script, you need to run the script
(either async_scheduler.py or sync_scheduler.py) that adds or updates a schedule to the
data store. This script will then pick up that schedule and start spawning jobs that
will print a line on the console on one-second intervals.

This script requires the "postgresql" service to be running.
To install prerequisites: pip install sqlalchemy asyncpg
To run: python async_worker.py
"""

from __future__ import annotations

import asyncio
import logging

from sqlalchemy.ext.asyncio import create_async_engine

from apscheduler import AsyncScheduler
from apscheduler.datastores.sqlalchemy import SQLAlchemyDataStore
from apscheduler.eventbrokers.asyncpg import AsyncpgEventBroker


async def main():
    async with AsyncScheduler(data_store, event_broker) as scheduler:
        await scheduler.run_until_stopped()


logging.basicConfig(level=logging.INFO)
engine = create_async_engine("postgresql+asyncpg://postgres:secret@localhost/testdb")
data_store = SQLAlchemyDataStore(engine)
event_broker = AsyncpgEventBroker.from_async_sqla_engine(engine)

# Uncomment the next two lines to use the Redis event broker instead
# from apscheduler.eventbrokers.redis import RedisEventBroker
# event_broker = RedisEventBroker.from_url("redis://localhost")

asyncio.run(main())


================================================
FILE: examples/separate_worker/example_tasks.py
================================================
"""
This module contains just the code for the scheduled task.
It should not be run directly.
"""

from __future__ import annotations

from datetime import datetime


def tick():
    print("Hello, the time is", datetime.now())


================================================
FILE: examples/separate_worker/sync_scheduler.py
================================================
"""
This is an example demonstrating the use of the scheduler as only an interface to the
scheduling system. This script adds or updates a single schedule and then exits. To see
the schedule acted on, you need to run the corresponding worker script (either
async_worker.py or sync_worker.py).

This script requires the "postgresql" service to be running.
To install prerequisites: pip install sqlalchemy asyncpg
To run: python sync_scheduler.py
"""

from __future__ import annotations

import logging

from example_tasks import tick
from sqlalchemy.ext.asyncio import create_async_engine

from apscheduler import Scheduler
from apscheduler.datastores.sqlalchemy import SQLAlchemyDataStore
from apscheduler.eventbrokers.asyncpg import AsyncpgEventBroker
from apscheduler.triggers.interval import IntervalTrigger

logging.basicConfig(level=logging.INFO)
engine = create_async_engine("postgresql+asyncpg://postgres:secret@localhost/testdb")
data_store = SQLAlchemyDataStore(engine)
event_broker = AsyncpgEventBroker.from_async_sqla_engine(engine)

# Uncomment the next two lines to use the MQTT event broker instead
# from apscheduler.eventbrokers.mqtt import MQTTEventBroker
# event_broker = MQTTEventBroker()

with Scheduler(data_store, event_broker) as scheduler:
    scheduler.add_schedule(tick, IntervalTrigger(seconds=1), id="tick")
    # Note: we don't actually start the scheduler here!


================================================
FILE: examples/separate_worker/sync_worker.py
================================================
"""
This is an example demonstrating how to run a scheduler to process schedules added by
another scheduler elsewhere. Prior to starting this script, you need to run the script
(either async_scheduler.py or sync_scheduler.py) that adds or updates a schedule to the
data store. This script will then pick up that schedule and start spawning jobs that
will print a line on the console on one-second intervals.

This script requires the "postgresql" service to be running.
To install prerequisites: pip install sqlalchemy asyncpg
To run: python sync_worker.py
"""

from __future__ import annotations

import logging

from sqlalchemy.ext.asyncio import create_async_engine

from apscheduler import Scheduler
from apscheduler.datastores.sqlalchemy import SQLAlchemyDataStore
from apscheduler.eventbrokers.asyncpg import AsyncpgEventBroker

logging.basicConfig(level=logging.INFO)
engine = create_async_engine("postgresql+asyncpg://postgres:secret@localhost/testdb")
data_store = SQLAlchemyDataStore(engine)
event_broker = AsyncpgEventBroker.from_async_sqla_engine(engine)

# Uncomment the next two lines to use the MQTT event broker instead
# from apscheduler.eventbrokers.mqtt import MQTTEventBroker
# event_broker = MQTTEventBroker()

with Scheduler(data_store, event_broker) as scheduler:
    scheduler.run_until_stopped()


================================================
FILE: examples/standalone/async_memory.py
================================================
"""
Example demonstrating use of the asynchronous scheduler in a simple asyncio app.

To run: python async_memory.py

It should print a line on the console on a one-second interval.
"""

from __future__ import annotations

from asyncio import run
from datetime import datetime

from apscheduler import AsyncScheduler
from apscheduler.triggers.interval import IntervalTrigger


def tick():
    print("Hello, the time is", datetime.now())


async def main():
    async with AsyncScheduler() as scheduler:
        await scheduler.add_schedule(tick, IntervalTrigger(seconds=1))
        await scheduler.run_until_stopped()


run(main())


================================================
FILE: examples/standalone/async_mysql.py
================================================
"""
Example demonstrating use of the asynchronous scheduler with persistence via MySQL or
MariaDB in a simple asyncio app.

Requires the "mysql" service to be running.
To install prerequisites: pip install sqlalchemy asyncmy
To run: python async_mysql.py

It should print a line on the console on a one-second interval.
"""

from __future__ import annotations

from asyncio import run
from datetime import datetime

from sqlalchemy.ext.asyncio import create_async_engine

from apscheduler import AsyncScheduler
from apscheduler.datastores.sqlalchemy import SQLAlchemyDataStore
from apscheduler.triggers.interval import IntervalTrigger


def tick():
    print("Hello, the time is", datetime.now())


async def main():
    engine = create_async_engine(
        "mysql+asyncmy://root:secret@localhost/testdb?charset=utf8mb4"
    )
    data_store = SQLAlchemyDataStore(engine)
    async with AsyncScheduler(data_store) as scheduler:
        await scheduler.add_schedule(tick, IntervalTrigger(seconds=1), id="tick")
        await scheduler.run_until_stopped()


run(main())


================================================
FILE: examples/standalone/async_postgres.py
================================================
"""
Example demonstrating use of the asynchronous scheduler with persistence via PostgreSQL
in a simple asyncio app.

Requires the "postgresql" service to be running.
To install prerequisites: pip install sqlalchemy asyncpg
To run: python async_postgres.py

It should print a line on the console on a one-second interval.
"""

from __future__ import annotations

from asyncio import run
from datetime import datetime

from sqlalchemy.ext.asyncio import create_async_engine

from apscheduler import AsyncScheduler
from apscheduler.datastores.sqlalchemy import SQLAlchemyDataStore
from apscheduler.triggers.interval import IntervalTrigger


def tick():
    print("Hello, the time is", datetime.now())


async def main():
    engine = create_async_engine(
        "postgresql+asyncpg://postgres:secret@localhost/testdb"
    )
    data_store = SQLAlchemyDataStore(engine)
    async with AsyncScheduler(data_store) as scheduler:
        await scheduler.add_schedule(tick, IntervalTrigger(seconds=1), id="tick")
        await scheduler.run_until_stopped()


run(main())


================================================
FILE: examples/standalone/sync_memory.py
================================================
"""
Example demonstrating use of the synchronous scheduler.

To run: python sync_memory.py

It should print a line on the console on a one-second interval.
"""

from __future__ import annotations

from datetime import datetime

from apscheduler import Scheduler
from apscheduler.triggers.interval import IntervalTrigger


def tick():
    print("Hello, the time is", datetime.now())


with Scheduler() as scheduler:
    scheduler.add_schedule(tick, IntervalTrigger(seconds=1))
    scheduler.run_until_stopped()


================================================
FILE: examples/web/asgi_fastapi.py
================================================
"""
Example demonstrating use with the FastAPI web framework.

Requires the "postgresql" service to be running.
To install prerequisites: pip install sqlalchemy asycnpg fastapi uvicorn
To run: uvicorn asgi_fastapi:app

It should print a line on the console on a one-second interval while running a
basic web app at http://localhost:8000.
"""

from __future__ import annotations

from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from datetime import datetime

from fastapi import FastAPI
from fastapi.responses import PlainTextResponse, Response
from sqlalchemy.ext.asyncio import create_async_engine

from apscheduler import AsyncScheduler
from apscheduler.datastores.sqlalchemy import SQLAlchemyDataStore
from apscheduler.eventbrokers.asyncpg import AsyncpgEventBroker
from apscheduler.triggers.interval import IntervalTrigger


def tick():
    print("Hello, the time is", datetime.now())


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
    engine = create_async_engine(
        "postgresql+asyncpg://postgres:secret@localhost/testdb"
    )
    data_store = SQLAlchemyDataStore(engine)
    event_broker = AsyncpgEventBroker.from_async_sqla_engine(engine)
    scheduler = AsyncScheduler(data_store, event_broker)

    async with scheduler:
        await scheduler.add_schedule(tick, IntervalTrigger(seconds=1), id="tick")
        await scheduler.start_in_background()
        yield


async def root() -> Response:
    return PlainTextResponse("Hello, world!")


app = FastAPI(lifespan=lifespan)
app.add_api_route("/", root)


================================================
FILE: examples/web/asgi_noframework.py
================================================
"""
Example demonstrating use with ASGI (raw ASGI application, no framework).

Requires the "postgresql" service to be running.
To install prerequisites: pip install sqlalchemy asyncpg uvicorn
To run: uvicorn asgi_noframework:app

It should print a line on the console on a one-second interval while running a
basic web app at http://localhost:8000.
"""

from __future__ import annotations

from datetime import datetime

from sqlalchemy.ext.asyncio import create_async_engine

from apscheduler import AsyncScheduler
from apscheduler.datastores.sqlalchemy import SQLAlchemyDataStore
from apscheduler.eventbrokers.asyncpg import AsyncpgEventBroker
from apscheduler.triggers.interval import IntervalTrigger


def tick():
    print("Hello, the time is", datetime.now())


async def original_app(scope, receive, send):
    """Trivial example of an ASGI application."""
    if scope["type"] == "http":
        await receive()
        await send(
            {
                "type": "http.response.start",
                "status": 200,
                "headers": [
                    [b"content-type", b"text/plain"],
                ],
            }
        )
        await send(
            {
                "type": "http.response.body",
                "body": b"Hello, world!",
                "more_body": False,
            }
        )
    elif scope["type"] == "lifespan":
        while True:
            message = await receive()
            if message["type"] == "lifespan.startup":
                await send({"type": "lifespan.startup.complete"})
            elif message["type"] == "lifespan.shutdown":
                await send({"type": "lifespan.shutdown.complete"})
                return


async def scheduler_middleware(scope, receive, send):
    if scope["type"] == "lifespan":
        engine = create_async_engine(
            "postgresql+asyncpg://postgres:secret@localhost/testdb"
        )
        data_store = SQLAlchemyDataStore(engine)
        event_broker = AsyncpgEventBroker.from_async_sqla_engine(engine)
        async with AsyncScheduler(data_store, event_broker) as scheduler:
            await scheduler.add_schedule(tick, IntervalTrigger(seconds=1), id="tick")
            await scheduler.start_in_background()
            await original_app(scope, receive, send)
    else:
        await original_app(scope, receive, send)


# This is just for consistency with the other ASGI examples
app = scheduler_middleware


================================================
FILE: examples/web/asgi_starlette.py
================================================
"""
Example demonstrating use with the Starlette web framework.

Requires the "postgresql" service to be running.
To install prerequisites: pip install sqlalchemy asycnpg starlette uvicorn
To run: uvicorn asgi_starlette:app

It should print a line on the console on a one-second interval while running a
basic web app at http://localhost:8000.
"""

from __future__ import annotations

from datetime import datetime

from sqlalchemy.ext.asyncio import create_async_engine
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.requests import Request
from starlette.responses import PlainTextResponse, Response
from starlette.routing import Route
from starlette.types import ASGIApp, Receive, Scope, Send

from apscheduler import AsyncScheduler
from apscheduler.datastores.sqlalchemy import SQLAlchemyDataStore
from apscheduler.eventbrokers.asyncpg import AsyncpgEventBroker
from apscheduler.triggers.interval import IntervalTrigger


def tick():
    print("Hello, the time is", datetime.now())


class SchedulerMiddleware:
    def __init__(
        self,
        app: ASGIApp,
        scheduler: AsyncScheduler,
    ) -> None:
        self.app = app
        self.scheduler = scheduler

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        if scope["type"] == "lifespan":
            async with self.scheduler:
                await self.scheduler.add_schedule(
                    tick, IntervalTrigger(seconds=1), id="tick"
                )
                await self.scheduler.start_in_background()
                await self.app(scope, receive, send)
        else:
            await self.app(scope, receive, send)


async def root(request: Request) -> Response:
    return PlainTextResponse("Hello, world!")


engine = create_async_engine("postgresql+asyncpg://postgres:secret@localhost/testdb")
data_store = SQLAlchemyDataStore(engine)
event_broker = AsyncpgEventBroker.from_async_sqla_engine(engine)
scheduler = AsyncScheduler(data_store, event_broker)
routes = [Route("/", root)]
middleware = [Middleware(SchedulerMiddleware, scheduler=scheduler)]
app = Starlette(routes=routes, middleware=middleware)


================================================
FILE: examples/web/wsgi_flask.py
================================================
"""
Example demonstrating use with WSGI (raw WSGI application, no framework).

Requires the "postgresql" and "redis" services to be running.
To install prerequisites: pip install sqlalchemy psycopg flask uwsgi
To run: uwsgi -T --http :8000 --wsgi-file wsgi_flask.py

It should print a line on the console on a one-second interval while running a
basic web app at http://localhost:8000.
"""

from __future__ import annotations

from datetime import datetime

from flask import Flask
from sqlalchemy.future import create_engine

from apscheduler import Scheduler
from apscheduler.datastores.sqlalchemy import SQLAlchemyDataStore
from apscheduler.eventbrokers.redis import RedisEventBroker
from apscheduler.triggers.interval import IntervalTrigger

app = Flask(__name__)


def tick():
    print("Hello, the time is", datetime.now())


@app.route("/")
def hello_world():
    return "<p>Hello, World!</p>"


engine = create_engine("postgresql+psycopg://postgres:secret@localhost/testdb")
data_store = SQLAlchemyDataStore(engine)
event_broker = RedisEventBroker("redis://localhost")
scheduler = Scheduler(data_store, event_broker)
scheduler.add_schedule(tick, IntervalTrigger(seconds=1), id="tick")
scheduler.start_in_background()


================================================
FILE: examples/web/wsgi_noframework.py
================================================
"""
Example demonstrating use with WSGI (raw WSGI application, no framework).

Requires the "postgresql" and "redis" services to be running.
To install prerequisites: pip install sqlalchemy psycopg uwsgi
To run: uwsgi -T --http :8000 --wsgi-file wsgi_noframework.py

It should print a line on the console on a one-second interval while running a
basic web app at http://localhost:8000.
"""

from __future__ import annotations

from datetime import datetime

from sqlalchemy.future import create_engine

from apscheduler import Scheduler
from apscheduler.datastores.sqlalchemy import SQLAlchemyDataStore
from apscheduler.eventbrokers.redis import RedisEventBroker
from apscheduler.triggers.interval import IntervalTrigger


def tick():
    print("Hello, the time is", datetime.now())


def application(environ, start_response):
    response_body = b"Hello, World!"
    response_headers = [
        ("Content-Type", "text/plain"),
        ("Content-Length", str(len(response_body))),
    ]
    start_response("200 OK", response_headers)
    return [response_body]


engine = create_engine("postgresql+psycopg://postgres:secret@localhost/testdb")
data_store = SQLAlchemyDataStore(engine)
event_broker = RedisEventBroker("redis://localhost")
scheduler = Scheduler(data_store, event_broker)
scheduler.add_schedule(tick, IntervalTrigger(seconds=1), id="tick")
scheduler.start_in_background()


================================================
FILE: pyproject.toml
================================================
[build-system]
requires = [
    "setuptools >= 77",
    "setuptools_scm >= 6.4"
]
build-backend = "setuptools.build_meta"

[project]
name = "APScheduler"
description = "In-process task scheduler with Cron-like capabilities"
readme = "README.rst"
authors = [{name = "Alex Grönholm", email = "alex.gronholm@nextday.fi"}]
classifiers = [
    "Development Status :: 5 - Production/Stable",
    "Intended Audience :: Developers",
    "Framework :: AnyIO",
    "Typing :: Typed",
    "Programming Language :: Python",
    "Programming Language :: Python :: 3 :: Only",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "Programming Language :: Python :: 3.13",
    "Programming Language :: Python :: 3.14",
]
keywords = ["scheduling", "cron"]
license = "MIT"
requires-python = ">= 3.10"
dependencies = [
    "anyio ~= 4.0",
    "attrs >= 22.1",
    "tenacity >= 8.0, < 10.0",
    "tzlocal >= 3.0",
    "typing_extensions >= 4.0; python_version < '3.11'"
]
dynamic = ["version"]

[project.urls]
Documentation = "https://apscheduler.readthedocs.io/en/master/"
Changelog = "https://apscheduler.readthedocs.io/en/master/versionhistory.html"
"Source code" = "https://github.com/agronholm/apscheduler"
"Issue tracker" = "https://github.com/agronholm/apscheduler/issues"

[project.optional-dependencies]
asyncpg = ["asyncpg >= 0.20"]
cbor = ["cbor2 >= 5.0"]
mongodb = ["pymongo >= 4.13.0"]
mqtt = ["paho-mqtt >= 2.0"]
redis = ["redis >= 5.0.1"]
sqlalchemy = ["sqlalchemy[asyncio] >= 2.0.24"]

[dependency-groups]
test = [
    "APScheduler[cbor,mongodb,mqtt,redis,sqlalchemy]",
    "asyncpg >= 0.20; python_implementation == 'CPython' and python_version < '3.13'",
    "aiosqlite >= 0.19",
    "anyio[trio]",
    "asyncmy >= 0.2.5; python_implementation == 'CPython'",
    "coverage >= 7",
    "psycopg[binary]",
    "pymongo >= 4",
    "pymysql[rsa]",
    "PySide6 >= 6.6; python_implementation == 'CPython' and python_version < '3.13'",
    "pytest >= 7.4",
    "pytest-lazy-fixtures",
    "pytest-mock",
    "time-machine >= 2.13.0; python_implementation == 'CPython'",
    """\
    uwsgi >= 2.0.31; python_implementation == 'CPython' and platform_system == 'Linux'\
    and python_version < '3.13'\
    """,
]
doc = [
    "sphinx",
    "sphinx-autodoc-typehints >= 2.2.3",
    "sphinx-rtd-theme >= 1.3.0",
    "sphinx-tabs >= 3.3.1",
]

[tool.setuptools_scm]
version_scheme = "post-release"
local_scheme = "dirty-tag"

[tool.pytest.ini_options]
addopts = "-rsx --tb=short"
testpaths = "tests"
filterwarnings = "always"
markers = [
    "external_service: marks tests as requiring some external service",
]

[tool.coverage.run]
source = ["apscheduler"]

[tool.coverage.report]
show_missing = true

[tool.ruff.lint]
extend-select = [
    "ASYNC",        # flake8-async
    "B",            # flake8-bugbear
    "C4",           # flake8-comprehensions
    "G",            # flake8-logging-format
    "I",            # isort
    "ISC",          # flake8-implicit-str-concat
    "PERF",         # flake8-performance
    "PGH",          # pygrep-hooks
    "RUF100",       # unused noqa (yesqa)
    "T201",         # print
    "UP",           # pyupgrade
    "W",            # pycodestyle warnings
]
ignore = [
    "PERF203",
    "RUF001",
    "RUF002",
]

[tool.ruff.lint.isort]
known-first-party = ["apscheduler"]
required-imports = ["from __future__ import annotations"]

[tool.ruff.lint.per-file-ignores]
"examples/**/*.py" = ["T201"]

[tool.mypy]
python_version = "3.14"
ignore_missing_imports = true
disable_error_code = "type-abstract"

[tool.tox]
env_list = ["py310", "py311", "py312", "py313", "py314", "pypy3"]
skip_missing_interpreters = true
requires = ["tox >= 4.22"]

[tool.tox.env_run_base]
commands = [["pytest", { replace = "posargs", extend = true }]]
package = "editable"
dependency_groups = ["test"]

[tool.tox.env.pyright]
commands = [["pyright", "--verifytypes", "apscheduler"]]
deps = ["pyright"]

[tool.tox.env.docs]
commands = [["sphinx-build", "-W", "-n", "docs", "build/sphinx"]]
dependency_groups = ["doc"]


================================================
FILE: src/apscheduler/__init__.py
================================================
from __future__ import annotations

from typing import Any

from ._context import current_async_scheduler as current_async_scheduler
from ._context import current_job as current_job
from ._context import current_scheduler as current_scheduler
from ._decorators import task as task
from ._enums import CoalescePolicy as CoalescePolicy
from ._enums import ConflictPolicy as ConflictPolicy
from ._enums import JobOutcome as JobOutcome
from ._enums import RunState as RunState
from ._enums import SchedulerRole as SchedulerRole
from ._events import DataStoreEvent as DataStoreEvent
from ._events import Event as Event
from ._events import JobAcquired as JobAcquired
from ._events import JobAdded as JobAdded
from ._events import JobDeserializationFailed as JobDeserializationFailed
from ._events import JobReleased as JobReleased
from ._events import JobRemoved as JobRemoved
from ._events import ScheduleAdded as ScheduleAdded
from ._events import ScheduleDeserializationFailed as ScheduleDeserializationFailed
from ._events import ScheduleRemoved as ScheduleRemoved
from ._events import SchedulerEvent as SchedulerEvent
from ._events import SchedulerStarted as SchedulerStarted
from ._events import SchedulerStopped as SchedulerStopped
from ._events import ScheduleUpdated as ScheduleUpdated
from ._events import TaskAdded as TaskAdded
from ._events import TaskRemoved as TaskRemoved
from ._events import TaskUpdated as TaskUpdated
from ._exceptions import CallableLookupError as CallableLookupError
from ._exceptions import ConflictingIdError as ConflictingIdError
from ._exceptions import DeserializationError as DeserializationError
from ._exceptions import JobCancelled as JobCancelled
from ._exceptions import JobDeadlineMissed as JobDeadlineMissed
from ._exceptions import JobLookupError as JobLookupError
from ._exceptions import JobResultNotReady as JobResultNotReady
from ._exceptions import MaxIterationsReached as MaxIterationsReached
from ._exceptions import ScheduleLookupError as ScheduleLookupError
from ._exceptions import SerializationError as SerializationError
from ._exceptions import TaskLookupError as TaskLookupError
from ._retry import RetryMixin as RetryMixin
from ._retry import RetrySettings as RetrySettings
from ._schedulers.async_ import AsyncScheduler as AsyncScheduler
from ._schedulers.sync import Scheduler as Scheduler
from ._structures import Job as Job
from ._structures import JobResult as JobResult
from ._structures import Schedule as Schedule
from ._structures import ScheduleResult as ScheduleResult
from ._structures import Task as Task
from ._structures import TaskDefaults as TaskDefaults
from ._utils import UnsetValue as UnsetValue

# Re-export imports, so they look like they live directly in this package
value: Any
for value in list(locals().values()):
    if getattr(value, "__module__", "").startswith("apscheduler."):
        value.__module__ = __name__


================================================
FILE: src/apscheduler/_context.py
================================================
from __future__ import annotations

from contextvars import ContextVar
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from ._schedulers.async_ import AsyncScheduler
    from ._schedulers.sync import Scheduler
    from ._structures import Job

#: The currently running (local) scheduler
current_scheduler: ContextVar[Scheduler | None] = ContextVar(
    "current_scheduler", default=None
)
current_async_scheduler: ContextVar[AsyncScheduler | None] = ContextVar(
    "current_async_scheduler", default=None
)
#: Metadata about the current job
current_job: ContextVar[Job] = ContextVar("job_info")


================================================
FILE: src/apscheduler/_converters.py
================================================
from __future__ import annotations

from collections.abc import Callable
from datetime import date, datetime, timedelta, timezone, tzinfo
from typing import Any
from uuid import UUID
from zoneinfo import ZoneInfo

from tzlocal import get_localzone


def as_int(value: int | str) -> int:
    if isinstance(value, str):
        return int(value)

    return value


def as_datetime(value: datetime | str) -> datetime:
    if isinstance(value, str):
        # Before Python 3.11, fromisoformat() could not handle the "Z" suffix
        if value.upper().endswith("Z"):
            value = value[:-1] + "+00:00"

        value = datetime.fromisoformat(value)

    return value


def as_aware_datetime(value: datetime | str) -> datetime:
    value_as_datetime = as_datetime(value)
    if isinstance(value_as_datetime, datetime) and value_as_datetime.tzinfo is None:
        value_as_datetime = value_as_datetime.astimezone(get_localzone())

    return value_as_datetime


def as_date(value: date | str) -> date:
    if isinstance(value, str):
        return date.fromisoformat(value)

    return value


def as_timezone(value: tzinfo | str) -> tzinfo:
    if isinstance(value, str):
        return get_localzone() if value == "local" else ZoneInfo(value)
    elif value is timezone.utc:
        return ZoneInfo("UTC")

    return value


def as_uuid(value: UUID | str) -> UUID:
    if isinstance(value, str):
        return UUID(value)

    return value


def as_timedelta(value: timedelta | int) -> timedelta:
    if isinstance(value, (float, int)):
        return timedelta(seconds=value)

    return value


def as_enum(enum_class: Any) -> Callable[[Any], Any]:
    def converter(value: Any) -> Any:
        if isinstance(value, str):
            return enum_class[value]

        return value

    return converter


def list_converter(converter: Callable[[Any], Any]) -> Callable[[Any], Any]:
    def convert(value: Any) -> Any:
        if isinstance(value, list):
            return [converter(item) for item in value]

        return value

    return convert


================================================
FILE: src/apscheduler/_decorators.py
================================================
from __future__ import annotations

from collections.abc import Callable
from datetime import timedelta
from typing import Any, TypeVar

import attrs
from attr.validators import instance_of, optional

from ._converters import as_timedelta
from ._structures import MetadataType, TaskDefaults
from ._utils import UnsetValue, unset
from ._validators import if_not_unset, valid_metadata

T = TypeVar("T", bound="Callable[..., Any]")

TASK_PARAMETERS_KEY = "_apscheduler_taskdef"


@attrs.define(kw_only=True)
class TaskParameters(TaskDefaults):
    id: str | UnsetValue = attrs.field(default=unset)
    job_executor: str | UnsetValue = attrs.field(
        validator=if_not_unset(instance_of(str)), default=unset
    )
    max_running_jobs: int | None | UnsetValue = attrs.field(
        validator=if_not_unset(optional(instance_of(int))), default=unset
    )
    misfire_grace_time: timedelta | None | UnsetValue = attrs.field(
        converter=as_timedelta,
        validator=if_not_unset(optional(instance_of(timedelta))),
        default=unset,
    )
    metadata: MetadataType | UnsetValue = attrs.field(
        validator=if_not_unset(valid_metadata), default=unset
    )


def task(
    id: str | UnsetValue = unset,
    *,
    job_executor: str | UnsetValue = unset,
    max_running_jobs: int | None | UnsetValue = unset,
    misfire_grace_time: int | timedelta | None | UnsetValue = unset,
    metadata: MetadataType | UnsetValue = unset,
) -> Callable[[T], T]:
    """
    Decorate a function to have implied defaults as an APScheduler task.

    :param id: the task ID to use
    :param str job_executor: name of the job executor that will run the task
    :param int | None max_running_jobs: maximum number of instances of the task that are
        allowed to run concurrently
    :param ~datetime.timedelta | None misfire_grace_time: maximum number of seconds the
        run time of jobs created for the task are allowed to be late, compared to the
        scheduled run time
    :param metadata: key-value pairs for storing JSON compatible custom information
    """

    def wrapper(func: T) -> T:
        if not isinstance(func, Callable):
            raise ValueError("only functions can be decorated with @task")

        if hasattr(func, TASK_PARAMETERS_KEY):
            raise ValueError(
                "this function already has APScheduler task parameters set"
            )

        setattr(
            func,
            TASK_PARAMETERS_KEY,
            TaskParameters(
                id=id,
                job_executor=job_executor,
                max_running_jobs=max_running_jobs,
                misfire_grace_time=misfire_grace_time,
                metadata=metadata,
            ),
        )
        return func

    return wrapper


def get_task_params(func: Callable[..., Any]) -> TaskParameters:
    return getattr(func, TASK_PARAMETERS_KEY, None) or TaskParameters()


================================================
FILE: src/apscheduler/_enums.py
================================================
from __future__ import annotations

from enum import Enum, auto


class SchedulerRole(Enum):
    """
    Specifies what the scheduler should be doing when it's running.

    .. attribute:: scheduler

        processes due schedules, but won't run jobs

    .. attribute:: worker

        runs due jobs, but won't process schedules

    .. attribute:: both

        processes schedules and runs due jobs
    """

    scheduler = auto()
    worker = auto()
    both = auto()


class RunState(Enum):
    """
    Used to track the running state of schedulers.

    .. attribute:: starting

        not running yet, but in the process of starting

    .. attribute:: started

        running

    .. attribute:: stopping

        still running but in the process of shutting down

    .. attribute:: stopped

        not running
    """

    starting = auto()
    started = auto()
    stopping = auto()
    stopped = auto()


class JobOutcome(Enum):
    """
    Used to indicate how the execution of a job ended.

    .. attribute:: success

        the job completed successfully

    .. attribute:: error

        the job raised an exception

    .. attribute:: missed_start_deadline

        the job's execution was delayed enough for it to miss its start deadline
        (scheduled time + misfire grace time)

    .. attribute:: deserialization_failed

        the deserialization operation failed

    .. attribute:: cancelled

        the job's execution was cancelled

    .. attribute:: abandoned

        the worker running the job stopped unexpectedly and the job was never marked
        as done
    """

    success = auto()
    error = auto()
    missed_start_deadline = auto()
    deserialization_failed = auto()
    cancelled = auto()
    abandoned = auto()


class ConflictPolicy(Enum):
    """
    Used to indicate what to do when trying to add a schedule whose ID conflicts with an
    existing schedule.

    .. attribute:: replace

        replace the existing schedule with a new one

    .. attribute:: do_nothing

        keep the existing schedule as-is and drop the new schedule

    .. attribute:: exception

        raise an exception if a conflict is detected
    """

    replace = auto()
    do_nothing = auto()
    exception = auto()


class CoalescePolicy(Enum):
    """
    Used to indicate how to queue jobs for a schedule that has accumulated multiple
    run times since the last scheduler iteration.

    .. attribute:: earliest

        run once, with the earliest fire time

    .. attribute:: latest

        run once, with the latest fire time

    .. attribute:: all

        submit one job for every accumulated fire time
    """

    earliest = auto()
    latest = auto()
    all = auto()


================================================
FILE: src/apscheduler/_events.py
================================================
from __future__ import annotations

from datetime import datetime, timezone
from functools import partial
from traceback import format_tb
from typing import Any, TypeVar
from uuid import UUID

import attrs
from attrs.converters import optional

from ._converters import as_aware_datetime, as_enum, as_uuid
from ._enums import JobOutcome
from ._structures import Job, JobResult
from ._utils import qualified_name

T_Event = TypeVar("T_Event", bound="Event")


@attrs.define(kw_only=True, frozen=True)
class Event:
    """
    Base class for all events.

    :ivar timestamp: the time when the event occurred
    """

    timestamp: datetime = attrs.field(
        factory=partial(datetime.now, timezone.utc), converter=as_aware_datetime
    )

    def marshal(self) -> dict[str, Any]:
        return attrs.asdict(self)

    @classmethod
    def unmarshal(cls, marshalled: dict[str, Any]) -> Event:
        return cls(**marshalled)


#
# Data store events
#


@attrs.define(kw_only=True, frozen=True)
class DataStoreEvent(Event):
    """Base class for events originating from a data store."""


@attrs.define(kw_only=True, frozen=True)
class TaskAdded(DataStoreEvent):
    """
    Signals that a new task was added to the store.

    :ivar task_id: ID of the task that was added
    """

    task_id: str


@attrs.define(kw_only=True, frozen=True)
class TaskUpdated(DataStoreEvent):
    """
    Signals that a task was updated in a data store.

    :ivar task_id: ID of the task that was updated
    """

    task_id: str


@attrs.define(kw_only=True, frozen=True)
class TaskRemoved(DataStoreEvent):
    """
    Signals that a task was removed from the store.

    :ivar task_id: ID of the task that was removed
    """

    task_id: str


@attrs.define(kw_only=True, frozen=True)
class ScheduleAdded(DataStoreEvent):
    """
    Signals that a new schedule was added to the store.

    :ivar schedule_id: ID of the schedule that was added
    :ivar task_id: ID of the task the schedule belongs to
    :ivar next_fire_time: the first run time calculated for the schedule
    """

    schedule_id: str
    task_id: str
    next_fire_time: datetime | None = attrs.field(converter=optional(as_aware_datetime))


@attrs.define(kw_only=True, frozen=True)
class ScheduleUpdated(DataStoreEvent):
    """
    Signals that a schedule has been updated in the store.

    :ivar schedule_id: ID of the schedule that was updated
    :ivar task_id: ID of the task the schedule belongs to
    :ivar next_fire_time: the next time the schedule will run
    """

    schedule_id: str
    task_id: str
    next_fire_time: datetime | None = attrs.field(converter=optional(as_aware_datetime))


@attrs.define(kw_only=True, frozen=True)
class ScheduleRemoved(DataStoreEvent):
    """
    Signals that a schedule was removed from the store.

    :ivar schedule_id: ID of the schedule that was removed
    :ivar task_id: ID of the task the schedule belongs to
    :ivar finished: ``True`` if the schedule was removed automatically because its
        trigger had no more fire times left
    """

    schedule_id: str
    task_id: str
    finished: bool


@attrs.define(kw_only=True, frozen=True)
class JobAdded(DataStoreEvent):
    """
    Signals that a new job was added to the store.

    :ivar job_id: ID of the job that was added
    :ivar task_id: ID of the task the job would run
    :ivar schedule_id: ID of the schedule the job was created from
    """

    job_id: UUID = attrs.field(converter=as_uuid)
    task_id: str
    schedule_id: str | None


@attrs.define(kw_only=True, frozen=True)
class JobRemoved(DataStoreEvent):
    """
    Signals that a job was removed from the store.

    :ivar job_id: ID of the job that was removed
    :ivar task_id: ID of the task the job would have run

    """

    job_id: UUID = attrs.field(converter=as_uuid)
    task_id: str


@attrs.define(kw_only=True, frozen=True)
class ScheduleDeserializationFailed(DataStoreEvent):
    """
    Signals that the deserialization of a schedule has failed.

    :ivar schedule_id: ID of the schedule that failed to deserialize
    :ivar exception: the exception that was raised during deserialization
    """

    schedule_id: str
    exception: BaseException


@attrs.define(kw_only=True, frozen=True)
class JobDeserializationFailed(DataStoreEvent):
    """
    Signals that the deserialization of a job has failed.

    :ivar job_id: ID of the job that failed to deserialize
    :ivar exception: the exception that was raised during deserialization
    """

    job_id: UUID = attrs.field(converter=as_uuid)
    exception: BaseException


#
# Scheduler events
#


@attrs.define(kw_only=True, frozen=True)
class SchedulerEvent(Event):
    """Base class for events originating from a scheduler."""


@attrs.define(kw_only=True, frozen=True)
class SchedulerStarted(SchedulerEvent):
    pass


@attrs.define(kw_only=True, frozen=True)
class SchedulerStopped(SchedulerEvent):
    """
    Signals that a scheduler has stopped.

    :ivar exception: the exception that caused the scheduler to stop, if any
    """

    exception: BaseException | None = None


@attrs.define(kw_only=True, frozen=True)
class JobAcquired(SchedulerEvent):
    """
    Signals that a scheduler has acquired a job for processing.

    :param job_id: the ID of the job that was acquired
    :param scheduler_id: the ID of the scheduler that acquired the job
    :param task_id: ID of the task the job belongs to
    :param schedule_id: ID of the schedule that
    :param scheduled_start: the time the job was scheduled to start via a schedule (if
        any)
    """

    job_id: UUID = attrs.field(converter=as_uuid)
    scheduler_id: str
    task_id: str
    schedule_id: str | None = None
    scheduled_start: datetime | None = attrs.field(converter=as_aware_datetime)

    @classmethod
    def from_job(cls, job: Job, scheduler_id: str) -> JobAcquired:
        """
        Create a new job-acquired event from a job and a scheduler ID.

        :param job: the job that was acquired
        :param scheduler_id: the ID of the scheduler that acquired the job
        :return: a new job-acquired event

        """
        return cls(
            job_id=job.id,
            scheduler_id=scheduler_id,
            task_id=job.task_id,
            schedule_id=job.schedule_id,
            scheduled_start=job.scheduled_fire_time,
        )


@attrs.define(kw_only=True, frozen=True)
class JobReleased(SchedulerEvent):
    """
    Signals that a scheduler has finished processing of a job.

    :param uuid.UUID job_id: the ID of the job that was released
    :param scheduler_id: the ID of the scheduler that released the job
    :param task_id: ID of the task run by the job
    :param schedule_id: ID of the schedule (if any) that created the job
    :param scheduled_start: the time the job was scheduled to start via the schedule (if
        any)
    :param started_at: the time the executor actually started running the job (``None``
        if the job was skipped due to missing its start deadline)
    :param outcome: the outcome of the job
    :param exception_type: the fully qualified name of the exception if ``outcome`` is
        :attr:`JobOutcome.error`
    :param exception_message: the result of ``str(exception)`` if ``outcome`` is
        :attr:`JobOutcome.error`
    :param exception_traceback: the traceback lines from the exception if ``outcome`` is
        :attr:`JobOutcome.error`
    """

    job_id: UUID = attrs.field(converter=as_uuid)
    scheduler_id: str
    task_id: str
    schedule_id: str | None = None
    scheduled_start: datetime | None = attrs.field(converter=as_aware_datetime)
    started_at: datetime | None = attrs.field(converter=as_aware_datetime)
    outcome: JobOutcome = attrs.field(converter=as_enum(JobOutcome))
    exception_type: str | None = None
    exception_message: str | None = None
    exception_traceback: list[str] | None = None

    @classmethod
    def from_result(
        cls,
        result: JobResult,
        scheduler_id: str,
        task_id: str,
        schedule_id: str | None,
        scheduled_fire_time: datetime | None = None,
    ) -> JobReleased:
        """
        Create a new job-released event from a job, the job result and a scheduler ID.

        :param result: the result of the job
        :param scheduler_id: the ID of the scheduler that acquired the job
        :param task_id: the job's task ID
        :param schedule_id: ID of the schedule (if any) from which the job was spawned
        :param scheduled_fire_time: the time the job was scheduled to start (if the job
            was spawned from a schedule)
        :return: a new job-released event

        """
        if result.exception is not None:
            exception_type: str | None = qualified_name(result.exception.__class__)
            exception_message: str | None = str(result.exception)
            exception_traceback: list[str] | None = format_tb(
                result.exception.__traceback__
            )
        else:
            exception_type = exception_message = exception_traceback = None

        return cls(
            timestamp=result.finished_at,
            job_id=result.job_id,
            scheduler_id=scheduler_id,
            task_id=task_id,
            schedule_id=schedule_id,
            outcome=result.outcome,
            scheduled_start=scheduled_fire_time,
            started_at=result.started_at,
            exception_type=exception_type,
            exception_message=exception_message,
            exception_traceback=exception_traceback,
        )

    def marshal(self) -> dict[str, Any]:
        marshalled = super().marshal()
        return marshalled


================================================
FILE: src/apscheduler/_exceptions.py
================================================
from __future__ import annotations

from uuid import UUID


class TaskLookupError(LookupError):
    """Raised by a data store when it cannot find the requested task."""

    def __init__(self, task_id: str):
        super().__init__(f"No task by the id of {task_id!r} was found")


class ScheduleLookupError(LookupError):
    """Raised by a scheduler when it cannot find the requested schedule."""

    def __init__(self, schedule_id: str):
        super().__init__(f"No schedule by the id of {schedule_id!r} was found")


class JobLookupError(LookupError):
    """Raised when the job store cannot find a job for update or removal."""

    def __init__(self, job_id: UUID):
        super().__init__(f"No job by the id of {job_id} was found")


class CallableLookupError(LookupError):
    """Raised when the target callable for a job could not be found."""


class JobResultNotReady(Exception):
    """
    Raised by :meth:`~Scheduler.get_job_result` if the job result is
    not ready.
    """

    def __init__(self, job_id: UUID):
        super().__init__(f"No job by the id of {job_id} was found")


class JobCancelled(Exception):
    """
    Raised by :meth:`~Scheduler.get_job_result` if the job was
    cancelled.
    """


class JobDeadlineMissed(Exception):
    """
    Raised by :meth:`~Scheduler.get_job_result` if the job failed to
    start within the allotted time.
    """


class ConflictingIdError(KeyError):
    """
    Raised when trying to add a schedule to a store that already contains a schedule by
    that ID, and the conflict policy of ``exception`` is used.
    """

    def __init__(self, schedule_id):
        super().__init__(
            f"This data store already contains a schedule with the identifier "
            f"{schedule_id!r}"
        )


class SerializationError(Exception):
    """Raised when a serializer fails to serialize the given object."""


class DeserializationError(Exception):
    """Raised when a serializer fails to deserialize the given object."""


class MaxIterationsReached(Exception):
    """
    Raised when a trigger has reached its maximum number of allowed computation
    iterations when trying to calculate the next fire time.
    """


================================================
FILE: src/apscheduler/_marshalling.py
================================================
from __future__ import annotations

from collections.abc import Callable
from datetime import tzinfo
from functools import partial
from inspect import isclass, ismethod, ismethoddescriptor
from typing import Any
from zoneinfo import ZoneInfo

from ._exceptions import DeserializationError, SerializationError


def marshal_object(obj) -> tuple[str, Any]:
    return (
        f"{obj.__class__.__module__}:{obj.__class__.__qualname__}",
        obj.__getstate__(),
    )


def unmarshal_object(ref: str, state: Any) -> Any:
    cls = callable_from_ref(ref)
    if not isinstance(cls, type):
        raise TypeError(f"{ref} is not a class")

    instance = cls.__new__(cls)
    instance.__setstate__(state)
    return instance


def marshal_timezone(value: tzinfo) -> str:
    if isinstance(value, ZoneInfo):
        return value.key
    elif hasattr(value, "zone"):  # pytz timezones
        return value.zone

    raise SerializationError(
        f"Unserializable time zone: {value!r}\n"
        f"Only time zones from the zoneinfo or pytz modules can be serialized."
    )


def unmarshal_timezone(value: str) -> ZoneInfo:
    return ZoneInfo(value)


def callable_to_ref(func: Callable) -> str:
    """
    Return a reference to the given callable.

    :raises SerializationError: if the given object is not callable, is a partial(),
        bound method, lambda or local function or does not have the ``__module__`` and
        ``__qualname__`` attributes

    """
    if isinstance(func, partial):
        raise SerializationError("Cannot create a reference to a partial()")

    if ismethod(func):
        if not isclass(func.__self__):
            raise SerializationError("Cannot create a reference to an instance method")

        return f"{func.__module__}:{func.__self__.__qualname__}.{func.__name__}"

    if ismethoddescriptor(func):
        return f"{func.__objclass__.__module__}:{func.__qualname__}"

    if not hasattr(func, "__module__"):
        raise SerializationError("Callable has no __module__ attribute")

    if not hasattr(func, "__qualname__"):
        raise SerializationError("Callable has no __qualname__ attribute")

    if "<lambda>" in func.__qualname__:
        raise SerializationError("Cannot create a reference to a lambda")

    if "<locals>" in func.__qualname__:
        raise SerializationError("Cannot create a reference to a nested function")

    return f"{func.__module__}:{func.__qualname__}"


def callable_from_ref(ref: str) -> Callable:
    """
    Return the callable pointed to by ``ref``.

    :raises DeserializationError: if the reference could not be resolved or the looked
        up object is not callable

    """
    if ":" not in ref:
        raise ValueError(f"Invalid reference: {ref}")

    modulename, rest = ref.split(":", 1)
    try:
        obj = __import__(modulename, fromlist=[rest])
    except ImportError as exc:
        raise LookupError(
            f"Error resolving reference {ref!r}: could not import module"
        ) from exc

    try:
        for name in rest.split("."):
            obj = getattr(obj, name)
    except Exception as exc:
        raise DeserializationError(
            f"Error resolving reference {ref!r}: error looking up object"
        ) from exc

    if not callable(obj):
        raise DeserializationError(
            f"{ref!r} points to an object of type "
            f"{obj.__class__.__qualname__} which is not callable"
        )

    return obj


================================================
FILE: src/apscheduler/_retry.py
================================================
from __future__ import annotations

from logging import Logger

import attrs
from attr.validators import instance_of
from tenacity import (
    AsyncRetrying,
    RetryCallState,
    retry_if_exception_type,
    stop_after_delay,
    wait_exponential,
)
from tenacity.stop import stop_base
from tenacity.wait import wait_base


@attrs.define(kw_only=True, frozen=True)
class RetrySettings:
    """
    Settings for retrying an operation with Tenacity.

    :param stop: defines when to stop trying
    :param wait: defines how long to wait between attempts
    """

    stop: stop_base = attrs.field(
        validator=instance_of(stop_base),
        default=stop_after_delay(60),
    )
    wait: wait_base = attrs.field(
        validator=instance_of(wait_base),
        default=wait_exponential(min=0.5, max=20),
    )


@attrs.define(kw_only=True, slots=False)
class RetryMixin:
    """
    Mixin that provides support for retrying operations.

    :param retry_settings: Tenacity settings for retrying operations in case of a
        database connecitivty problem
    """

    retry_settings: RetrySettings = attrs.field(default=RetrySettings())
    _logger: Logger = attrs.field(init=False)

    @property
    def _temporary_failure_exceptions(self) -> tuple[type[Exception], ...]:
        """
        Tuple of exception classes which indicate that the operation should be retried.

        """
        return ()

    def _retry(self) -> AsyncRetrying:
        def after_attempt(retry_state: RetryCallState) -> None:
            self._logger.warning(
                "Temporary data store error (attempt %d): %s",
                retry_state.attempt_number,
                retry_state.outcome.exception(),
            )

        return AsyncRetrying(
            stop=self.retry_settings.stop,
            wait=self.retry_settings.wait,
            retry=retry_if_exception_type(self._temporary_failure_exceptions),
            after=after_attempt,
            reraise=True,
        )


================================================
FILE: src/apscheduler/_schedulers/__init__.py
================================================


================================================
FILE: src/apscheduler/_schedulers/async_.py
================================================
from __future__ import annotations

import os
import platform
import random
import sys
from collections.abc import Callable, Iterable, Mapping, MutableMapping, Sequence
from contextlib import AsyncExitStack
from datetime import datetime, timedelta, timezone
from functools import partial
from inspect import isbuiltin, isclass, ismethod, ismodule
from logging import Logger, getLogger
from types import TracebackType
from typing import Any, Literal, TypeAlias, TypeVar, cast, overload
from uuid import UUID, uuid4

import anyio
import attrs
from anyio import (
    TASK_STATUS_IGNORED,
    CancelScope,
    create_task_group,
    get_cancelled_exc_class,
    move_on_after,
    sleep,
)
from anyio.abc import TaskGroup, TaskStatus
from attr.validators import instance_of, optional

from .. import JobAdded, SerializationError, TaskLookupError
from .._context import current_async_scheduler, current_job
from .._converters import as_enum, as_timedelta
from .._decorators import TaskParameters, get_task_params
from .._enums import CoalescePolicy, ConflictPolicy, JobOutcome, RunState, SchedulerRole
from .._events import (
    Event,
    JobReleased,
    ScheduleAdded,
    SchedulerStarted,
    SchedulerStopped,
    ScheduleUpdated,
    T_Event,
)
from .._exceptions import (
    CallableLookupError,
    DeserializationError,
    JobCancelled,
    JobDeadlineMissed,
    JobLookupError,
    ScheduleLookupError,
)
from .._marshalling import callable_from_ref, callable_to_ref
from .._structures import (
    Job,
    JobResult,
    MetadataType,
    Schedule,
    ScheduleResult,
    Task,
    TaskDefaults,
)
from .._utils import UnsetValue, create_repr, merge_metadata, unset
from .._validators import non_negative_number
from ..abc import DataStore, EventBroker, JobExecutor, Subscription, Trigger
from ..datastores.memory import MemoryDataStore
from ..eventbrokers.local import LocalEventBroker
from ..executors.async_ import AsyncJobExecutor
from ..executors.subprocess import ProcessPoolJobExecutor
from ..executors.thread import ThreadPoolJobExecutor

if sys.version_info >= (3, 11):
    from typing import Self
else:
    from typing_extensions import Self

_microsecond_delta = timedelta(microseconds=1)
_zero_timedelta = timedelta()

TaskType: TypeAlias = "Task | str | Callable[..., Any]"
T = TypeVar("T")


@attrs.define(eq=False, repr=False)
class AsyncScheduler:
    """
    An asynchronous (AnyIO based) scheduler implementation.

    Requires either :mod:`asyncio` or Trio_ to work.

    .. note:: If running on Trio, ensure that the data store and event broker are
        compatible with Trio.

    .. _AnyIO: https://pypi.org/project/anyio/
    .. _Trio: https://pypi.org/project/trio/

    :param data_store: the data store for tasks, schedules and jobs
    :param event_broker: the event broker to use for publishing an subscribing events
    :param identity: the unique identifier of the scheduler
    :param role: specifies what the scheduler should be doing when running (scheduling
        only, job running only, or both)
    :param max_concurrent_jobs: Maximum number of jobs the scheduler will run at once
    :param job_executors: a mutable mapping of executor names to executor instances
    :param task_defaults: default settings for newly configured tasks
    :param cleanup_interval: interval (as seconds or timedelta) between automatic
        calls to :meth:`cleanup` – ``None`` to disable automatic clean-up
    :param lease_duration: maximum amount of time (as seconds or timedelta) that
        the scheduler can keep a lock on a schedule or task
    :param logger: the logger instance used to log events from the scheduler, data store
        and event broker
    """

    data_store: DataStore = attrs.field(
        validator=instance_of(DataStore), factory=MemoryDataStore
    )
    event_broker: EventBroker = attrs.field(
        validator=instance_of(EventBroker), factory=LocalEventBroker
    )
    identity: str = attrs.field(kw_only=True, validator=instance_of(str), default="")
    role: SchedulerRole = attrs.field(
        kw_only=True, converter=as_enum(SchedulerRole), default=SchedulerRole.both
    )
    task_defaults: TaskDefaults = attrs.field(kw_only=True, factory=TaskDefaults)
    max_concurrent_jobs: int = attrs.field(
        kw_only=True, validator=non_negative_number, default=100
    )
    job_executors: MutableMapping[str, JobExecutor] = attrs.field(
        kw_only=True, validator=instance_of(MutableMapping), factory=dict
    )
    cleanup_interval: timedelta | None = attrs.field(
        kw_only=True,
        converter=as_timedelta,
        validator=optional(instance_of(timedelta)),
        default=timedelta(minutes=15),
    )
    lease_duration: timedelta = attrs.field(converter=as_timedelta, default=30)
    logger: Logger = attrs.field(kw_only=True, default=getLogger(__name__))

    _state: RunState = attrs.field(init=False, default=RunState.stopped)
    _services_task_group: TaskGroup | None = attrs.field(init=False, default=None)
    _exit_stack: AsyncExitStack = attrs.field(init=False)
    _services_initialized: bool = attrs.field(init=False, default=False)
    _scheduler_cancel_scope: CancelScope | None = attrs.field(init=False, default=None)
    _running_jobs: set[Job] = attrs.field(init=False, factory=set)
    _task_callables: dict[str, Callable] = attrs.field(init=False, factory=dict)

    def __attrs_post_init__(self) -> None:
        if not self.identity:
            self.identity = f"{platform.node()}-{os.getpid()}-{id(self)}"

        if not self.job_executors:
            self.job_executors = {
                "async": AsyncJobExecutor(),
                "threadpool": ThreadPoolJobExecutor(),
                "processpool": ProcessPoolJobExecutor(),
            }

        if self.task_defaults.job_executor is unset:
            self.task_defaults.job_executor = next(iter(self.job_executors))
        elif self.task_defaults.job_executor not in self.job_executors:
            valid_executors = ", ".join(self.job_executors)
            raise ValueError(
                f"the default job executor must be one of the given job executors "
                f"({valid_executors})"
            )

        if self.task_defaults.max_running_jobs is unset:
            self.task_defaults.max_running_jobs = 1

        if self.task_defaults.misfire_grace_time is unset:
            self.task_defaults.misfire_grace_time = None

    async def __aenter__(self) -> Self:
        async with AsyncExitStack() as exit_stack:
            await self._ensure_services_initialized(exit_stack)
            self._services_task_group = await exit_stack.enter_async_context(
                create_task_group()
            )
            exit_stack.callback(setattr, self, "_services_task_group", None)
            exit_stack.push_async_callback(self.stop)
            self._exit_stack = exit_stack.pop_all()

        return self

    async def __aexit__(
        self,
        exc_type: type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: TracebackType | None,
    ) -> None:
        await self._exit_stack.__aexit__(exc_type, exc_val, exc_tb)

    def __repr__(self) -> str:
        return create_repr(self, "identity", "role", "data_store", "event_broker")

    async def _ensure_services_initialized(self, exit_stack: AsyncExitStack) -> None:
        """
        Initialize the data store and event broker if this hasn't already been done.

        """
        if not self._services_initialized:
            self._services_initialized = True
            exit_stack.callback(setattr, self, "_services_initialized", False)

            await self.event_broker.start(exit_stack, self.logger)
            await self.data_store.start(exit_stack, self.event_broker, self.logger)

    def _check_initialized(self) -> None:
        """Raise RuntimeError if the services have not been initialized yet."""
        if not self._services_initialized:
            raise RuntimeError(
                "The scheduler has not been initialized yet. Use the scheduler as an "
                "async context manager (async with ...) in order to call methods other "
                "than run_until_stopped()."
            )

    async def _cleanup_loop(self) -> None:
        delay = self.cleanup_interval.total_seconds()
        assert delay > 0
        while self._state in (RunState.starting, RunState.started):
            await self.cleanup()
            await sleep(delay)

    @property
    def state(self) -> RunState:
        """The current running state of the scheduler."""
        return self._state

    async def cleanup(self) -> None:
        """Clean up expired job results and finished schedules."""
        await self.data_store.cleanup()
        self.logger.info("Cleaned up expired job results and finished schedules")

    @overload
    def subscribe(
        self,
        callback: Callable[[T_Event], Any],
        event_types: type[T_Event],
        *,
        one_shot: bool = ...,
        is_async: bool = ...,
    ) -> Subscription: ...

    @overload
    def subscribe(
        self,
        callback: Callable[[Event], Any],
        event_types: Iterable[type[Event]] | None = None,
        *,
        one_shot: bool = False,
        is_async: bool = True,
    ) -> Subscription: ...

    def subscribe(
        self,
        callback: Callable[[T_Event], Any],
        event_types: type[T_Event] | Iterable[type[T_Event]] | None = None,
        *,
        one_shot: bool = False,
        is_async: bool = True,
    ) -> Subscription:
        """
        Subscribe to events.

        To unsubscribe, call the :meth:`~abc.Subscription.unsubscribe` method on the
        returned object.

        :param callback: callable to be called with the event object when an event is
            published
        :param event_types: an event class or an iterable event classes to subscribe to
        :param one_shot: if ``True``, automatically unsubscribe after the first matching
            event
        :param is_async: ``True`` if the (synchronous) callback should be called on the
            event loop thread, ``False`` if it should be called in a worker thread.
            If ``callback`` is a coroutine function, this flag is ignored.

        """
        self._check_initialized()
        if isclass(event_types):
            event_types = {event_types}

        return self.event_broker.subscribe(
            callback, event_types, is_async=is_async, one_shot=one_shot
        )

    @overload
    async def get_next_event(self, event_types: type[T_Event]) -> T_Event: ...

    @overload
    async def get_next_event(self, event_types: Iterable[type[Event]]) -> Event: ...

    async def get_next_event(
        self, event_types: type[Event] | Iterable[type[Event]]
    ) -> Event:
        """
        Wait until the next event matching one of the given types arrives.

        :param event_types: an event class or an iterable event classes to subscribe to

        """
        received_event: Event | None = None

        def receive_event(ev: Event) -> None:
            nonlocal received_event
            received_event = ev
            event.set()

        event = anyio.Event()
        with self.subscribe(receive_event, event_types, one_shot=True):
            await event.wait()
            return received_event

    async def configure_task(
        self,
        func_or_task_id: TaskType,
        *,
        func: Callable[..., Any] | UnsetValue = unset,
        job_executor: str | UnsetValue = unset,
        misfire_grace_time: float | timedelta | None | UnsetValue = unset,
        max_running_jobs: int | None | UnsetValue = unset,
        metadata: MetadataType | UnsetValue = unset,
    ) -> Task:
        """
        Add or update a :ref:`task <task>` definition.

        Any options not explicitly passed to this method will use their default values
        (from ``task_defaults``) when a new task is created:

        * ``job_executor``: the value of ``default_job_executor`` scheduler attribute
        * ``misfire_grace_time``: ``None``
        * ``max_running_jobs``: 1

        When updating a task, any options not explicitly passed will remain the same.

        If a callable is passed as the first argument, its fully qualified name will be
        used as the task ID.

        :param func_or_task_id: either a task, task ID or a callable
        :param func: a callable that will be associated with the task (can be omitted if
            the callable is already passed as ``func_or_task_id``)
        :param job_executor: name of the job executor to run the task with
        :param misfire_grace_time: maximum number of seconds the scheduled job's actual
            run time is allowed to be late, compared to the scheduled run time
        :param max_running_jobs: maximum number of instances of the task that are
            allowed to run concurrently
        :param metadata: key-value pairs for storing JSON compatible custom information
        :raises TypeError: if ``func_or_task_id`` is neither a task, task ID or a
            callable
        :return: the created or updated task definition

        """
        func_ref: str | None = None
        task: Task | None = None
        if callable(func_or_task_id):
            task_params = get_task_params(func_or_task_id)
            if task_params.id is unset:
                task_params.id = callable_to_ref(func_or_task_id)

            if func is unset:
                func = func_or_task_id
        elif isinstance(func_or_task_id, Task):
            task_params = TaskParameters(
                id=func_or_task_id.id,
                job_executor=func_or_task_id.job_executor,
                max_running_jobs=func_or_task_id.max_running_jobs,
                misfire_grace_time=func_or_task_id.misfire_grace_time,
                metadata=func_or_task_id.metadata,
            )
        elif isinstance(func_or_task_id, str) and func_or_task_id:
            try:
                task = await self.data_store.get_task(func_or_task_id)
                task_params = TaskParameters(
                    id=task.id,
                    job_executor=task.job_executor,
                    max_running_jobs=task.max_running_jobs,
                    misfire_grace_time=task.misfire_grace_time,
                    metadata=task.metadata,
                )
            except TaskLookupError:
                task_params = (
                    get_task_params(func) if callable(func) else TaskParameters()
                )
                task_params.id = func_or_task_id
        else:
            raise TypeError(
                "func_or_task_id must be either a task, its identifier or a callable"
            )

        assert task_params.id

        # Apply any settings passed directly to this function as arguments
        if job_executor is not unset:
            task_params.job_executor = job_executor
        if max_running_jobs is not unset:
            task_params.max_running_jobs = max_running_jobs
        if misfire_grace_time is not unset:
            task_params.misfire_grace_time = misfire_grace_time

        # Fill in unset values with the defaults
        if task_params.job_executor is unset:
            task_params.job_executor = self.task_defaults.job_executor
        if task_params.max_running_jobs is unset:
            task_params.max_running_jobs = self.task_defaults.max_running_jobs
        if task_params.misfire_grace_time is unset:
            task_params.misfire_grace_time = self.task_defaults.misfire_grace_time

        # Merge the metadata from the defaults, task definition and explicitly passed
        # metadata
        task_params.metadata = merge_metadata(
            self.task_defaults.metadata, task_params.metadata, metadata
        )

        if callable(func):
            self._task_callables[task_params.id] = func
            try:
                func_ref = callable_to_ref(func)
            except SerializationError:
                pass

        modified = False
        try:
            task = task or await self.data_store.get_task(cast(str, task_params.id))
        except TaskLookupError:
            task = Task(
                id=task_params.id,
                func=func_ref,
                job_executor=task_params.job_executor,
                max_running_jobs=task_params.max_running_jobs,
                misfire_grace_time=task_params.misfire_grace_time,
                metadata=task_params.metadata,
            )
            modified = True
        else:
            changes: dict[str, Any] = {}
            if func is not unset and task.func != func_ref:
                changes["func"] = func_ref

            if task_params.job_executor != task.job_executor:
                changes["job_executor"] = task_params.job_executor

            if task_params.max_running_jobs != task.max_running_jobs:
                changes["max_running_jobs"] = task_params.max_running_jobs

            if task_params.misfire_grace_time != task.misfire_grace_time:
                changes["misfire_grace_time"] = task_params.misfire_grace_time

            if task_params.metadata != task.metadata:
                changes["metadata"] = task_params.metadata

            if changes:
                task = attrs.evolve(task, **changes)
                modified = True

        if modified:
            await self.data_store.add_task(task)

        return task

    async def get_tasks(self) -> Sequence[Task]:
        """
        Retrieve all currently defined tasks.

        :return: a sequence of tasks, sorted by ID

        """
        self._check_initialized()
        return await self.data_store.get_tasks()

    async def add_schedule(
        self,
        func_or_task_id: TaskType,
        trigger: Trigger,
        *,
        id: str | None = None,
        args: Iterable[Any] | None = None,
        kwargs: Mapping[str, Any] | None = None,
        paused: bool = False,
        coalesce: CoalescePolicy = CoalescePolicy.latest,
        job_executor: str | UnsetValue = unset,
        misfire_grace_time: float | timedelta | None | UnsetValue = unset,
        metadata: MetadataType | UnsetValue = unset,
        max_jitter: float | timedelta | None = None,
        job_result_expiration_time: float | timedelta = 0,
        conflict_policy: ConflictPolicy = ConflictPolicy.do_nothing,
    ) -> str:
        """
        Schedule a task to be run one or more times in the future.

        :param func_or_task_id: either a callable or an ID of an existing task
            definition
        :param trigger: determines the times when the task should be run
        :param id: an explicit identifier for the schedule (if omitted, a random, UUID
            based ID will be assigned)
        :param args: positional arguments to be passed to the task function
        :param kwargs: keyword arguments to be passed to the task function
        :param paused: whether the schedule is paused
        :param job_executor: name of the job executor to run the scheduled jobs with
            (overrides the executor specified in the task settings)
        :param coalesce: determines what to do when processing the schedule if multiple
            fire times have become due for this schedule since the last processing
        :param misfire_grace_time: maximum number of seconds the scheduled job's actual
            run time is allowed to be late, compared to the scheduled run time
        :param metadata: key-value pairs for storing JSON compatible custom information
        :param max_jitter: maximum time (in seconds, or as a timedelta) to randomly add
            to the scheduled time for each job created from this schedule
        :param job_result_expiration_time: minimum time (in seconds, or as a timedelta)
            to keep the job results in storage from the jobs created by this schedule
        :param conflict_policy: determines what to do if a schedule with the same ID
            already exists in the data store
        :return: the ID of the newly added schedule

        """
        self._check_initialized()
        schedule_id = id or str(uuid4())
        args = tuple(args or ())
        kwargs = dict(kwargs or {})

        # Unpack the function and positional + keyword arguments from a partial()
        if isinstance(func_or_task_id, partial):
            args = func_or_task_id.args + args
            kwargs.update(func_or_task_id.keywords)
            func_or_task_id = func_or_task_id.func

        # For instance methods, use the unbound function as the function, and  the
        # "self" argument as the first positional argument
        if ismethod(func_or_task_id):
            args = (func_or_task_id.__self__, *args)
            func_or_task_id = func_or_task_id.__func__
        elif (
            isbuiltin(func_or_task_id)
            and func_or_task_id.__self__ is not None
            and not ismodule(func_or_task_id.__self__)
        ):
            args = (func_or_task_id.__self__, *args)
            method_class = type(func_or_task_id.__self__)
            func_or_task_id = getattr(method_class, func_or_task_id.__name__)

        task = await self.configure_task(func_or_task_id)
        schedule = Schedule(
            id=schedule_id,
            task_id=task.id,
            trigger=trigger,
            args=args,
            kwargs=kwargs,
            paused=paused,
            coalesce=coalesce,
            misfire_grace_time=task.misfire_grace_time
            if misfire_grace_time is unset
            else misfire_grace_time,
            metadata=task.metadata.copy()
            if metadata is unset
            else merge_metadata(task.metadata, metadata),
            max_jitter=max_jitter,
            job_executor=task.job_executor if job_executor is unset else job_executor,
            job_result_expiration_time=job_result_expiration_time,
        )
        schedule.next_fire_time = trigger.next()
        await self.data_store.add_schedule(schedule, conflict_policy)
        self.logger.info(
            "Added new schedule (task=%r, trigger=%r); next run time at %s",
            task.id,
            trigger,
            schedule.next_fire_time,
        )
        return schedule.id

    async def get_schedule(self, id: str) -> Schedule:
        """
        Retrieve a schedule from the data store.

        :param id: the unique identifier of the schedule
        :raises ScheduleLookupError: if the schedule could not be found

        """
        self._check_initialized()
        schedules = await self.data_store.get_schedules({id})
        if schedules:
            return schedules[0]
        else:
            raise ScheduleLookupError(id)

    async def get_schedules(self) -> list[Schedule]:
        """
        Retrieve all schedules from the data store.

        :return: a list of schedules, in an unspecified order

        """
        self._check_initialized()
        return await self.data_store.get_schedules()

    async def remove_schedule(self, id: str) -> None:
        """
        Remove the given schedule from the data store.

        :param id: the unique identifier of the schedule

        """
        self._check_initialized()
        await self.data_store.remove_schedules({id})

    async def pause_schedule(self, id: str) -> None:
        """Pause the specified schedule."""
        self._check_initialized()
        await self.data_store.add_schedule(
            schedule=attrs.evolve(await self.get_schedule(id), paused=True),
            conflict_policy=ConflictPolicy.replace,
        )

    async def unpause_schedule(
        self,
        id: str,
        *,
        resume_from: datetime | Literal["now"] | None = None,
    ) -> None:
        """
        Unpause the specified schedule.


        :param resume_from: the time to resume the schedules from, or ``'now'`` as a
            shorthand for ``datetime.now(tz=UTC)`` or ``None`` to resume from where the
            schedule left off which may cause it to misfire

        """
        self._check_initialized()
        schedule = await self.get_schedule(id)

        if resume_from == "now":
            resume_from = datetime.now(tz=timezone.utc)

        if resume_from is None:
            next_fire_time = schedule.next_fire_time
        elif (
            schedule.next_fire_time is not None
            and schedule.next_fire_time >= resume_from
        ):
            next_fire_time = schedule.next_fire_time
        else:
            # Advance `next_fire_time` until its at or past `resume_from`, or until it's
            # exhausted
            while next_fire_time := schedule.trigger.next():
                if next_fire_time is None or next_fire_time >= resume_from:
                    break

        await self.data_store.add_schedule(
            schedule=attrs.evolve(
                schedule,
                paused=False,
                next_fire_time=next_fire_time,
            ),
            conflict_policy=ConflictPolicy.replace,
        )

    async def add_job(
        self,
        func_or_task_id: TaskType,
        *,
        args: Iterable[Any] | None = None,
        kwargs: Mapping[str, Any] | None = None,
        job_executor: str | UnsetValue = unset,
        metadata: MetadataType | UnsetValue = unset,
        result_expiration_time: timedelta | float = 0,
    ) -> UUID:
        """
        Add a job to the data store.

        :param func_or_task_id:
            Either the ID of a pre-existing task, or a function/method. If a function is
            given, a task will be created with the fully qualified name of the function
            as the task ID (unless that task already exists of course).
        :param args: positional arguments to call the target callable with
        :param kwargs: keyword arguments to call the target callable with
        :param job_executor: name of the job executor to run the task with
            (overrides the executor in the task definition, if any)
        :param metadata: key-value pairs for storing JSON compatible custom information
        :param result_expiration_time: the minimum time (as seconds, or timedelta) to
            keep the result of the job available for fetching (the result won't be
            saved at all if that time is 0)
        :return: the ID of the newly created job

        """
        self._check_initialized()
        args = tuple(args or ())
        kwargs = dict(kwargs or {})

        # Unpack the function and positional + keyword arguments from a 
Download .txt
gitextract_2so_8z_h/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yaml
│   │   ├── config.yml
│   │   └── features_request.yaml
│   ├── dependabot.yml
│   ├── pull_request_template.md
│   └── workflows/
│       ├── publish.yml
│       └── test.yml
├── .gitignore
├── .mailmap
├── .pre-commit-config.yaml
├── .readthedocs.yml
├── LICENSE.txt
├── README.rst
├── docker-compose.yml
├── docs/
│   ├── api.rst
│   ├── conf.py
│   ├── contributing.rst
│   ├── extending.rst
│   ├── faq.rst
│   ├── index.rst
│   ├── integrations.rst
│   ├── migration.rst
│   ├── userguide.rst
│   └── versionhistory.rst
├── examples/
│   ├── README.rst
│   ├── gui/
│   │   └── qt_executor.py
│   ├── separate_worker/
│   │   ├── async_scheduler.py
│   │   ├── async_worker.py
│   │   ├── example_tasks.py
│   │   ├── sync_scheduler.py
│   │   └── sync_worker.py
│   ├── standalone/
│   │   ├── async_memory.py
│   │   ├── async_mysql.py
│   │   ├── async_postgres.py
│   │   └── sync_memory.py
│   └── web/
│       ├── asgi_fastapi.py
│       ├── asgi_noframework.py
│       ├── asgi_starlette.py
│       ├── wsgi_flask.py
│       └── wsgi_noframework.py
├── pyproject.toml
├── src/
│   └── apscheduler/
│       ├── __init__.py
│       ├── _context.py
│       ├── _converters.py
│       ├── _decorators.py
│       ├── _enums.py
│       ├── _events.py
│       ├── _exceptions.py
│       ├── _marshalling.py
│       ├── _retry.py
│       ├── _schedulers/
│       │   ├── __init__.py
│       │   ├── async_.py
│       │   └── sync.py
│       ├── _structures.py
│       ├── _utils.py
│       ├── _validators.py
│       ├── abc.py
│       ├── datastores/
│       │   ├── __init__.py
│       │   ├── base.py
│       │   ├── memory.py
│       │   ├── mongodb.py
│       │   └── sqlalchemy.py
│       ├── eventbrokers/
│       │   ├── __init__.py
│       │   ├── asyncpg.py
│       │   ├── base.py
│       │   ├── local.py
│       │   ├── mqtt.py
│       │   ├── psycopg.py
│       │   └── redis.py
│       ├── executors/
│       │   ├── __init__.py
│       │   ├── async_.py
│       │   ├── qt.py
│       │   ├── subprocess.py
│       │   └── thread.py
│       ├── py.typed
│       ├── serializers/
│       │   ├── __init__.py
│       │   ├── cbor.py
│       │   ├── json.py
│       │   └── pickle.py
│       └── triggers/
│           ├── __init__.py
│           ├── calendarinterval.py
│           ├── combining.py
│           ├── cron/
│           │   ├── __init__.py
│           │   ├── expressions.py
│           │   └── fields.py
│           ├── date.py
│           └── interval.py
├── tests/
│   ├── conftest.py
│   ├── test_datastores.py
│   ├── test_eventbrokers.py
│   ├── test_marshalling.py
│   ├── test_schedulers.py
│   ├── test_serializers.py
│   └── triggers/
│       ├── test_calendarinterval.py
│       ├── test_combining.py
│       ├── test_cron.py
│       ├── test_date.py
│       └── test_interval.py
└── tools/
    └── dockerize
Download .txt
SYMBOL INDEX (712 symbols across 61 files)

FILE: examples/gui/qt_executor.py
  class MainWindow (line 19) | class MainWindow(QMainWindow):
    method __init__ (line 20) | def __init__(self):
    method update_time (line 33) | def update_time(self) -> None:

FILE: examples/separate_worker/async_scheduler.py
  function main (line 26) | async def main():

FILE: examples/separate_worker/async_worker.py
  function main (line 25) | async def main():

FILE: examples/separate_worker/example_tasks.py
  function tick (line 11) | def tick():

FILE: examples/standalone/async_memory.py
  function tick (line 18) | def tick():
  function main (line 22) | async def main():

FILE: examples/standalone/async_mysql.py
  function tick (line 24) | def tick():
  function main (line 28) | async def main():

FILE: examples/standalone/async_postgres.py
  function tick (line 24) | def tick():
  function main (line 28) | async def main():

FILE: examples/standalone/sync_memory.py
  function tick (line 17) | def tick():

FILE: examples/web/asgi_fastapi.py
  function tick (line 28) | def tick():
  function lifespan (line 33) | async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
  function root (line 47) | async def root() -> Response:

FILE: examples/web/asgi_noframework.py
  function tick (line 24) | def tick():
  function original_app (line 28) | async def original_app(scope, receive, send):
  function scheduler_middleware (line 58) | async def scheduler_middleware(scope, receive, send):

FILE: examples/web/asgi_starlette.py
  function tick (line 30) | def tick():
  class SchedulerMiddleware (line 34) | class SchedulerMiddleware:
    method __init__ (line 35) | def __init__(
    method __call__ (line 43) | async def __call__(self, scope: Scope, receive: Receive, send: Send) -...
  function root (line 55) | async def root(request: Request) -> Response:

FILE: examples/web/wsgi_flask.py
  function tick (line 27) | def tick():
  function hello_world (line 32) | def hello_world():

FILE: examples/web/wsgi_noframework.py
  function tick (line 24) | def tick():
  function application (line 28) | def application(environ, start_response):

FILE: src/apscheduler/_converters.py
  function as_int (line 12) | def as_int(value: int | str) -> int:
  function as_datetime (line 19) | def as_datetime(value: datetime | str) -> datetime:
  function as_aware_datetime (line 30) | def as_aware_datetime(value: datetime | str) -> datetime:
  function as_date (line 38) | def as_date(value: date | str) -> date:
  function as_timezone (line 45) | def as_timezone(value: tzinfo | str) -> tzinfo:
  function as_uuid (line 54) | def as_uuid(value: UUID | str) -> UUID:
  function as_timedelta (line 61) | def as_timedelta(value: timedelta | int) -> timedelta:
  function as_enum (line 68) | def as_enum(enum_class: Any) -> Callable[[Any], Any]:
  function list_converter (line 78) | def list_converter(converter: Callable[[Any], Any]) -> Callable[[Any], A...

FILE: src/apscheduler/_decorators.py
  class TaskParameters (line 21) | class TaskParameters(TaskDefaults):
  function task (line 39) | def task(
  function get_task_params (line 85) | def get_task_params(func: Callable[..., Any]) -> TaskParameters:

FILE: src/apscheduler/_enums.py
  class SchedulerRole (line 6) | class SchedulerRole(Enum):
  class RunState (line 28) | class RunState(Enum):
  class JobOutcome (line 55) | class JobOutcome(Enum):
  class ConflictPolicy (line 94) | class ConflictPolicy(Enum):
  class CoalescePolicy (line 117) | class CoalescePolicy(Enum):

FILE: src/apscheduler/_events.py
  class Event (line 21) | class Event:
    method marshal (line 32) | def marshal(self) -> dict[str, Any]:
    method unmarshal (line 36) | def unmarshal(cls, marshalled: dict[str, Any]) -> Event:
  class DataStoreEvent (line 46) | class DataStoreEvent(Event):
  class TaskAdded (line 51) | class TaskAdded(DataStoreEvent):
  class TaskUpdated (line 62) | class TaskUpdated(DataStoreEvent):
  class TaskRemoved (line 73) | class TaskRemoved(DataStoreEvent):
  class ScheduleAdded (line 84) | class ScheduleAdded(DataStoreEvent):
  class ScheduleUpdated (line 99) | class ScheduleUpdated(DataStoreEvent):
  class ScheduleRemoved (line 114) | class ScheduleRemoved(DataStoreEvent):
  class JobAdded (line 130) | class JobAdded(DataStoreEvent):
  class JobRemoved (line 145) | class JobRemoved(DataStoreEvent):
  class ScheduleDeserializationFailed (line 159) | class ScheduleDeserializationFailed(DataStoreEvent):
  class JobDeserializationFailed (line 172) | class JobDeserializationFailed(DataStoreEvent):
  class SchedulerEvent (line 190) | class SchedulerEvent(Event):
  class SchedulerStarted (line 195) | class SchedulerStarted(SchedulerEvent):
  class SchedulerStopped (line 200) | class SchedulerStopped(SchedulerEvent):
  class JobAcquired (line 211) | class JobAcquired(SchedulerEvent):
    method from_job (line 230) | def from_job(cls, job: Job, scheduler_id: str) -> JobAcquired:
  class JobReleased (line 249) | class JobReleased(SchedulerEvent):
    method from_result (line 282) | def from_result(
    method marshal (line 325) | def marshal(self) -> dict[str, Any]:

FILE: src/apscheduler/_exceptions.py
  class TaskLookupError (line 6) | class TaskLookupError(LookupError):
    method __init__ (line 9) | def __init__(self, task_id: str):
  class ScheduleLookupError (line 13) | class ScheduleLookupError(LookupError):
    method __init__ (line 16) | def __init__(self, schedule_id: str):
  class JobLookupError (line 20) | class JobLookupError(LookupError):
    method __init__ (line 23) | def __init__(self, job_id: UUID):
  class CallableLookupError (line 27) | class CallableLookupError(LookupError):
  class JobResultNotReady (line 31) | class JobResultNotReady(Exception):
    method __init__ (line 37) | def __init__(self, job_id: UUID):
  class JobCancelled (line 41) | class JobCancelled(Exception):
  class JobDeadlineMissed (line 48) | class JobDeadlineMissed(Exception):
  class ConflictingIdError (line 55) | class ConflictingIdError(KeyError):
    method __init__ (line 61) | def __init__(self, schedule_id):
  class SerializationError (line 68) | class SerializationError(Exception):
  class DeserializationError (line 72) | class DeserializationError(Exception):
  class MaxIterationsReached (line 76) | class MaxIterationsReached(Exception):

FILE: src/apscheduler/_marshalling.py
  function marshal_object (line 13) | def marshal_object(obj) -> tuple[str, Any]:
  function unmarshal_object (line 20) | def unmarshal_object(ref: str, state: Any) -> Any:
  function marshal_timezone (line 30) | def marshal_timezone(value: tzinfo) -> str:
  function unmarshal_timezone (line 42) | def unmarshal_timezone(value: str) -> ZoneInfo:
  function callable_to_ref (line 46) | def callable_to_ref(func: Callable) -> str:
  function callable_from_ref (line 82) | def callable_from_ref(ref: str) -> Callable:

FILE: src/apscheduler/_retry.py
  class RetrySettings (line 19) | class RetrySettings:
  class RetryMixin (line 38) | class RetryMixin:
    method _temporary_failure_exceptions (line 50) | def _temporary_failure_exceptions(self) -> tuple[type[Exception], ...]:
    method _retry (line 57) | def _retry(self) -> AsyncRetrying:

FILE: src/apscheduler/_schedulers/async_.py
  class AsyncScheduler (line 84) | class AsyncScheduler:
    method __attrs_post_init__ (line 146) | def __attrs_post_init__(self) -> None:
    method __aenter__ (line 172) | async def __aenter__(self) -> Self:
    method __aexit__ (line 184) | async def __aexit__(
    method __repr__ (line 192) | def __repr__(self) -> str:
    method _ensure_services_initialized (line 195) | async def _ensure_services_initialized(self, exit_stack: AsyncExitStac...
    method _check_initialized (line 207) | def _check_initialized(self) -> None:
    method _cleanup_loop (line 216) | async def _cleanup_loop(self) -> None:
    method state (line 224) | def state(self) -> RunState:
    method cleanup (line 228) | async def cleanup(self) -> None:
    method subscribe (line 234) | def subscribe(
    method subscribe (line 244) | def subscribe(
    method subscribe (line 253) | def subscribe(
    method get_next_event (line 286) | async def get_next_event(self, event_types: type[T_Event]) -> T_Event:...
    method get_next_event (line 289) | async def get_next_event(self, event_types: Iterable[type[Event]]) -> ...
    method get_next_event (line 291) | async def get_next_event(
    method configure_task (line 312) | async def configure_task(
    method get_tasks (line 458) | async def get_tasks(self) -> Sequence[Task]:
    method add_schedule (line 468) | async def add_schedule(
    method get_schedule (line 566) | async def get_schedule(self, id: str) -> Schedule:
    method get_schedules (line 581) | async def get_schedules(self) -> list[Schedule]:
    method remove_schedule (line 591) | async def remove_schedule(self, id: str) -> None:
    method pause_schedule (line 601) | async def pause_schedule(self, id: str) -> None:
    method unpause_schedule (line 609) | async def unpause_schedule(
    method add_job (line 653) | async def add_job(
    method get_jobs (line 717) | async def get_jobs(self) -> Sequence[Job]:
    method get_job_result (line 722) | async def get_job_result(
    method run_job (line 758) | async def run_job(
    method stop (line 820) | async def stop(self) -> None:
    method wait_until_stopped (line 832) | async def wait_until_stopped(self) -> None:
    method start_in_background (line 845) | async def start_in_background(self) -> None:
    method run_until_stopped (line 852) | async def run_until_stopped(
    method _process_schedules (line 925) | async def _process_schedules(self, *, task_status: TaskStatus[None]) -...
    method _get_task_callable (line 1091) | def _get_task_callable(self, task: Task) -> Callable:
    method _process_jobs (line 1113) | async def _process_jobs(self, *, task_status: TaskStatus[None]) -> None:
    method _run_job (line 1178) | async def _run_job(self, job: Job, func: Callable[..., Any], executor:...

FILE: src/apscheduler/_schedulers/sync.py
  class Scheduler (line 33) | class Scheduler:
    method __init__ (line 50) | def __init__(
    method logger (line 93) | def logger(self) -> Logger:
    method data_store (line 97) | def data_store(self) -> DataStore:
    method event_broker (line 101) | def event_broker(self) -> EventBroker:
    method identity (line 105) | def identity(self) -> str:
    method role (line 109) | def role(self) -> SchedulerRole:
    method max_concurrent_jobs (line 113) | def max_concurrent_jobs(self) -> int:
    method cleanup_interval (line 117) | def cleanup_interval(self) -> timedelta | None:
    method lease_duration (line 121) | def lease_duration(self) -> timedelta:
    method job_executors (line 125) | def job_executors(self) -> MutableMapping[str, JobExecutor]:
    method task_defaults (line 129) | def task_defaults(self) -> TaskDefaults:
    method state (line 133) | def state(self) -> RunState:
    method __enter__ (line 137) | def __enter__(self: Self) -> Self:
    method __exit__ (line 141) | def __exit__(
    method _ensure_services_ready (line 149) | def _ensure_services_ready(
    method __repr__ (line 173) | def __repr__(self) -> str:
    method cleanup (line 176) | def cleanup(self) -> None:
    method subscribe (line 181) | def subscribe(
    method subscribe (line 190) | def subscribe(
    method subscribe (line 198) | def subscribe(
    method get_next_event (line 230) | def get_next_event(self, event_types: type[T_Event]) -> T_Event: ...
    method get_next_event (line 233) | def get_next_event(self, event_types: Iterable[type[Event]]) -> Event:...
    method get_next_event (line 235) | def get_next_event(self, event_types: type[Event] | Iterable[type[Even...
    method configure_task (line 239) | def configure_task(
    method get_tasks (line 262) | def get_tasks(self) -> Sequence[Task]:
    method add_schedule (line 266) | def add_schedule(
    method get_schedule (line 303) | def get_schedule(self, id: str) -> Schedule:
    method get_schedules (line 307) | def get_schedules(self) -> list[Schedule]:
    method remove_schedule (line 311) | def remove_schedule(self, id: str) -> None:
    method pause_schedule (line 315) | def pause_schedule(self, id: str) -> None:
    method unpause_schedule (line 319) | def unpause_schedule(
    method add_job (line 334) | def add_job(
    method get_jobs (line 357) | def get_jobs(self) -> Sequence[Job]:
    method get_job_result (line 361) | def get_job_result(self, job_id: UUID, *, wait: bool = True) -> JobRes...
    method run_job (line 367) | def run_job(
    method start_in_background (line 388) | def start_in_background(self) -> None:
    method stop (line 410) | def stop(self) -> None:
    method wait_until_stopped (line 414) | def wait_until_stopped(self) -> None:
    method run_until_stopped (line 418) | def run_until_stopped(self) -> None:

FILE: src/apscheduler/_structures.py
  function serialize (line 23) | def serialize(inst: Any, field: attrs.Attribute, value: Any) -> Any:
  class Task (line 31) | class Task:
    method marshal (line 67) | def marshal(self, serializer: Serializer) -> dict[str, Any]:
    method unmarshal (line 71) | def unmarshal(cls, serializer: Serializer, marshalled: dict[str, Any])...
    method __hash__ (line 74) | def __hash__(self) -> int:
    method __eq__ (line 77) | def __eq__(self, other: object) -> bool:
    method __lt__ (line 83) | def __lt__(self, other: object) -> bool:
  class TaskDefaults (line 91) | class TaskDefaults:
  class Schedule (line 120) | class Schedule:
    method marshal (line 199) | def marshal(self, serializer: Serializer) -> dict[str, Any]:
    method unmarshal (line 211) | def unmarshal(cls, serializer: Serializer, marshalled: dict[str, Any])...
    method __hash__ (line 217) | def __hash__(self) -> int:
    method __eq__ (line 220) | def __eq__(self, other: object) -> bool:
    method __lt__ (line 226) | def __lt__(self, other: object) -> bool:
  class ScheduleResult (line 243) | class ScheduleResult:
  class Job (line 262) | class Job:
    method original_scheduled_time (line 322) | def original_scheduled_time(self) -> datetime | None:
    method marshal (line 329) | def marshal(self, serializer: Serializer) -> dict[str, Any]:
    method unmarshal (line 340) | def unmarshal(cls, serializer: Serializer, marshalled: dict[str, Any])...
    method __hash__ (line 349) | def __hash__(self) -> int:
    method __eq__ (line 352) | def __eq__(self, other: object) -> bool:
  class JobResult (line 360) | class JobResult:
    method from_job (line 386) | def from_job(
    method marshal (line 408) | def marshal(self, serializer: Serializer) -> dict[str, Any]:
    method unmarshal (line 423) | def unmarshal(cls, serializer: Serializer, marshalled: dict[str, Any])...
    method __hash__ (line 433) | def __hash__(self) -> int:
    method __eq__ (line 436) | def __eq__(self, other: object) -> bool:

FILE: src/apscheduler/_utils.py
  class UnsetValue (line 24) | class UnsetValue:
    method __new__ (line 29) | def __new__(cls) -> UnsetValue:
    method __getstate__ (line 35) | def __getstate__(self) -> NoReturn:
    method __repr__ (line 38) | def __repr__(self) -> str:
  function timezone_repr (line 45) | def timezone_repr(timezone: tzinfo) -> str:
  function absolute_datetime_diff (line 52) | def absolute_datetime_diff(dateval1: datetime, dateval2: datetime) -> fl...
  function qualified_name (line 56) | def qualified_name(cls: type) -> str:
  function require_state_version (line 64) | def require_state_version(
  function merge_metadata (line 82) | def merge_metadata(
  function create_repr (line 95) | def create_repr(instance: object, *attrnames: str, **kwargs) -> str:
  function time_exists (line 110) | def time_exists(dt: datetime) -> bool:
  function current_async_library (line 121) | def current_async_library() -> str:

FILE: src/apscheduler/_validators.py
  function positive_number (line 11) | def positive_number(instance: Any, attribute: Attribute, value: Any) -> ...
  function non_negative_number (line 16) | def non_negative_number(instance: Any, attribute: Attribute, value: Any)...
  function aware_datetime (line 21) | def aware_datetime(instance: Any, attribute: Attribute, value: Any) -> N...
  function if_not_unset (line 26) | def if_not_unset(validator: Callable[[Any, Any, Any], None]) -> None:
  function valid_metadata (line 34) | def valid_metadata(instance: Any, attribute: Attribute, value: Any) -> N...

FILE: src/apscheduler/abc.py
  class Trigger (line 23) | class Trigger(Iterator[datetime], metaclass=ABCMeta):
    method next (line 31) | def next(self) -> datetime | None:
    method __getstate__ (line 43) | def __getstate__(self) -> Any:
    method __setstate__ (line 47) | def __setstate__(self, state: Any) -> None:
    method __iter__ (line 50) | def __iter__(self) -> Self:
    method __next__ (line 53) | def __next__(self) -> datetime:
  class Serializer (line 61) | class Serializer(metaclass=ABCMeta):
    method serialize (line 67) | def serialize(self, obj: object) -> bytes:
    method deserialize (line 81) | def deserialize(self, serialized: bytes) -> Any:
  class Subscription (line 90) | class Subscription(metaclass=ABCMeta):
    method __enter__ (line 97) | def __enter__(self) -> Subscription:
    method __exit__ (line 100) | def __exit__(self, exc_type, exc_val, exc_tb) -> None:
    method unsubscribe (line 104) | def unsubscribe(self) -> None:
  class EventBroker (line 112) | class EventBroker(metaclass=ABCMeta):
    method start (line 119) | async def start(self, exit_stack: AsyncExitStack, logger: Logger) -> N...
    method publish (line 129) | async def publish(self, event: Event) -> None:
    method publish_local (line 133) | async def publish_local(self, event: Event) -> None:
    method subscribe (line 137) | def subscribe(
  class DataStore (line 159) | class DataStore(metaclass=ABCMeta):
    method start (line 168) | async def start(
    method add_task (line 182) | async def add_task(self, task: Task) -> None:
    method remove_task (line 193) | async def remove_task(self, task_id: str) -> None:
    method get_task (line 202) | async def get_task(self, task_id: str) -> Task:
    method get_tasks (line 212) | async def get_tasks(self) -> list[Task]:
    method get_schedules (line 220) | async def get_schedules(self, ids: set[str] | None = None) -> list[Sch...
    method add_schedule (line 230) | async def add_schedule(
    method remove_schedules (line 242) | async def remove_schedules(self, ids: Iterable[str]) -> None:
    method acquire_schedules (line 250) | async def acquire_schedules(
    method release_schedules (line 274) | async def release_schedules(
    method get_next_schedule_run_time (line 294) | async def get_next_schedule_run_time(self) -> datetime | None:
    method add_job (line 301) | async def add_job(self, job: Job) -> None:
    method get_jobs (line 309) | async def get_jobs(self, ids: Iterable[UUID] | None = None) -> list[Job]:
    method acquire_jobs (line 319) | async def acquire_jobs(
    method release_job (line 336) | async def release_job(self, scheduler_id: str, job: Job, result: JobRe...
    method get_job_result (line 346) | async def get_job_result(self, job_id: UUID) -> JobResult | None:
    method extend_acquired_schedule_leases (line 357) | async def extend_acquired_schedule_leases(
    method extend_acquired_job_leases (line 370) | async def extend_acquired_job_leases(
    method reap_abandoned_jobs (line 382) | async def reap_abandoned_jobs(self, scheduler_id: str) -> None:
    method cleanup (line 396) | async def cleanup(self) -> None:
  class JobExecutor (line 410) | class JobExecutor(metaclass=ABCMeta):
    method start (line 411) | async def start(self, exit_stack: AsyncExitStack) -> None:  # noqa: B027
    method run_job (line 420) | async def run_job(self, func: Callable[..., Any], job: Job) -> Any:

FILE: src/apscheduler/datastores/base.py
  class BaseDataStore (line 14) | class BaseDataStore(DataStore):
    method start (line 20) | async def start(
  class BaseExternalDataStore (line 28) | class BaseExternalDataStore(BaseDataStore, RetryMixin):

FILE: src/apscheduler/datastores/memory.py
  class MemoryDataStore (line 34) | class MemoryDataStore(BaseDataStore):
    method __repr__ (line 52) | def __repr__(self) -> str:
    method _find_schedule_index (line 55) | def _find_schedule_index(self, schedule: Schedule) -> int:
    method get_schedules (line 60) | async def get_schedules(self, ids: set[str] | None = None) -> list[Sch...
    method add_task (line 70) | async def add_task(self, task: Task) -> None:
    method remove_task (line 79) | async def remove_task(self, task_id: str) -> None:
    method get_task (line 87) | async def get_task(self, task_id: str) -> Task:
    method get_tasks (line 93) | async def get_tasks(self) -> list[Task]:
    method add_schedule (line 96) | async def add_schedule(
    method remove_schedules (line 130) | async def remove_schedules(
    method acquire_schedules (line 144) | async def acquire_schedules(
    method release_schedules (line 175) | async def release_schedules(
    method get_next_schedule_run_time (line 198) | async def get_next_schedule_run_time(self) -> datetime | None:
    method add_job (line 201) | async def add_job(self, job: Job) -> None:
    method get_jobs (line 214) | async def get_jobs(self, ids: Iterable[UUID] | None = None) -> list[Job]:
    method acquire_jobs (line 225) | async def acquire_jobs(
    method release_job (line 290) | async def release_job(self, scheduler_id: str, job: Job, result: JobRe...
    method get_job_result (line 327) | async def get_job_result(self, job_id: UUID) -> JobResult | None:
    method extend_acquired_schedule_leases (line 330) | async def extend_acquired_schedule_leases(
    method extend_acquired_job_leases (line 338) | async def extend_acquired_job_leases(
    method reap_abandoned_jobs (line 346) | async def reap_abandoned_jobs(self, scheduler_id: str) -> None:
    method cleanup (line 355) | async def cleanup(self) -> None:

FILE: src/apscheduler/datastores/mongodb.py
  class CustomEncoder (line 54) | class CustomEncoder(TypeEncoder):
    method __init__ (line 55) | def __init__(self, python_type: type, encoder: Callable):
    method python_type (line 60) | def python_type(self) -> type:
    method transform_python (line 63) | def transform_python(self, value: Any) -> Any:
  function marshal_timestamp (line 67) | def marshal_timestamp(timestamp: datetime | None, key: str) -> Mapping[s...
  function marshal_document (line 78) | def marshal_document(document: dict[str, Any]) -> None:
  function unmarshal_timestamps (line 87) | def unmarshal_timestamps(document: dict[str, Any]) -> None:
  class MongoDBDataStore (line 97) | class MongoDBDataStore(BaseExternalDataStore):
    method _temporary_failure_exceptions (line 138) | def _temporary_failure_exceptions(self) -> tuple[type[Exception], ...]:
    method __attrs_post_init__ (line 141) | def __attrs_post_init__(self) -> None:
    method __repr__ (line 166) | def __repr__(self) -> str:
    method _initialize (line 170) | async def _initialize(self) -> None:
    method start (line 187) | async def start(
    method add_task (line 205) | async def add_task(self, task: Task) -> None:
    method remove_task (line 219) | async def remove_task(self, task_id: str) -> None:
    method get_task (line 227) | async def get_task(self, task_id: str) -> Task:
    method get_tasks (line 241) | async def get_tasks(self) -> list[Task]:
    method get_schedules (line 254) | async def get_schedules(self, ids: set[str] | None = None) -> list[Sch...
    method add_schedule (line 275) | async def add_schedule(
    method remove_schedules (line 311) | async def remove_schedules(self, ids: Iterable[str]) -> None:
    method acquire_schedules (line 332) | async def acquire_schedules(
    method release_schedules (line 428) | async def release_schedules(
    method get_next_schedule_run_time (line 489) | async def get_next_schedule_run_time(self) -> datetime | None:
    method add_job (line 504) | async def add_job(self, job: Job) -> None:
    method get_jobs (line 518) | async def get_jobs(self, ids: Iterable[UUID] | None = None) -> list[Job]:
    method acquire_jobs (line 539) | async def acquire_jobs(
    method _release_job (line 709) | async def _release_job(
    method release_job (line 743) | async def release_job(self, scheduler_id: str, job: Job, result: JobRe...
    method get_job_result (line 759) | async def get_job_result(self, job_id: UUID) -> JobResult | None:
    method extend_acquired_schedule_leases (line 771) | async def extend_acquired_schedule_leases(
    method extend_acquired_job_leases (line 789) | async def extend_acquired_job_leases(
    method reap_abandoned_jobs (line 807) | async def reap_abandoned_jobs(self, scheduler_id: str) -> None:
    method cleanup (line 838) | async def cleanup(self) -> None:

FILE: src/apscheduler/datastores/sqlalchemy.py
  class EmulatedTimestampTZ (line 84) | class EmulatedTimestampTZ(TypeDecorator[datetime]):
    method process_bind_param (line 88) | def process_bind_param(
    method process_result_value (line 93) | def process_result_value(
  class EmulatedInterval (line 99) | class EmulatedInterval(TypeDecorator[timedelta]):
    method process_bind_param (line 103) | def process_bind_param(
    method process_result_value (line 108) | def process_result_value(
  function marshal_timestamp (line 114) | def marshal_timestamp(timestamp: datetime | None, key: str) -> Mapping[s...
  class _JobDiscard (line 126) | class _JobDiscard:
  class SQLAlchemyDataStore (line 137) | class SQLAlchemyDataStore(BaseExternalDataStore):
    method __attrs_post_init__ (line 184) | def __attrs_post_init__(self) -> None:
    method __repr__ (line 209) | def __repr__(self) -> str:
    method _retry (line 212) | def _retry(self) -> tenacity.AsyncRetrying:
    method _begin_transaction (line 231) | async def _begin_transaction(
    method _create_metadata (line 250) | async def _create_metadata(self, conn: Connection | AsyncConnection) -...
    method _execute (line 256) | async def _execute(
    method _temporary_failure_exceptions (line 268) | def _temporary_failure_exceptions(self) -> tuple[type[Exception], ...]:
    method _convert_incoming_fire_times (line 275) | def _convert_incoming_fire_times(self, data: dict[str, Any]) -> dict[s...
    method _convert_outgoing_fire_times (line 286) | def _convert_outgoing_fire_times(self, data: dict[str, Any]) -> dict[s...
    method get_table_definitions (line 300) | def get_table_definitions(self) -> MetaData:
    method start (line 396) | async def start(
    method _deserialize_schedules (line 457) | async def _deserialize_schedules(self, result: Result) -> list[Schedule]:
    method _deserialize_jobs (line 474) | async def _deserialize_jobs(self, result: Result) -> list[Job]:
    method add_task (line 486) | async def add_task(self, task: Task) -> None:
    method remove_task (line 521) | async def remove_task(self, task_id: str) -> None:
    method get_task (line 532) | async def get_task(self, task_id: str) -> Task:
    method get_tasks (line 545) | async def get_tasks(self) -> list[Task]:
    method add_schedule (line 557) | async def add_schedule(
    method remove_schedules (line 597) | async def remove_schedules(self, ids: Iterable[str]) -> None:
    method get_schedules (line 635) | async def get_schedules(self, ids: set[str] | None = None) -> list[Sch...
    method acquire_schedules (line 647) | async def acquire_schedules(
    method release_schedules (line 701) | async def release_schedules(
    method get_next_schedule_run_time (line 814) | async def get_next_schedule_run_time(self) -> datetime | None:
    method add_job (line 843) | async def add_job(self, job: Job) -> None:
    method get_jobs (line 858) | async def get_jobs(self, ids: Iterable[UUID] | None = None) -> list[Job]:
    method acquire_jobs (line 871) | async def acquire_jobs(
    method _release_job (line 1036) | async def _release_job(
    method release_job (line 1071) | async def release_job(self, scheduler_id: str, job: Job, result: JobRe...
    method get_job_result (line 1087) | async def get_job_result(self, job_id: UUID) -> JobResult | None:
    method extend_acquired_schedule_leases (line 1104) | async def extend_acquired_schedule_leases(
    method extend_acquired_job_leases (line 1121) | async def extend_acquired_job_leases(
    method reap_abandoned_jobs (line 1138) | async def reap_abandoned_jobs(self, scheduler_id: str) -> None:
    method cleanup (line 1168) | async def cleanup(self) -> None:

FILE: src/apscheduler/eventbrokers/asyncpg.py
  class AsyncpgEventBroker (line 30) | class AsyncpgEventBroker(BaseExternalEventBroker):
    method from_async_sqla_engine (line 55) | def from_async_sqla_engine(
    method __repr__ (line 84) | def __repr__(self) -> str:
    method _temporary_failure_exceptions (line 88) | def _temporary_failure_exceptions(self) -> tuple[type[Exception], ...]:
    method _connect (line 92) | async def _connect(self) -> AsyncGenerator[asyncpg.Connection, None]:
    method start (line 102) | async def start(self, exit_stack: AsyncExitStack, logger: Logger) -> N...
    method _listen_notifications (line 108) | async def _listen_notifications(
    method publish (line 158) | async def publish(self, event: Event) -> None:

FILE: src/apscheduler/eventbrokers/base.py
  class LocalSubscription (line 23) | class LocalSubscription(Subscription):
    method unsubscribe (line 31) | def unsubscribe(self) -> None:
  class BaseEventBroker (line 36) | class BaseEventBroker(EventBroker):
    method start (line 44) | async def start(self, exit_stack: AsyncExitStack, logger: Logger) -> N...
    method subscribe (line 49) | def subscribe(
    method unsubscribe (line 65) | def unsubscribe(self, token: object) -> None:
    method publish_local (line 68) | async def publish_local(self, event: Event) -> None:
    method _deliver_event (line 83) | async def _deliver_event(
  class BaseExternalEventBroker (line 102) | class BaseExternalEventBroker(BaseEventBroker, RetryMixin):
    method generate_notification (line 111) | def generate_notification(self, event: Event) -> bytes:
    method generate_notification_str (line 115) | def generate_notification_str(self, event: Event) -> str:
    method _reconstitute_event (line 119) | def _reconstitute_event(self, event_type: str, serialized: bytes) -> E...
    method reconstitute_event (line 146) | def reconstitute_event(self, payload: bytes) -> Event | None:
    method reconstitute_event_str (line 158) | def reconstitute_event_str(self, payload: str) -> Event | None:

FILE: src/apscheduler/eventbrokers/local.py
  class LocalEventBroker (line 11) | class LocalEventBroker(BaseEventBroker):
    method __repr__ (line 21) | def __repr__(self) -> str:
    method publish (line 24) | async def publish(self, event: Event) -> None:

FILE: src/apscheduler/eventbrokers/mqtt.py
  class MQTTEventBroker (line 25) | class MQTTEventBroker(BaseExternalEventBroker):
    method __attrs_post_init__ (line 67) | def __attrs_post_init__(self) -> None:
    method __repr__ (line 84) | def __repr__(self) -> str:
    method start (line 87) | async def start(self, exit_stack: AsyncExitStack, logger: Logger) -> N...
    method _on_connect (line 106) | def _on_connect(self, client: Client, *_: Any) -> None:
    method _on_connect_fail (line 114) | def _on_connect_fail(self, *_: Any) -> None:
    method _on_disconnect (line 118) | def _on_disconnect(self, *args: Any) -> None:
    method _on_subscribe (line 124) | def _on_subscribe(self, *_: Any) -> None:
    method _on_message (line 128) | def _on_message(self, _: Any, __: Any, msg: MQTTMessage) -> None:
    method publish (line 133) | async def publish(self, event: Event) -> None:

FILE: src/apscheduler/eventbrokers/psycopg.py
  function convert_options (line 29) | def convert_options(value: Mapping[str, Any]) -> dict[str, Any]:
  class PsycopgEventBroker (line 34) | class PsycopgEventBroker(BaseExternalEventBroker):
    method from_async_sqla_engine (line 64) | def from_async_sqla_engine(
    method __repr__ (line 95) | def __repr__(self) -> str:
    method _temporary_failure_exceptions (line 99) | def _temporary_failure_exceptions(self) -> tuple[type[Exception], ...]:
    method _connect (line 103) | async def _connect(self) -> AsyncGenerator[AsyncConnection, None]:
    method start (line 113) | async def start(self, exit_stack: AsyncExitStack, logger: Logger) -> N...
    method _listen_notifications (line 125) | async def _listen_notifications(self, *, task_status: TaskStatus[None]...
    method _publish_notifications (line 143) | async def _publish_notifications(
    method publish (line 170) | async def publish(self, event: Event) -> None:

FILE: src/apscheduler/eventbrokers/redis.py
  class RedisEventBroker (line 23) | class RedisEventBroker(BaseExternalEventBroker):
    method __attrs_post_init__ (line 51) | def __attrs_post_init__(self) -> None:
    method __repr__ (line 59) | def __repr__(self) -> str:
    method _retry (line 62) | def _retry(self) -> tenacity.AsyncRetrying:
    method _close_client (line 80) | async def _close_client(self) -> None:
    method start (line 84) | async def start(self, exit_stack: AsyncExitStack, logger: Logger) -> N...
    method _listen_messages (line 100) | async def _listen_messages(self, pubsub: PubSub) -> None:
    method publish (line 124) | async def publish(self, event: Event) -> None:

FILE: src/apscheduler/executors/async_.py
  class AsyncJobExecutor (line 11) | class AsyncJobExecutor(JobExecutor):
    method run_job (line 19) | async def run_job(self, func: Callable[..., Any], job: Job) -> Any:

FILE: src/apscheduler/executors/qt.py
  class _SchedulerSignals (line 31) | class _SchedulerSignals(QObject):
  class QtJobExecutor (line 36) | class QtJobExecutor(JobExecutor):
    method __attrs_post_init__ (line 40) | def __attrs_post_init__(self):
    method start (line 43) | async def start(self, exit_stack: AsyncExitStack) -> None:
    method run_job (line 46) | async def run_job(self, func: Callable[..., T_Retval], job: Job) -> Any:
    method run_in_qt_thread (line 53) | def run_in_qt_thread(

FILE: src/apscheduler/executors/subprocess.py
  class ProcessPoolJobExecutor (line 16) | class ProcessPoolJobExecutor(JobExecutor):
    method start (line 26) | async def start(self, exit_stack: AsyncExitStack) -> None:
    method run_job (line 29) | async def run_job(self, func: Callable[..., Any], job: Job) -> Any:

FILE: src/apscheduler/executors/thread.py
  class ThreadPoolJobExecutor (line 16) | class ThreadPoolJobExecutor(JobExecutor):
    method start (line 26) | async def start(self, exit_stack: AsyncExitStack) -> None:
    method run_job (line 29) | async def run_job(self, func: Callable[..., Any], job: Job) -> Any:

FILE: src/apscheduler/serializers/cbor.py
  class CBORSerializer (line 16) | class CBORSerializer(Serializer):
    method __attrs_post_init__ (line 32) | def __attrs_post_init__(self) -> None:
    method _default_hook (line 36) | def _default_hook(self, encoder: CBOREncoder, value: object) -> None:
    method _tag_hook (line 53) | def _tag_hook(
    method serialize (line 60) | def serialize(self, obj: object) -> bytes:
    method deserialize (line 66) | def deserialize(self, serialized: bytes):

FILE: src/apscheduler/serializers/json.py
  class JSONSerializer (line 21) | class JSONSerializer(Serializer):
    method __attrs_post_init__ (line 39) | def __attrs_post_init__(self):
    method _default_hook (line 43) | def _default_hook(self, obj):
    method _object_hook (line 62) | def _object_hook(self, obj_state: dict[str, Any]):
    method serialize (line 69) | def serialize(self, obj: object) -> bytes:
    method deserialize (line 75) | def deserialize(self, serialized: bytes):

FILE: src/apscheduler/serializers/pickle.py
  class PickleSerializer (line 12) | class PickleSerializer(Serializer):
    method serialize (line 27) | def serialize(self, obj: object) -> bytes:
    method deserialize (line 33) | def deserialize(self, serialized: bytes):

FILE: src/apscheduler/triggers/calendarinterval.py
  class CalendarIntervalTrigger (line 15) | class CalendarIntervalTrigger(Trigger):
    method __attrs_post_init__ (line 84) | def __attrs_post_init__(self) -> None:
    method next (line 93) | def next(self) -> datetime | None:
    method __getstate__ (line 127) | def __getstate__(self) -> dict[str, Any]:
    method __setstate__ (line 138) | def __setstate__(self, state: dict[str, Any]) -> None:
    method __repr__ (line 147) | def __repr__(self) -> str:

FILE: src/apscheduler/triggers/combining.py
  class BaseCombiningTrigger (line 17) | class BaseCombiningTrigger(Trigger):
    method __getstate__ (line 23) | def __getstate__(self) -> dict[str, Any]:
    method __setstate__ (line 31) | def __setstate__(self, state: dict[str, Any]) -> None:
  class AndTrigger (line 39) | class AndTrigger(BaseCombiningTrigger):
    method next (line 63) | def next(self) -> datetime | None:
    method __getstate__ (line 94) | def __getstate__(self) -> dict[str, Any]:
    method __setstate__ (line 100) | def __setstate__(self, state: dict[str, Any]) -> None:
    method __repr__ (line 106) | def __repr__(self) -> str:
  class OrTrigger (line 115) | class OrTrigger(BaseCombiningTrigger):
    method next (line 126) | def next(self) -> datetime | None:
    method __setstate__ (line 145) | def __setstate__(self, state: dict[str, Any]) -> None:
    method __repr__ (line 149) | def __repr__(self) -> str:

FILE: src/apscheduler/triggers/cron/__init__.py
  class CronTrigger (line 25) | class CronTrigger(Trigger):
    method __attrs_post_init__ (line 85) | def __attrs_post_init__(self) -> None:
    method _set_fields (line 101) | def _set_fields(self, values: Sequence[int | str | None]) -> None:
    method from_crontab (line 117) | def from_crontab(
    method _to_trigger_timezone (line 154) | def _to_trigger_timezone(self, dt: datetime | None, name: str) -> date...
    method _increment_field_value (line 168) | def _increment_field_value(
    method _set_field_value (line 213) | def _set_field_value(
    method next (line 228) | def next(self) -> datetime | None:
    method __getstate__ (line 276) | def __getstate__(self) -> dict[str, Any]:
    method __setstate__ (line 286) | def __setstate__(self, state: dict[str, Any]) -> None:
    method __repr__ (line 294) | def __repr__(self) -> str:

FILE: src/apscheduler/triggers/cron/expressions.py
  function get_weekday_index (line 37) | def get_weekday_index(weekday: str) -> int:
  class AllExpression (line 45) | class AllExpression:
    method validate_range (line 54) | def validate_range(self, field_name: str, min_value: int, max_value: i...
    method get_next_value (line 62) | def get_next_value(self, dateval: datetime, field: BaseField) -> int |...
    method __str__ (line 76) | def __str__(self) -> str:
  class RangeExpression (line 81) | class RangeExpression(AllExpression):
    method __attrs_post_init__ (line 95) | def __attrs_post_init__(self) -> None:
    method validate_range (line 104) | def validate_range(self, field_name: str, min_value: int, max_value: i...
    method get_next_value (line 123) | def get_next_value(self, dateval: datetime, field: BaseField) -> int |...
    method __str__ (line 140) | def __str__(self) -> str:
  class MonthRangeExpression (line 152) | class MonthRangeExpression(RangeExpression):
    method __init__ (line 157) | def __init__(self, first: str, last: str | None = None):
    method __str__ (line 173) | def __str__(self) -> str:
  class WeekdayRangeExpression (line 181) | class WeekdayRangeExpression(RangeExpression):
    method __init__ (line 186) | def __init__(self, first: str, last: str | None = None):
    method __str__ (line 191) | def __str__(self) -> str:
  class WeekdayPositionExpression (line 199) | class WeekdayPositionExpression(AllExpression):
    method __init__ (line 209) | def __init__(self, *, option_name: str, weekday_name: str):
    method get_next_value (line 218) | def get_next_value(self, dateval: datetime, field: BaseField) -> int |...
    method __str__ (line 239) | def __str__(self) -> str:
  class LastDayOfMonthExpression (line 243) | class LastDayOfMonthExpression(AllExpression):
    method __init__ (line 246) | def __init__(self) -> None:
    method get_next_value (line 249) | def get_next_value(self, dateval: datetime, field: BaseField) -> int |...
    method __str__ (line 252) | def __str__(self) -> str:

FILE: src/apscheduler/triggers/cron/fields.py
  class BaseField (line 57) | class BaseField:
    method __init_subclass__ (line 63) | def __init_subclass__(cls, real: bool = True, extra_compilers: Sequenc...
    method __init__ (line 68) | def __init__(self, name: str, exprs: int | str):
    method get_min (line 74) | def get_min(self, dateval: datetime) -> int:
    method get_max (line 77) | def get_max(self, dateval: datetime) -> int:
    method get_value (line 80) | def get_value(self, dateval: datetime) -> int:
    method get_next_value (line 83) | def get_next_value(self, dateval: datetime) -> int | None:
    method append_expression (line 92) | def append_expression(self, expr: str) -> None:
    method __str__ (line 112) | def __str__(self) -> str:
  class WeekField (line 117) | class WeekField(BaseField, real=False):
    method get_value (line 120) | def get_value(self, dateval: datetime) -> int:
  class DayOfMonthField (line 124) | class DayOfMonthField(
    method get_max (line 129) | def get_max(self, dateval: datetime) -> int:
  class DayOfWeekField (line 133) | class DayOfWeekField(BaseField, real=False, extra_compilers=(WeekdayRang...
    method append_expression (line 136) | def append_expression(self, expr: str) -> None:
    method get_value (line 165) | def get_value(self, dateval: datetime) -> int:
  class MonthField (line 169) | class MonthField(BaseField, extra_compilers=(MonthRangeExpression,)):

FILE: src/apscheduler/triggers/date.py
  class DateTrigger (line 15) | class DateTrigger(Trigger):
    method next (line 27) | def next(self) -> datetime | None:
    method __getstate__ (line 34) | def __getstate__(self) -> dict[str, Any]:
    method __setstate__ (line 41) | def __setstate__(self, state: dict[str, Any]) -> None:
    method __repr__ (line 46) | def __repr__(self) -> str:

FILE: src/apscheduler/triggers/interval.py
  class IntervalTrigger (line 15) | class IntervalTrigger(Trigger):
    method __attrs_post_init__ (line 57) | def __attrs_post_init__(self) -> None:
    method next (line 73) | def next(self) -> datetime | None:
    method __getstate__ (line 84) | def __getstate__(self) -> dict[str, Any]:
    method __setstate__ (line 100) | def __setstate__(self, state: dict[str, Any]) -> None:
    method __repr__ (line 122) | def __repr__(self) -> str:

FILE: tests/conftest.py
  function timezone (line 23) | def timezone() -> ZoneInfo:
  function utc_timezone (line 28) | def utc_timezone() -> ZoneInfo:
  function serializer (line 39) | def serializer(request) -> Serializer | None:
  function anyio_backend (line 44) | def anyio_backend() -> str:
  function local_broker (line 49) | def local_broker() -> EventBroker:
  function redis_broker (line 56) | async def redis_broker(serializer: Serializer) -> EventBroker:
  function mqtt_broker (line 67) | def mqtt_broker(serializer: Serializer) -> EventBroker:
  function asyncpg_broker (line 74) | async def asyncpg_broker(serializer: Serializer) -> EventBroker:
  function psycopg_broker (line 85) | async def psycopg_broker(serializer: Serializer) -> EventBroker:
  function raw_event_broker (line 118) | async def raw_event_broker(request: SubRequest) -> EventBroker:
  function event_broker (line 123) | async def event_broker(
  function memory_store (line 132) | def memory_store() -> Generator[DataStore, None, None]:
  function mongodb_store (line 137) | async def mongodb_store() -> AsyncGenerator[DataStore, Any]:
  function psycopg_async_store (line 147) | async def psycopg_async_store() -> AsyncGenerator[DataStore, None]:
  function psycopg_sync_store (line 169) | def psycopg_sync_store() -> Generator[DataStore, None, None]:
  function pymysql_store (line 188) | def pymysql_store() -> Generator[DataStore, None, None]:
  function aiosqlite_store (line 202) | async def aiosqlite_store(tmp_path: Path) -> AsyncGenerator[DataStore, N...
  function asyncpg_store (line 215) | async def asyncpg_store() -> AsyncGenerator[DataStore, None]:
  function asyncmy_store (line 250) | async def asyncmy_store() -> AsyncGenerator[DataStore, None]:
  function raw_datastore (line 308) | async def raw_datastore(request: SubRequest) -> DataStore:
  function logger (line 313) | def logger() -> Logger:

FILE: tests/test_datastores.py
  function datastore (line 49) | async def datastore(
  function schedules (line 59) | def schedules() -> list[Schedule]:
  function capture_events (line 82) | async def capture_events(
  function test_add_replace_task (line 102) | async def test_add_replace_task(datastore: DataStore) -> None:
  function test_add_schedules (line 137) | async def test_add_schedules(datastore: DataStore, schedules: list[Sched...
  function test_replace_schedules (line 155) | async def test_replace_schedules(
  function test_remove_schedules (line 193) | async def test_remove_schedules(
  function test_acquire_release_schedules (line 218) | async def test_acquire_release_schedules(
  function test_release_schedule_two_identical_fire_times (line 307) | async def test_release_schedule_two_identical_fire_times(datastore: Data...
  function test_release_two_schedules_at_once (line 342) | async def test_release_two_schedules_at_once(datastore: DataStore) -> None:
  function test_acquire_schedules_lock_timeout (line 372) | async def test_acquire_schedules_lock_timeout(
  function test_acquire_multiple_workers (line 415) | async def test_acquire_multiple_workers(datastore: DataStore) -> None:
  function test_job_release_success (line 438) | async def test_job_release_success(datastore: DataStore) -> None:
  function test_job_release_failure (line 471) | async def test_job_release_failure(datastore: DataStore) -> None:
  function test_job_release_missed_deadline (line 505) | async def test_job_release_missed_deadline(datastore: DataStore):
  function test_job_release_cancelled (line 537) | async def test_job_release_cancelled(datastore: DataStore) -> None:
  function test_acquire_jobs_lock_timeout (line 570) | async def test_acquire_jobs_lock_timeout(
  function test_acquire_jobs_max_number_exceeded (line 604) | async def test_acquire_jobs_max_number_exceeded(datastore: DataStore) ->...
  function test_add_get_task (line 655) | async def test_add_get_task(datastore: DataStore) -> None:
  function test_cancel_start (line 667) | async def test_cancel_start(
  function test_cancel_stop (line 676) | async def test_cancel_stop(
  function test_next_schedule_run_time (line 685) | async def test_next_schedule_run_time(datastore: DataStore, schedules: l...
  function test_extend_acquired_schedule_leases (line 700) | async def test_extend_acquired_schedule_leases(
  function test_extend_acquired_job_leases (line 759) | async def test_extend_acquired_job_leases(
  function test_acquire_jobs_deserialization_failure (line 812) | async def test_acquire_jobs_deserialization_failure(
  function test_reap_abandoned_jobs (line 837) | async def test_reap_abandoned_jobs(
  class TestRepr (line 863) | class TestRepr:
    method test_memory (line 864) | async def test_memory(self, memory_store: MemoryDataStore) -> None:
    method test_sqlite (line 867) | async def test_sqlite(self, tmp_path: Path) -> None:
    method test_psycopg (line 877) | async def test_psycopg(self) -> None:
    method test_mongodb (line 890) | async def test_mongodb(self) -> None:

FILE: tests/test_eventbrokers.py
  function test_publish_subscribe (line 23) | async def test_publish_subscribe(event_broker: EventBroker) -> None:
  function test_subscribe_one_shot (line 47) | async def test_subscribe_one_shot(event_broker: EventBroker) -> None:
  function test_unsubscribe (line 75) | async def test_unsubscribe(event_broker: EventBroker) -> None:
  function test_publish_no_subscribers (line 89) | async def test_publish_no_subscribers(
  function test_publish_exception (line 96) | async def test_publish_exception(
  function test_cancel_start (line 114) | async def test_cancel_start(raw_event_broker: EventBroker, logger: Logge...
  function test_cancel_stop (line 121) | async def test_cancel_stop(raw_event_broker: EventBroker, logger: Logger...
  function test_asyncpg_broker_from_async_engine (line 128) | def test_asyncpg_broker_from_async_engine() -> None:
  function test_psycopg_broker_from_async_engine (line 152) | def test_psycopg_broker_from_async_engine() -> None:

FILE: tests/test_marshalling.py
  class DummyClass (line 14) | class DummyClass:
    method meth (line 15) | def meth(self):
    method staticmeth (line 19) | def staticmeth():
    method classmeth (line 23) | def classmeth(cls):
    method __call__ (line 26) | def __call__(self):
    class InnerDummyClass (line 29) | class InnerDummyClass:
      method innerclassmeth (line 31) | def innerclassmeth(cls):
  class InheritedDummyClass (line 35) | class InheritedDummyClass(DummyClass):
  class TestCallableToRef (line 39) | class TestCallableToRef:
    method test_errors (line 48) | def test_errors(self, obj, error):
    method test_nested_function_error (line 52) | def test_nested_function_error(self):
    method test_valid_refs (line 84) | def test_valid_refs(self, input, expected):
  class TestCallableFromRef (line 88) | class TestCallableFromRef:
    method test_valid_ref (line 89) | def test_valid_ref(self):
    method test_complex_path (line 97) | def test_complex_path(self):
    method test_lookup_error (line 111) | def test_lookup_error(self, input, error):

FILE: tests/test_schedulers.py
  function dummy_async_job (line 79) | async def dummy_async_job(delay: float = 0, fail: bool = False) -> str:
  function dummy_sync_job (line 87) | def dummy_sync_job(delay: float = 0, fail: bool = False) -> str:
  function decorated_job (line 100) | def decorated_job() -> None:
  class DummyClass (line 104) | class DummyClass:
    method __init__ (line 105) | def __init__(self, value: int):
    method dummy_static_method (line 109) | async def dummy_static_method() -> str:
    method dummy_async_static_method (line 113) | async def dummy_async_static_method() -> str:
    method dummy_class_method (line 117) | def dummy_class_method(cls) -> str:
    method dummy_async_class_method (line 121) | async def dummy_async_class_method(cls) -> str:
    method dummy_instance_method (line 124) | def dummy_instance_method(self) -> int:
    method dummy_async_instance_method (line 127) | async def dummy_async_instance_method(self) -> int:
    method __call__ (line 130) | def __call__(self) -> int:
  class TestAsyncScheduler (line 134) | class TestAsyncScheduler:
    method test_repr (line 135) | def test_repr(self) -> None:
    method test_use_before_initialized (line 142) | async def test_use_before_initialized(self) -> None:
    method test_properties (line 149) | async def test_properties(self) -> None:
    method test_async_executor (line 167) | async def test_async_executor(self, as_default: bool) -> None:
    method test_threadpool_executor (line 179) | async def test_threadpool_executor(self) -> None:
    method test_processpool_executor (line 187) | async def test_processpool_executor(self) -> None:
    method test_configure_task (line 193) | async def test_configure_task(self, raw_datastore: DataStore) -> None:
    method test_configure_task_with_decorator (line 215) | async def test_configure_task_with_decorator(self) -> None:
    method test_configure_local_task_with_decorator (line 224) | async def test_configure_local_task_with_decorator(self) -> None:
    method test_add_pause_unpause_remove_schedule (line 246) | async def test_add_pause_unpause_remove_schedule(
    method test_add_job_wait_result (line 307) | async def test_add_job_wait_result(self, raw_datastore: DataStore) -> ...
    method test_run_job (line 367) | async def test_run_job(self, raw_datastore: DataStore, success: bool) ...
    method test_callable_types (line 458) | async def test_callable_types(
    method test_scheduled_job_missed_deadline (line 492) | async def test_scheduled_job_missed_deadline(
    method test_coalesce_policy (line 559) | async def test_coalesce_policy(
    method test_jitter (line 619) | async def test_jitter(
    method test_add_job_get_result_success (line 678) | async def test_add_job_get_result_success(self, raw_datastore: DataSto...
    method test_add_job_get_result_empty (line 692) | async def test_add_job_get_result_empty(self, raw_datastore: DataStore...
    method test_add_job_get_result_error (line 728) | async def test_add_job_get_result_error(self) -> None:
    method test_add_job_get_result_no_ready_yet (line 745) | async def test_add_job_get_result_no_ready_yet(self) -> None:
    method test_add_job_not_rewriting_task_config (line 765) | async def test_add_job_not_rewriting_task_config(
    method test_contextvars (line 792) | async def test_contextvars(self, mocker: MockerFixture, timezone: Zone...
    method test_explicit_cleanup (line 830) | async def test_explicit_cleanup(self, raw_datastore: DataStore) -> None:
    method test_explicit_cleanup_avoid_schedules_still_having_jobs (line 882) | async def test_explicit_cleanup_avoid_schedules_still_having_jobs(
    method test_implicit_cleanup (line 929) | async def test_implicit_cleanup(self, mocker: MockerFixture) -> None:
    method test_wait_until_stopped (line 942) | async def test_wait_until_stopped(self) -> None:
    method test_max_concurrent_jobs (line 950) | async def test_max_concurrent_jobs(self) -> None:
    method test_pause_unpause_schedule (line 979) | async def test_pause_unpause_schedule(
    method test_schedule_job_result_expiration_time (line 1029) | async def test_schedule_job_result_expiration_time(
    method test_scheduler_crash_restart_schedule_immediately (line 1058) | async def test_scheduler_crash_restart_schedule_immediately(
    method test_scheduler_crash_reap_abandoned_jobs (line 1107) | async def test_scheduler_crash_reap_abandoned_jobs(
    method test_stop_scheduler_while_job_running (line 1166) | async def test_stop_scheduler_while_job_running(
  class TestSyncScheduler (line 1207) | class TestSyncScheduler:
    method test_interface_parity (line 1208) | def test_interface_parity(self) -> None:
    method test_repr (line 1241) | def test_repr(self) -> None:
    method test_configure (line 1248) | def test_configure(self) -> None:
    method test_threadpool_executor (line 1267) | def test_threadpool_executor(self, as_default: bool) -> None:
    method test_processpool_executor (line 1279) | def test_processpool_executor(self) -> None:
    method test_properties (line 1285) | def test_properties(self) -> None:
    method test_use_without_contextmanager (line 1302) | def test_use_without_contextmanager(self, mocker: MockFixture) -> None:
    method test_configure_task (line 1309) | def test_configure_task(self) -> None:
    method test_add_remove_schedule (line 1329) | def test_add_remove_schedule(self, timezone: ZoneInfo) -> None:
    method test_add_job_wait_result (line 1363) | def test_add_job_wait_result(self) -> None:
    method test_wait_until_stopped (line 1410) | def test_wait_until_stopped(self) -> None:
    method test_explicit_cleanup (line 1431) | def test_explicit_cleanup(self) -> None:
    method test_run_until_stopped (line 1449) | def test_run_until_stopped(self) -> None:
    method test_uwsgi_threads_error (line 1469) | def test_uwsgi_threads_error(self, monkeypatch: MonkeyPatch) -> None:

FILE: tests/test_serializers.py
  function test_serialize_event (line 45) | def test_serialize_event(event: Event, serializer: Serializer) -> None:
  function test_serialization_error (line 51) | def test_serialization_error(serializer: Serializer) -> None:
  function test_deserialization_error (line 70) | def test_deserialization_error(payload: bytes, serializer: Serializer) -...

FILE: tests/triggers/test_calendarinterval.py
  function test_bad_interval (line 10) | def test_bad_interval(timezone):
  function test_bad_start_end_dates (line 15) | def test_bad_start_end_dates(timezone):
  function test_end_date (line 27) | def test_end_date(timezone, serializer):
  function test_missing_time (line 40) | def test_missing_time(timezone, serializer):
  function test_repeated_time (line 55) | def test_repeated_time(timezone, serializer):
  function test_nonexistent_days (line 70) | def test_nonexistent_days(timezone, serializer):
  function test_repr (line 83) | def test_repr(timezone, serializer):
  function test_utc_timezone (line 105) | def test_utc_timezone(utc_timezone):

FILE: tests/triggers/test_combining.py
  class TestAndTrigger (line 15) | class TestAndTrigger:
    method test_two_datetriggers (line 17) | def test_two_datetriggers(self, timezone, serializer, threshold):
    method test_max_iterations (line 32) | def test_max_iterations(self, timezone, serializer):
    method test_repr (line 47) | def test_repr(self, timezone, serializer):
    method test_overlapping_triggers (line 153) | def test_overlapping_triggers(
  class TestOrTrigger (line 165) | class TestOrTrigger:
    method test_two_datetriggers (line 166) | def test_two_datetriggers(self, timezone, serializer):
    method test_two_interval_triggers (line 179) | def test_two_interval_triggers(self, timezone, serializer):
    method test_repr (line 203) | def test_repr(self, timezone):

FILE: tests/triggers/test_cron.py
  function test_invalid_expression (line 11) | def test_invalid_expression():
  function test_invalid_step (line 16) | def test_invalid_step():
  function test_invalid_range (line 21) | def test_invalid_range():
  function test_invalid_month_name (line 27) | def test_invalid_month_name(expr):
  function test_invalid_weekday_name (line 33) | def test_invalid_weekday_name(expr):
  function test_invalid_weekday_position_name (line 38) | def test_invalid_weekday_position_name():
  function test_invalid_ranges (line 74) | def test_invalid_ranges(values, expected):
  function test_cron_trigger_1 (line 78) | def test_cron_trigger_1(timezone, serializer):
  function test_cron_trigger_2 (line 106) | def test_cron_trigger_2(timezone, serializer):
  function test_cron_trigger_3 (line 125) | def test_cron_trigger_3(timezone, serializer):
  function test_cron_trigger_4 (line 147) | def test_cron_trigger_4(timezone, serializer):
  function test_weekday_overlap (line 164) | def test_weekday_overlap(timezone, serializer, expr):
  function test_weekday_range (line 185) | def test_weekday_range(timezone, serializer):
  function test_last_weekday (line 209) | def test_last_weekday(timezone, serializer):
  function test_increment_weekday (line 227) | def test_increment_weekday(timezone, serializer):
  function test_month_rollover (line 246) | def test_month_rollover(timezone, serializer):
  function test_weekday_nomatch (line 257) | def test_weekday_nomatch(timezone, serializer, weekday):
  function test_weekday_positional (line 278) | def test_weekday_positional(timezone, serializer):
  function test_end_time (line 294) | def test_end_time(timezone, serializer):
  function test_week_1 (line 314) | def test_week_1(timezone, serializer):
  function test_week_2 (line 334) | def test_week_2(timezone, serializer, weekday):
  function test_dst_change (line 385) | def test_dst_change(
  function test_dst_change2 (line 434) | def test_dst_change2(
  function test_zero_value (line 449) | def test_zero_value(timezone):
  function test_year_list (line 461) | def test_year_list(timezone, serializer):
  function test_from_crontab (line 529) | def test_from_crontab(expr, expected_repr, timezone, serializer):
  function test_from_crontab_wrong_number_of_fields (line 538) | def test_from_crontab_wrong_number_of_fields():
  function test_from_crontab_start_end_time (line 543) | def test_from_crontab_start_end_time(timezone: ZoneInfo) -> None:
  function test_aware_start_time_timezone_conversion (line 553) | def test_aware_start_time_timezone_conversion() -> None:
  function test_aware_end_time_timezone_conversion (line 563) | def test_aware_end_time_timezone_conversion() -> None:
  function test_non_existing_naive_start_time (line 575) | def test_non_existing_naive_start_time() -> None:
  function test_non_existing_naive_end_time (line 582) | def test_non_existing_naive_end_time() -> None:

FILE: tests/triggers/test_date.py
  function test_run_time (line 8) | def test_run_time(timezone, serializer):

FILE: tests/triggers/test_interval.py
  function test_bad_interval (line 10) | def test_bad_interval():
  function test_bad_end_time (line 15) | def test_bad_end_time(timezone):
  function test_end_time (line 24) | def test_end_time(timezone, serializer):
  function test_repr (line 41) | def test_repr(timezone, serializer):
Condensed preview — 99 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (631K chars).
[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yaml",
    "chars": 1862,
    "preview": "name: Bug Report\ndescription: File a bug report\nlabels: [\"bug\"]\nbody:\n  - type: markdown\n    attributes:\n      value: >\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 483,
    "preview": "blank_issues_enabled: false\ncontact_links:\n  - name: Stack Overflow\n    url: https://stackoverflow.com/questions/tagged/"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/features_request.yaml",
    "chars": 1052,
    "preview": "name: Feature request\ndescription: Suggest a new feature\nlabels: [\"enhancement\"]\nbody:\n  - type: markdown\n    attributes"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 580,
    "preview": "# Keep GitHub Actions up to date with GitHub's Dependabot...\n# https://docs.github.com/en/code-security/dependabot/worki"
  },
  {
    "path": ".github/pull_request_template.md",
    "chars": 1135,
    "preview": "<!-- Thank you for your contribution! -->\n## Changes\n\nFixes #. <!-- Provide issue number if exists -->\n\n<!-- Please give"
  },
  {
    "path": ".github/workflows/publish.yml",
    "chars": 1469,
    "preview": "name: Publish packages to PyPI\n\non:\n  push:\n    tags:\n      - \"[0-9]+.[0-9]+.[0-9]+\"\n      - \"[0-9]+.[0-9]+.[0-9]+.post["
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 2777,
    "preview": "name: test suite\n\non:\n  push:\n    branches: [master]\n  pull_request:\n\njobs:\n  test-linux:\n    strategy:\n      fail-fast:"
  },
  {
    "path": ".gitignore",
    "chars": 166,
    "preview": ".project\n.pydevproject\n.idea/\n.coverage\n.cache/\n.mypy_cache\n.pytest_cache/\n.tox/\n.eggs/\n*.egg-info/\n*.pyc\ndist/\ndocs/_bu"
  },
  {
    "path": ".mailmap",
    "chars": 140,
    "preview": "Alex Grönholm <alex.gronholm@nextday.fi> agronholm <devnull@localhost>\nAlex Grönholm <alex.gronholm@nextday.fi> demigod "
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 1458,
    "preview": "# This is the configuration file for pre-commit (https://pre-commit.com/).\n# To use:\n# * Install pre-commit (https://pre"
  },
  {
    "path": ".readthedocs.yml",
    "chars": 312,
    "preview": "version: 2\n\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.12\"\n  jobs:\n    install:\n      - python -m pip install --n"
  },
  {
    "path": "LICENSE.txt",
    "chars": 1080,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2009 Alex Grönholm\n\nPermission is hereby granted, free of charge, to any person obt"
  },
  {
    "path": "README.rst",
    "chars": 3804,
    "preview": ".. image:: https://github.com/agronholm/apscheduler/actions/workflows/test.yml/badge.svg\n  :target: https://github.com/a"
  },
  {
    "path": "docker-compose.yml",
    "chars": 509,
    "preview": "services:\n  postgresql:\n    image: postgres\n    ports:\n      - 127.0.0.1:5432:5432\n    environment:\n      POSTGRES_DB: t"
  },
  {
    "path": "docs/api.rst",
    "chars": 4707,
    "preview": "API reference\n=============\n\nData structures\n---------------\n\n.. autoclass:: apscheduler.Task\n.. autoclass:: apscheduler"
  },
  {
    "path": "docs/conf.py",
    "chars": 2769,
    "preview": "# Configuration file for the Sphinx documentation builder.\n#\n# This file only contains a selection of the most common op"
  },
  {
    "path": "docs/contributing.rst",
    "chars": 4085,
    "preview": "Contributing to APScheduler\n===========================\n\n.. highlight:: bash\n\nIf you wish to contribute a fix or feature"
  },
  {
    "path": "docs/extending.rst",
    "chars": 7229,
    "preview": "#####################\nExtending APScheduler\n#####################\n\n.. py:currentmodule:: apscheduler\n\nThis document is m"
  },
  {
    "path": "docs/faq.rst",
    "chars": 890,
    "preview": "##########################\nFrequently Asked Questions\n##########################\n\nIs there a graphical user interface fo"
  },
  {
    "path": "docs/index.rst",
    "chars": 273,
    "preview": "Advanced Python Scheduler\n=========================\n\n.. include:: ../README.rst\n   :end-before: Documentation\n\n\nTable of"
  },
  {
    "path": "docs/integrations.rst",
    "chars": 3580,
    "preview": "Integrating with application frameworks\n=======================================\n\n.. py:currentmodule:: apscheduler\n\nWSGI"
  },
  {
    "path": "docs/migration.rst",
    "chars": 10063,
    "preview": "###############################################\nMigrating from previous versions of APScheduler\n########################"
  },
  {
    "path": "docs/userguide.rst",
    "chars": 30849,
    "preview": "##########\nUser guide\n##########\n\n.. py:currentmodule:: apscheduler\n\nInstallation\n============\n\nThe preferred installati"
  },
  {
    "path": "docs/versionhistory.rst",
    "chars": 28440,
    "preview": "Version history\n===============\n\nTo find out how to migrate your application from a previous version of\nAPScheduler, see"
  },
  {
    "path": "examples/README.rst",
    "chars": 4814,
    "preview": "APScheduler practical examples\n==============================\n\n.. highlight:: bash\n\nThis directory contains a number of "
  },
  {
    "path": "examples/gui/qt_executor.py",
    "chars": 1269,
    "preview": "from __future__ import annotations\n\nimport sys\nfrom datetime import datetime\n\nfrom apscheduler import Scheduler\nfrom aps"
  },
  {
    "path": "examples/separate_worker/async_scheduler.py",
    "chars": 1553,
    "preview": "\"\"\"\nThis is an example demonstrating the use of the scheduler as only an interface to the\nscheduling system. This script"
  },
  {
    "path": "examples/separate_worker/async_worker.py",
    "chars": 1440,
    "preview": "\"\"\"\nThis is an example demonstrating how to run a scheduler to process schedules added by\nanother scheduler elsewhere. P"
  },
  {
    "path": "examples/separate_worker/example_tasks.py",
    "chars": 227,
    "preview": "\"\"\"\nThis module contains just the code for the scheduled task.\nIt should not be run directly.\n\"\"\"\n\nfrom __future__ impor"
  },
  {
    "path": "examples/separate_worker/sync_scheduler.py",
    "chars": 1392,
    "preview": "\"\"\"\nThis is an example demonstrating the use of the scheduler as only an interface to the\nscheduling system. This script"
  },
  {
    "path": "examples/separate_worker/sync_worker.py",
    "chars": 1321,
    "preview": "\"\"\"\nThis is an example demonstrating how to run a scheduler to process schedules added by\nanother scheduler elsewhere. P"
  },
  {
    "path": "examples/standalone/async_memory.py",
    "chars": 632,
    "preview": "\"\"\"\nExample demonstrating use of the asynchronous scheduler in a simple asyncio app.\n\nTo run: python async_memory.py\n\nIt"
  },
  {
    "path": "examples/standalone/async_mysql.py",
    "chars": 1069,
    "preview": "\"\"\"\nExample demonstrating use of the asynchronous scheduler with persistence via MySQL or\nMariaDB in a simple asyncio ap"
  },
  {
    "path": "examples/standalone/async_postgres.py",
    "chars": 1064,
    "preview": "\"\"\"\nExample demonstrating use of the asynchronous scheduler with persistence via PostgreSQL\nin a simple asyncio app.\n\nRe"
  },
  {
    "path": "examples/standalone/sync_memory.py",
    "chars": 510,
    "preview": "\"\"\"\nExample demonstrating use of the synchronous scheduler.\n\nTo run: python sync_memory.py\n\nIt should print a line on th"
  },
  {
    "path": "examples/web/asgi_fastapi.py",
    "chars": 1594,
    "preview": "\"\"\"\nExample demonstrating use with the FastAPI web framework.\n\nRequires the \"postgresql\" service to be running.\nTo insta"
  },
  {
    "path": "examples/web/asgi_noframework.py",
    "chars": 2445,
    "preview": "\"\"\"\nExample demonstrating use with ASGI (raw ASGI application, no framework).\n\nRequires the \"postgresql\" service to be r"
  },
  {
    "path": "examples/web/asgi_starlette.py",
    "chars": 2195,
    "preview": "\"\"\"\nExample demonstrating use with the Starlette web framework.\n\nRequires the \"postgresql\" service to be running.\nTo ins"
  },
  {
    "path": "examples/web/wsgi_flask.py",
    "chars": 1225,
    "preview": "\"\"\"\nExample demonstrating use with WSGI (raw WSGI application, no framework).\n\nRequires the \"postgresql\" and \"redis\" ser"
  },
  {
    "path": "examples/web/wsgi_noframework.py",
    "chars": 1386,
    "preview": "\"\"\"\nExample demonstrating use with WSGI (raw WSGI application, no framework).\n\nRequires the \"postgresql\" and \"redis\" ser"
  },
  {
    "path": "pyproject.toml",
    "chars": 4108,
    "preview": "[build-system]\nrequires = [\n    \"setuptools >= 77\",\n    \"setuptools_scm >= 6.4\"\n]\nbuild-backend = \"setuptools.build_meta"
  },
  {
    "path": "src/apscheduler/__init__.py",
    "chars": 2905,
    "preview": "from __future__ import annotations\n\nfrom typing import Any\n\nfrom ._context import current_async_scheduler as current_asy"
  },
  {
    "path": "src/apscheduler/_context.py",
    "chars": 604,
    "preview": "from __future__ import annotations\n\nfrom contextvars import ContextVar\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHECKIN"
  },
  {
    "path": "src/apscheduler/_converters.py",
    "chars": 2061,
    "preview": "from __future__ import annotations\n\nfrom collections.abc import Callable\nfrom datetime import date, datetime, timedelta,"
  },
  {
    "path": "src/apscheduler/_decorators.py",
    "chars": 2905,
    "preview": "from __future__ import annotations\n\nfrom collections.abc import Callable\nfrom datetime import timedelta\nfrom typing impo"
  },
  {
    "path": "src/apscheduler/_enums.py",
    "chars": 2729,
    "preview": "from __future__ import annotations\n\nfrom enum import Enum, auto\n\n\nclass SchedulerRole(Enum):\n    \"\"\"\n    Specifies what "
  },
  {
    "path": "src/apscheduler/_events.py",
    "chars": 9689,
    "preview": "from __future__ import annotations\n\nfrom datetime import datetime, timezone\nfrom functools import partial\nfrom traceback"
  },
  {
    "path": "src/apscheduler/_exceptions.py",
    "chars": 2200,
    "preview": "from __future__ import annotations\n\nfrom uuid import UUID\n\n\nclass TaskLookupError(LookupError):\n    \"\"\"Raised by a data "
  },
  {
    "path": "src/apscheduler/_marshalling.py",
    "chars": 3456,
    "preview": "from __future__ import annotations\n\nfrom collections.abc import Callable\nfrom datetime import tzinfo\nfrom functools impo"
  },
  {
    "path": "src/apscheduler/_retry.py",
    "chars": 1991,
    "preview": "from __future__ import annotations\n\nfrom logging import Logger\n\nimport attrs\nfrom attr.validators import instance_of\nfro"
  },
  {
    "path": "src/apscheduler/_schedulers/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/apscheduler/_schedulers/async_.py",
    "chars": 50586,
    "preview": "from __future__ import annotations\n\nimport os\nimport platform\nimport random\nimport sys\nfrom collections.abc import Calla"
  },
  {
    "path": "src/apscheduler/_schedulers/sync.py",
    "chars": 14576,
    "preview": "from __future__ import annotations\n\nimport atexit\nimport sys\nimport threading\nfrom collections.abc import Callable, Iter"
  },
  {
    "path": "src/apscheduler/_structures.py",
    "chars": 17423,
    "preview": "from __future__ import annotations\n\nfrom datetime import datetime, timedelta, timezone\nfrom functools import partial\nfro"
  },
  {
    "path": "src/apscheduler/_utils.py",
    "chars": 3651,
    "preview": "\"\"\"This module contains several handy functions primarily meant for internal use.\"\"\"\n\nfrom __future__ import annotations"
  },
  {
    "path": "src/apscheduler/_validators.py",
    "chars": 1854,
    "preview": "from __future__ import annotations\n\nfrom collections.abc import Callable\nfrom typing import Any\n\nfrom attrs import Attri"
  },
  {
    "path": "src/apscheduler/abc.py",
    "chars": 14038,
    "preview": "from __future__ import annotations\n\nimport sys\nfrom abc import ABCMeta, abstractmethod\nfrom collections.abc import Calla"
  },
  {
    "path": "src/apscheduler/datastores/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/apscheduler/datastores/base.py",
    "chars": 1163,
    "preview": "from __future__ import annotations\n\nfrom contextlib import AsyncExitStack\nfrom logging import Logger\n\nimport attrs\n\nfrom"
  },
  {
    "path": "src/apscheduler/datastores/memory.py",
    "chars": 14288,
    "preview": "from __future__ import annotations\n\nfrom bisect import bisect_left, bisect_right, insort_right\nfrom collections import d"
  },
  {
    "path": "src/apscheduler/datastores/mongodb.py",
    "chars": 38032,
    "preview": "from __future__ import annotations\n\nimport operator\nfrom collections.abc import (\n    Callable,\n    Iterable,\n    Mappin"
  },
  {
    "path": "src/apscheduler/datastores/sqlalchemy.py",
    "chars": 52580,
    "preview": "from __future__ import annotations\n\nfrom collections import defaultdict\nfrom collections.abc import AsyncGenerator, Iter"
  },
  {
    "path": "src/apscheduler/eventbrokers/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/apscheduler/eventbrokers/asyncpg.py",
    "chars": 6381,
    "preview": "from __future__ import annotations\n\nfrom collections.abc import AsyncGenerator, Mapping\nfrom contextlib import AsyncExit"
  },
  {
    "path": "src/apscheduler/eventbrokers/base.py",
    "chars": 5801,
    "preview": "from __future__ import annotations\n\nfrom base64 import b64decode, b64encode\nfrom collections.abc import Callable, Iterab"
  },
  {
    "path": "src/apscheduler/eventbrokers/local.py",
    "chars": 613,
    "preview": "from __future__ import annotations\n\nimport attrs\n\nfrom .._events import Event\nfrom .._utils import create_repr\nfrom .bas"
  },
  {
    "path": "src/apscheduler/eventbrokers/mqtt.py",
    "chars": 5464,
    "preview": "from __future__ import annotations\n\nimport sys\nfrom concurrent.futures import Future\nfrom contextlib import AsyncExitSta"
  },
  {
    "path": "src/apscheduler/eventbrokers/psycopg.py",
    "chars": 6719,
    "preview": "from __future__ import annotations\n\nfrom collections.abc import AsyncGenerator, Mapping\nfrom contextlib import AsyncExit"
  },
  {
    "path": "src/apscheduler/eventbrokers/redis.py",
    "chars": 4806,
    "preview": "from __future__ import annotations\n\nfrom asyncio import CancelledError\nfrom contextlib import AsyncExitStack\nfrom loggin"
  },
  {
    "path": "src/apscheduler/executors/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/apscheduler/executors/async_.py",
    "chars": 670,
    "preview": "from __future__ import annotations\n\nfrom collections.abc import Callable\nfrom inspect import isawaitable\nfrom typing imp"
  },
  {
    "path": "src/apscheduler/executors/qt.py",
    "chars": 2113,
    "preview": "from __future__ import annotations\n\nimport sys\nfrom collections.abc import Callable\nfrom concurrent.futures import Futur"
  },
  {
    "path": "src/apscheduler/executors/subprocess.py",
    "chars": 946,
    "preview": "from __future__ import annotations\n\nfrom collections.abc import Callable\nfrom contextlib import AsyncExitStack\nfrom func"
  },
  {
    "path": "src/apscheduler/executors/thread.py",
    "chars": 900,
    "preview": "from __future__ import annotations\n\nfrom collections.abc import Callable\nfrom contextlib import AsyncExitStack\nfrom func"
  },
  {
    "path": "src/apscheduler/py.typed",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/apscheduler/serializers/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/apscheduler/serializers/cbor.py",
    "chars": 2553,
    "preview": "from __future__ import annotations\n\nfrom datetime import date, timedelta, tzinfo\nfrom enum import Enum\nfrom typing impor"
  },
  {
    "path": "src/apscheduler/serializers/json.py",
    "chars": 2653,
    "preview": "from __future__ import annotations\n\nfrom datetime import date, timedelta, tzinfo\nfrom enum import Enum\nfrom json import "
  },
  {
    "path": "src/apscheduler/serializers/pickle.py",
    "chars": 1119,
    "preview": "from __future__ import annotations\n\nfrom pickle import dumps, loads\n\nimport attrs\n\nfrom .. import DeserializationError, "
  },
  {
    "path": "src/apscheduler/triggers/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/apscheduler/triggers/calendarinterval.py",
    "chars": 6885,
    "preview": "from __future__ import annotations\n\nfrom datetime import date, datetime, time, timedelta, tzinfo\nfrom typing import Any\n"
  },
  {
    "path": "src/apscheduler/triggers/combining.py",
    "chars": 5726,
    "preview": "from __future__ import annotations\n\nfrom abc import abstractmethod\nfrom datetime import datetime, timedelta\nfrom typing "
  },
  {
    "path": "src/apscheduler/triggers/cron/__init__.py",
    "chars": 10194,
    "preview": "from __future__ import annotations\n\nfrom collections.abc import Sequence\nfrom datetime import datetime, tzinfo\nfrom typi"
  },
  {
    "path": "src/apscheduler/triggers/cron/expressions.py",
    "chars": 8217,
    "preview": "\"\"\"This module contains the expressions applicable for CronTrigger's fields.\"\"\"\n\nfrom __future__ import annotations\n\nimp"
  },
  {
    "path": "src/apscheduler/triggers/cron/fields.py",
    "chars": 4863,
    "preview": "\"\"\"\nFields represent CronTrigger options which map to :class:`~datetime.datetime` fields.\n\"\"\"\n\nfrom __future__ import an"
  },
  {
    "path": "src/apscheduler/triggers/date.py",
    "chars": 1246,
    "preview": "from __future__ import annotations\n\nfrom datetime import datetime\nfrom typing import Any\n\nimport attrs\nfrom attr.validat"
  },
  {
    "path": "src/apscheduler/triggers/interval.py",
    "chars": 4348,
    "preview": "from __future__ import annotations\n\nfrom datetime import datetime, timedelta\nfrom typing import Any\n\nimport attrs\nfrom a"
  },
  {
    "path": "tests/conftest.py",
    "chars": 9265,
    "preview": "from __future__ import annotations\n\nimport logging\nfrom collections.abc import AsyncGenerator, Generator\nfrom contextlib"
  },
  {
    "path": "tests/test_datastores.py",
    "chars": 31466,
    "preview": "from __future__ import annotations\n\nimport platform\nfrom collections.abc import AsyncGenerator\nfrom contextlib import As"
  },
  {
    "path": "tests/test_eventbrokers.py",
    "chars": 5638,
    "preview": "from __future__ import annotations\n\nimport sys\nfrom contextlib import AsyncExitStack\nfrom datetime import datetime, time"
  },
  {
    "path": "tests/test_marshalling.py",
    "chars": 3137,
    "preview": "from __future__ import annotations\n\nimport sys\nfrom datetime import timedelta\nfrom functools import partial\nfrom types i"
  },
  {
    "path": "tests/test_schedulers.py",
    "chars": 59686,
    "preview": "from __future__ import annotations\n\nimport os\nimport sys\nimport threading\nimport time\nfrom collections import defaultdic"
  },
  {
    "path": "tests/test_serializers.py",
    "chars": 1965,
    "preview": "from __future__ import annotations\n\nfrom datetime import datetime, timezone\nfrom typing import NoReturn\nfrom uuid import"
  },
  {
    "path": "tests/triggers/test_calendarinterval.py",
    "chars": 3344,
    "preview": "from __future__ import annotations\n\nfrom datetime import date, datetime\n\nimport pytest\n\nfrom apscheduler.triggers.calend"
  },
  {
    "path": "tests/triggers/test_combining.py",
    "chars": 8599,
    "preview": "from __future__ import annotations\n\nfrom datetime import datetime, timedelta, timezone\n\nimport pytest\n\nfrom apscheduler "
  },
  {
    "path": "tests/triggers/test_cron.py",
    "chars": 21154,
    "preview": "from __future__ import annotations\n\nfrom datetime import datetime\nfrom zoneinfo import ZoneInfo\n\nimport pytest\n\nfrom aps"
  },
  {
    "path": "tests/triggers/test_date.py",
    "chars": 523,
    "preview": "from __future__ import annotations\n\nfrom datetime import datetime\n\nfrom apscheduler.triggers.date import DateTrigger\n\n\nd"
  },
  {
    "path": "tests/triggers/test_interval.py",
    "chars": 1953,
    "preview": "from __future__ import annotations\n\nfrom datetime import datetime, timedelta\n\nimport pytest\n\nfrom apscheduler.triggers.i"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the agronholm/apscheduler GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 99 files (589.5 KB), approximately 132.2k tokens, and a symbol index with 712 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!