From 099d3f025f7a33510150c54d53e0e2e1fca723ad Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 30 Dec 2024 23:47:59 +0100 Subject: [PATCH] Changes: - improved tests - bugfixes - updated store docs --- asyncz/schedulers/base.py | 3 ++- asyncz/schedulers/defaults.py | 7 ++++--- asyncz/stores/base.py | 2 +- docs/release-notes.md | 1 + docs/stores.md | 37 ++++++++++++++++++++++++++++++++--- tests/test_multiprocess.py | 9 +++++++-- tests/test_schedulers.py | 12 +++++++++++- tests/test_stores.py | 17 +++++++++++++++- 8 files changed, 76 insertions(+), 12 deletions(-) diff --git a/asyncz/schedulers/base.py b/asyncz/schedulers/base.py index ad8cb3d..8374ffe 100644 --- a/asyncz/schedulers/base.py +++ b/asyncz/schedulers/base.py @@ -1043,7 +1043,8 @@ def real_add_task( if start_task and self.state == SchedulerState.STATE_RUNNING: self.wakeup() - def resolve_load_plugin(self, module_name: str) -> Any: + @classmethod + def resolve_load_plugin(cls, module_name: str) -> Any: """ Resolve the plugin from its module and attrs. """ diff --git a/asyncz/schedulers/defaults.py b/asyncz/schedulers/defaults.py index f6f5df1..3c0c787 100644 --- a/asyncz/schedulers/defaults.py +++ b/asyncz/schedulers/defaults.py @@ -2,8 +2,8 @@ "date": "asyncz.triggers.date:DateTrigger", "interval": "asyncz.triggers.interval:IntervalTrigger", "cron": "asyncz.triggers.cron.trigger:CronTrigger", - "and": "asyncz.triggers.combining:AndTrigger", - "or": "asyncz.triggers.combining:OrTrigger", + "and": "asyncz.triggers.combination:AndTrigger", + "or": "asyncz.triggers.combination:OrTrigger", "shutdown": "asyncz.triggers.shutdown:ShutdownTrigger", } @@ -16,8 +16,9 @@ "asyncio": "asyncz.executors.asyncio:AsyncIOExecutor", } + stores: dict[str, str] = { - "memory": "asyncz.stores.memory:MemoryTaskStore", + "memory": "asyncz.stores.memory:MemoryStore", "mongodb": "asyncz.stores.mongo:MongoDBStore", "redis": "asyncz.stores.redis:RedisStore", "sqlalchemy": "asyncz.stores.sqlalchemy:SQLAlchemyStore", diff --git a/asyncz/stores/base.py b/asyncz/stores/base.py index 67451cf..d9e6ae0 100644 --- a/asyncz/stores/base.py +++ b/asyncz/stores/base.py @@ -28,7 +28,7 @@ def __init__(self, **kwargs: Any) -> None: def create_lock(self) -> LockProtectedProtocol: """ - Creates a reentrant lock object. + Creates a lock protector. """ if not self.scheduler or not self.scheduler.lock_path: return NullLockProtected() diff --git a/docs/release-notes.md b/docs/release-notes.md index f9b1e7b..6558e38 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ ### Fixed - `and` was mapped to the wrong trigger. +- Some defaults had wrong module pathes. - Missing export of NativeAsyncIOScheduler from schedulers. ## 0.12.0 diff --git a/docs/stores.md b/docs/stores.md index 5cc2f84..2ca4a91 100644 --- a/docs/stores.md +++ b/docs/stores.md @@ -42,7 +42,12 @@ from asyncz.stores.memory import MemoryStore **Store Alias** - `redis` ```python +from asyncz.schedulers import AsyncIOScheduler from asyncz.stores.redis import RedisStore +# assuming redis runs on localhost +scheduler = AsyncIOScheduler(stores={"default": RedisStore()}) +# or +scheduler = AsyncIOScheduler(stores={"default": {"type": "redis"}}) ``` ### Parameters @@ -64,14 +69,29 @@ highest available. Default: `pickle.HIGHEST_PROTOCOL` +* **host** - Host to connect. + + Default: `localhost` + +* **port** - Port to connect. + + Default: `6379` + ## MongoDBStore **Store Alias** - `mongo` ```python +from asyncz.schedulers import AsyncIOScheduler from asyncz.stores.mongo import MongoDBStore +# assuming mongo db runs on localhost +scheduler = AsyncIOScheduler(stores={"default": MongoDBStore()}) +# or +scheduler = AsyncIOScheduler(stores={"default": {"type": "mongo"}}) ``` + + ### Parameters * **database** - The database to store the tasks. @@ -91,20 +111,31 @@ available. Default: `pickle.HIGHEST_PROTOCOL` +* **host** - Host to connect. + + Default: `localhost` + +* **port** - Port to connect. + + Default: `27017` + ## SQLAlchemyStore **Store Alias** - `sqlalchemy` ```python +from asyncz.schedulers import AsyncIOScheduler from asyncz.stores.sqlalchemy import SQLAlchemyStore + +scheduler = AsyncIOScheduler(stores={"default": SQLAlchemyStore(database="sqlite:///./test_suite.sqlite3")}) +# or +scheduler = AsyncIOScheduler(stores={"default": {"type": "sqlalchemy", "database": "sqlite:///./test_suite.sqlite3"}}) ``` ### Parameters -* **database** - The database to store the tasks. Can be string url or engine - - Default: `asyncz` +* **database** - The database to store the tasks. Can be string url or engine. * **tablename** - The tablename. diff --git a/tests/test_multiprocess.py b/tests/test_multiprocess.py index 5ab1017..2b238cd 100644 --- a/tests/test_multiprocess.py +++ b/tests/test_multiprocess.py @@ -1,3 +1,5 @@ +import contextlib +import os import time import pytest @@ -31,11 +33,11 @@ def reset_job_called(): @pytest.mark.flaky(reruns=2) def test_simulated_multiprocess(): scheduler1 = AsyncIOScheduler( - stores={"default": SQLAlchemyStore(database="sqlite:///./test_suite.sqlite3")}, + stores={"default": SQLAlchemyStore(database="sqlite:///./test_mp.sqlite3")}, lock_path="/tmp/{store}_asyncz_test.pid", ) scheduler2 = AsyncIOScheduler( - stores={"default": SQLAlchemyStore(database="sqlite:///./test_suite.sqlite3")}, + stores={"default": SQLAlchemyStore(database="sqlite:///./test_mp.sqlite3")}, lock_path="/tmp/{store}_asyncz_test.pid", ) @@ -67,5 +69,8 @@ def test_simulated_multiprocess(): # fix CancelledError, by giving scheduler more time to send the tasks to the pool # if the pool is closed, newly submitted tasks are cancelled time.sleep(1) + scheduler1.stores["default"].metadata.drop_all(scheduler1.stores["default"].engine) + with contextlib.suppress(FileNotFoundError): + os.remove("./test_mp.sqlite3") assert dummy_job_called == 3 diff --git a/tests/test_schedulers.py b/tests/test_schedulers.py index 0315973..7199411 100644 --- a/tests/test_schedulers.py +++ b/tests/test_schedulers.py @@ -3,6 +3,7 @@ import pickle import time from datetime import datetime, timedelta, tzinfo +from inspect import isclass from threading import Thread from typing import Any, Optional, Union from unittest.mock import MagicMock, patch @@ -38,6 +39,7 @@ ) from asyncz.executors.base import BaseExecutor from asyncz.executors.debug import DebugExecutor +from asyncz.schedulers import defaults from asyncz.schedulers.asyncio import AsyncIOScheduler, NativeAsyncIOScheduler from asyncz.schedulers.base import BaseScheduler, ClassicLogging from asyncz.stores.base import BaseStore @@ -321,7 +323,7 @@ def test_start(self, real_add_task, dispatch_events, scheduler, create_task): assert "default" in scheduler.executors assert "default" in scheduler.stores - scheduler.real_add_task.assert_called_once_with(task, False, True) + scheduler.real_add_task.assert_called_once_with(task, False, True, None) assert scheduler.pending_tasks == [] assert scheduler.dispatch_event.call_count == 3 @@ -1315,3 +1317,11 @@ async def lifespan_gen(): assert is_setup is True assert call_count == 1 assert is_exited is True + + +@pytest.mark.parametrize( + "module_path", + [*defaults.executors.values(), *defaults.stores.values(), *defaults.triggers.values()], +) +def test_defaults_actually_load(module_path): + assert isclass(BaseScheduler.resolve_load_plugin(module_path)) diff --git a/tests/test_stores.py b/tests/test_stores.py index 854dc62..7f36ec8 100644 --- a/tests/test_stores.py +++ b/tests/test_stores.py @@ -3,6 +3,7 @@ import pytest from asyncz.exceptions import ConflictIdError, TaskLookupError +from asyncz.schedulers import AsyncIOScheduler from asyncz.stores.memory import MemoryStore @@ -225,7 +226,7 @@ def test_update_task_clear_next_runtime_when_run_times_are_initially_the_same( store, create_add_task, next_run_time, timezone, index ): tasks = [ - create_add_task(store, dummy_task, datetime(2020, 2, 26), "task%d" % i) for i in range(3) + create_add_task(store, dummy_task, datetime(2020, 2, 26), f"task{i}") for i in range(3) ] tasks[index].next_run_time = timezone.localize(next_run_time) if next_run_time else None store.update_task(tasks[index]) @@ -327,3 +328,17 @@ def test_mongodb_null_database(): mongodb = pytest.importorskip("asyncz.stores.mongo") exc = pytest.raises(ValueError, mongodb.MongoDBStore, database="") assert "database" in str(exc.value) + + +@pytest.mark.parametrize( + "param", + [ + {"type": "sqlalchemy", "database": "sqlite:///:memory:"}, + {"type": "memory"}, + ], + ids=["sqlalchemy", "memory"], +) +def test_store_as_type(param): + scheduler = AsyncIOScheduler(stores={"default": param}) + scheduler.start() + scheduler.shutdown()