summaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
authorAlex Grönholm <alex.gronholm@nextday.fi>2022-07-27 13:01:55 +0300
committerAlex Grönholm <alex.gronholm@nextday.fi>2022-07-27 13:02:08 +0300
commite4a926f5ec113be711dad94773c45b34aebd0ff1 (patch)
tree8b9f1d9553080b06201d7d10ae43de440401cce2 /examples
parent0bfc8d54f358c0c26d613088547b1b025cd16a6e (diff)
downloadapscheduler-e4a926f5ec113be711dad94773c45b34aebd0ff1.tar.gz
Updated the examples for v4
Diffstat (limited to 'examples')
-rw-r--r--examples/README.rst123
-rw-r--r--examples/executors/processpool.py26
-rw-r--r--examples/misc/reference.py19
-rw-r--r--examples/rpc/client.py20
-rw-r--r--examples/rpc/server.py62
-rw-r--r--examples/schedulers/async_.py26
-rw-r--r--examples/schedulers/sync.py22
-rw-r--r--examples/separate_worker/example_tasks.py12
-rw-r--r--examples/separate_worker/sync_scheduler.py29
-rw-r--r--examples/separate_worker/sync_worker.py26
-rw-r--r--examples/standalone/async_memory.py28
-rw-r--r--examples/standalone/async_mysql.py38
-rw-r--r--examples/standalone/async_postgres.py38
-rw-r--r--examples/standalone/sync_memory.py23
-rw-r--r--examples/web/asgi_fastapi.py63
-rw-r--r--examples/web/asgi_noframework.py73
-rw-r--r--examples/web/asgi_starlette.py64
-rw-r--r--examples/web/wsgi_flask.py41
-rw-r--r--examples/web/wsgi_noframework.py41
19 files changed, 599 insertions, 175 deletions
diff --git a/examples/README.rst b/examples/README.rst
new file mode 100644
index 0000000..0edc0e5
--- /dev/null
+++ b/examples/README.rst
@@ -0,0 +1,123 @@
+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.
diff --git a/examples/executors/processpool.py b/examples/executors/processpool.py
deleted file mode 100644
index f8538e1..0000000
--- a/examples/executors/processpool.py
+++ /dev/null
@@ -1,26 +0,0 @@
-"""
-Demonstrates how to schedule a job to be run in a process pool on 3 second intervals.
-"""
-
-from __future__ import annotations
-
-import os
-from datetime import datetime
-
-from apscheduler.schedulers.blocking import BlockingScheduler
-
-
-def tick():
- print("Tick! The time is: %s" % datetime.now())
-
-
-if __name__ == "__main__":
- scheduler = BlockingScheduler()
- scheduler.add_executor("processpool")
- scheduler.add_job(tick, "interval", seconds=3)
- print("Press Ctrl+{} to exit".format("Break" if os.name == "nt" else "C"))
-
- try:
- scheduler.initialize()
- except (KeyboardInterrupt, SystemExit):
- pass
diff --git a/examples/misc/reference.py b/examples/misc/reference.py
deleted file mode 100644
index 75bfb1c..0000000
--- a/examples/misc/reference.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""
-Basic example showing how to schedule a callable using a textual reference.
-"""
-
-from __future__ import annotations
-
-import os
-
-from apscheduler.schedulers.blocking import BlockingScheduler
-
-if __name__ == "__main__":
- scheduler = BlockingScheduler()
- scheduler.add_job("sys:stdout.write", "interval", seconds=3, args=["tick\n"])
- print("Press Ctrl+{} to exit".format("Break" if os.name == "nt" else "C"))
-
- try:
- scheduler.initialize()
- except (KeyboardInterrupt, SystemExit):
- pass
diff --git a/examples/rpc/client.py b/examples/rpc/client.py
deleted file mode 100644
index 2a3a52a..0000000
--- a/examples/rpc/client.py
+++ /dev/null
@@ -1,20 +0,0 @@
-"""
-This is an example RPC client that connects to the RPyC based scheduler service.
-
-It first connects to the RPyC server on localhost:12345.
-Then it schedules a job to run on 2 second intervals and sleeps for 10 seconds.
-After that, it unschedules the job and exits.
-"""
-
-from __future__ import annotations
-
-from time import sleep
-
-import rpyc
-
-conn = rpyc.connect("localhost", 12345)
-job = conn.root.add_job(
- "server:print_text", "interval", args=["Hello, World"], seconds=2
-)
-sleep(10)
-conn.root.remove_job(job.id)
diff --git a/examples/rpc/server.py b/examples/rpc/server.py
deleted file mode 100644
index fc40c04..0000000
--- a/examples/rpc/server.py
+++ /dev/null
@@ -1,62 +0,0 @@
-"""
-This is an example showing how to make the scheduler into a remotely accessible service.
-It uses RPyC to set up a service through which the scheduler can be made to add, modify and remove
-jobs.
-
-To run, first install RPyC using pip. Then change the working directory to the ``rpc`` directory
-and run it with ``python -m server``.
-"""
-
-from __future__ import annotations
-
-import rpyc
-from rpyc.utils.server import ThreadedServer
-
-from apscheduler.schedulers.background import BackgroundScheduler
-
-
-def print_text(text):
- print(text)
-
-
-class SchedulerService(rpyc.Service):
- def exposed_add_job(self, func, *args, **kwargs):
- return scheduler.add_job(func, *args, **kwargs)
-
- def exposed_modify_job(self, job_id, jobstore=None, **changes):
- return scheduler.modify_job(job_id, jobstore, **changes)
-
- def exposed_reschedule_job(
- self, job_id, jobstore=None, trigger=None, **trigger_args
- ):
- return scheduler.reschedule_job(job_id, jobstore, trigger, **trigger_args)
-
- def exposed_pause_job(self, job_id, jobstore=None):
- return scheduler.pause_job(job_id, jobstore)
-
- def exposed_resume_job(self, job_id, jobstore=None):
- return scheduler.resume_job(job_id, jobstore)
-
- def exposed_remove_job(self, job_id, jobstore=None):
- scheduler.remove_job(job_id, jobstore)
-
- def exposed_get_job(self, job_id):
- return scheduler.get_job(job_id)
-
- def exposed_get_jobs(self, jobstore=None):
- return scheduler.get_jobs(jobstore)
-
-
-if __name__ == "__main__":
- scheduler = BackgroundScheduler()
- scheduler.initialize()
- protocol_config = {"allow_public_attrs": True}
- server = ThreadedServer(
- SchedulerService, port=12345, protocol_config=protocol_config
- )
- try:
- server.initialize()
- except (KeyboardInterrupt, SystemExit):
- pass
- finally:
- scheduler.stop()
diff --git a/examples/schedulers/async_.py b/examples/schedulers/async_.py
deleted file mode 100644
index e4eac0c..0000000
--- a/examples/schedulers/async_.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from __future__ import annotations
-
-import logging
-
-import anyio
-
-from apscheduler.schedulers.async_ import AsyncScheduler
-from apscheduler.triggers.interval import IntervalTrigger
-from apscheduler.workers.async_ import AsyncWorker
-
-
-def say_hello():
- print("Hello!")
-
-
-async def main():
- async with AsyncScheduler() as scheduler, AsyncWorker(scheduler.data_store):
- await scheduler.add_schedule(say_hello, IntervalTrigger(seconds=1))
- await scheduler.wait_until_stopped()
-
-
-logging.basicConfig(level=logging.DEBUG)
-try:
- anyio.run(main)
-except (KeyboardInterrupt, SystemExit):
- pass
diff --git a/examples/schedulers/sync.py b/examples/schedulers/sync.py
deleted file mode 100644
index 12ae17f..0000000
--- a/examples/schedulers/sync.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from __future__ import annotations
-
-import logging
-
-from apscheduler.schedulers.sync import Scheduler
-from apscheduler.triggers.interval import IntervalTrigger
-from apscheduler.workers.sync import Worker
-
-
-def say_hello():
- print("Hello!")
-
-
-logging.basicConfig(level=logging.DEBUG)
-try:
- with Scheduler() as scheduler, Worker(
- scheduler.data_store, portal=scheduler.portal
- ):
- scheduler.add_schedule(say_hello, IntervalTrigger(seconds=1))
- scheduler.wait_until_stopped()
-except (KeyboardInterrupt, SystemExit):
- pass
diff --git a/examples/separate_worker/example_tasks.py b/examples/separate_worker/example_tasks.py
new file mode 100644
index 0000000..568b77c
--- /dev/null
+++ b/examples/separate_worker/example_tasks.py
@@ -0,0 +1,12 @@
+"""
+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())
diff --git a/examples/separate_worker/sync_scheduler.py b/examples/separate_worker/sync_scheduler.py
new file mode 100644
index 0000000..aeba93c
--- /dev/null
+++ b/examples/separate_worker/sync_scheduler.py
@@ -0,0 +1,29 @@
+"""
+Example demonstrating the separation of scheduler and worker.
+This script runs the scheduler part. You need to be running both this and the worker
+script simultaneously in order for the scheduled task to be run.
+
+Requires the "postgresql" and "redis" services to be running.
+To install prerequisites: pip install sqlalchemy psycopg2 redis
+To run: python sync_scheduler.py
+
+When run together with sync_worker.py, it should print a line on the console
+on a one-second interval.
+"""
+
+from __future__ import annotations
+
+from example_tasks import tick
+from sqlalchemy.future import create_engine
+
+from apscheduler.datastores.sqlalchemy import SQLAlchemyDataStore
+from apscheduler.eventbrokers.redis import RedisEventBroker
+from apscheduler.schedulers.sync import Scheduler
+from apscheduler.triggers.interval import IntervalTrigger
+
+engine = create_engine("postgresql+psycopg2://postgres:secret@localhost/testdb")
+data_store = SQLAlchemyDataStore(engine)
+event_broker = RedisEventBroker.from_url("redis://localhost")
+with Scheduler(data_store, event_broker, start_worker=False) as scheduler:
+ scheduler.add_schedule(tick, IntervalTrigger(seconds=1), id="tick")
+ scheduler.wait_until_stopped()
diff --git a/examples/separate_worker/sync_worker.py b/examples/separate_worker/sync_worker.py
new file mode 100644
index 0000000..30bdb78
--- /dev/null
+++ b/examples/separate_worker/sync_worker.py
@@ -0,0 +1,26 @@
+"""
+Example demonstrating the separation of scheduler and worker.
+This script runs the worker part. You need to be running both this and the scheduler
+script simultaneously in order for the scheduled task to be run.
+
+Requires the "postgresql" and "redis" services to be running.
+To install prerequisites: pip install sqlalchemy psycopg2 redis
+To run: python sync_worker.py
+
+When run together with sync_scheduler.py, it should print a line on the
+console on a one-second interval.
+"""
+
+from __future__ import annotations
+
+from sqlalchemy.future import create_engine
+
+from apscheduler.datastores.sqlalchemy import SQLAlchemyDataStore
+from apscheduler.eventbrokers.redis import RedisEventBroker
+from apscheduler.workers.sync import Worker
+
+engine = create_engine("postgresql+psycopg2://postgres:secret@localhost/testdb")
+data_store = SQLAlchemyDataStore(engine)
+event_broker = RedisEventBroker.from_url("redis://localhost")
+worker = Worker(data_store, event_broker)
+worker.run_until_stopped()
diff --git a/examples/standalone/async_memory.py b/examples/standalone/async_memory.py
new file mode 100644
index 0000000..b3db775
--- /dev/null
+++ b/examples/standalone/async_memory.py
@@ -0,0 +1,28 @@
+"""
+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.schedulers.async_ 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.wait_until_stopped()
+
+
+run(main())
diff --git a/examples/standalone/async_mysql.py b/examples/standalone/async_mysql.py
new file mode 100644
index 0000000..3f6f95f
--- /dev/null
+++ b/examples/standalone/async_mysql.py
@@ -0,0 +1,38 @@
+"""
+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_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.datastores.async_sqlalchemy import AsyncSQLAlchemyDataStore
+from apscheduler.schedulers.async_ import AsyncScheduler
+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 = AsyncSQLAlchemyDataStore(engine)
+ async with AsyncScheduler(data_store) as scheduler:
+ await scheduler.add_schedule(tick, IntervalTrigger(seconds=1), id="tick")
+ await scheduler.wait_until_stopped()
+
+
+run(main())
diff --git a/examples/standalone/async_postgres.py b/examples/standalone/async_postgres.py
new file mode 100644
index 0000000..e6083f7
--- /dev/null
+++ b/examples/standalone/async_postgres.py
@@ -0,0 +1,38 @@
+"""
+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.datastores.async_sqlalchemy import AsyncSQLAlchemyDataStore
+from apscheduler.schedulers.async_ import AsyncScheduler
+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 = AsyncSQLAlchemyDataStore(engine)
+ async with AsyncScheduler(data_store) as scheduler:
+ await scheduler.add_schedule(tick, IntervalTrigger(seconds=1), id="tick")
+ await scheduler.wait_until_stopped()
+
+
+run(main())
diff --git a/examples/standalone/sync_memory.py b/examples/standalone/sync_memory.py
new file mode 100644
index 0000000..6d804f7
--- /dev/null
+++ b/examples/standalone/sync_memory.py
@@ -0,0 +1,23 @@
+"""
+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.schedulers.sync 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.wait_until_stopped()
diff --git a/examples/web/asgi_fastapi.py b/examples/web/asgi_fastapi.py
new file mode 100644
index 0000000..2cfa5c4
--- /dev/null
+++ b/examples/web/asgi_fastapi.py
@@ -0,0 +1,63 @@
+"""
+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 datetime import datetime
+
+from fastapi import FastAPI
+from fastapi.middleware import Middleware
+from fastapi.requests import Request
+from fastapi.responses import PlainTextResponse, Response
+from sqlalchemy.ext.asyncio import create_async_engine
+from starlette.types import ASGIApp, Receive, Scope, Send
+
+from apscheduler.datastores.async_sqlalchemy import AsyncSQLAlchemyDataStore
+from apscheduler.eventbrokers.asyncpg import AsyncpgEventBroker
+from apscheduler.schedulers.async_ import AsyncScheduler
+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.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 = AsyncSQLAlchemyDataStore(engine)
+event_broker = AsyncpgEventBroker.from_async_sqla_engine(engine)
+scheduler = AsyncScheduler()
+middleware = [Middleware(SchedulerMiddleware, scheduler=scheduler)]
+app = FastAPI(middleware=middleware)
+app.add_api_route("/", root)
diff --git a/examples/web/asgi_noframework.py b/examples/web/asgi_noframework.py
new file mode 100644
index 0000000..c3ac184
--- /dev/null
+++ b/examples/web/asgi_noframework.py
@@ -0,0 +1,73 @@
+"""
+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.datastores.async_sqlalchemy import AsyncSQLAlchemyDataStore
+from apscheduler.eventbrokers.asyncpg import AsyncpgEventBroker
+from apscheduler.schedulers.async_ import AsyncScheduler
+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 = AsyncSQLAlchemyDataStore(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 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
diff --git a/examples/web/asgi_starlette.py b/examples/web/asgi_starlette.py
new file mode 100644
index 0000000..edc070c
--- /dev/null
+++ b/examples/web/asgi_starlette.py
@@ -0,0 +1,64 @@
+"""
+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.datastores.async_sqlalchemy import AsyncSQLAlchemyDataStore
+from apscheduler.eventbrokers.asyncpg import AsyncpgEventBroker
+from apscheduler.schedulers.async_ import AsyncScheduler
+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.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 = AsyncSQLAlchemyDataStore(engine)
+event_broker = AsyncpgEventBroker.from_async_sqla_engine(engine)
+scheduler = AsyncScheduler()
+routes = [Route("/", root)]
+middleware = [Middleware(SchedulerMiddleware, scheduler=scheduler)]
+app = Starlette(routes=routes, middleware=middleware)
diff --git a/examples/web/wsgi_flask.py b/examples/web/wsgi_flask.py
new file mode 100644
index 0000000..cc74c7b
--- /dev/null
+++ b/examples/web/wsgi_flask.py
@@ -0,0 +1,41 @@
+"""
+Example demonstrating use with WSGI (raw WSGI application, no framework).
+
+Requires the "postgresql" service to be running.
+To install prerequisites: pip install sqlalchemy psycopg2 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.datastores.sqlalchemy import SQLAlchemyDataStore
+from apscheduler.eventbrokers.redis import RedisEventBroker
+from apscheduler.schedulers.sync import Scheduler
+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+psycopg2://postgres:secret@localhost/testdb")
+data_store = SQLAlchemyDataStore(engine)
+event_broker = RedisEventBroker.from_url("redis://localhost")
+scheduler = Scheduler(data_store, event_broker)
+scheduler.add_schedule(tick, IntervalTrigger(seconds=1), id="tick")
+scheduler.start_in_background()
diff --git a/examples/web/wsgi_noframework.py b/examples/web/wsgi_noframework.py
new file mode 100644
index 0000000..c143f3f
--- /dev/null
+++ b/examples/web/wsgi_noframework.py
@@ -0,0 +1,41 @@
+"""
+Example demonstrating use with WSGI (raw WSGI application, no framework).
+
+Requires the "postgresql" service to be running.
+To install prerequisites: pip install sqlalchemy psycopg2 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.datastores.sqlalchemy import SQLAlchemyDataStore
+from apscheduler.schedulers.sync import Scheduler
+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+psycopg2://postgres:secret@localhost/testdb")
+data_store = SQLAlchemyDataStore(engine)
+scheduler = Scheduler(data_store)
+scheduler.add_schedule(tick, IntervalTrigger(seconds=1), id="tick")
+scheduler.start_in_background()