diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index 0146a10..27cc0b7 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -1,6 +1,6 @@ name: tox -on: [push, pull_request] +on: [ push, pull_request ] jobs: tox: @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] steps: - uses: actions/checkout@v4 @@ -16,7 +16,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v3 with: - version: "0.4.18" + version: "0.5" enable-cache: true - name: Set up Python ${{ matrix.python-version }} diff --git a/.gitignore b/.gitignore index d01cff3..a41c0c0 100644 --- a/.gitignore +++ b/.gitignore @@ -55,4 +55,6 @@ MANIFEST .epg_data/* # uv -.python-version \ No newline at end of file +.python-version + +.venv/ \ No newline at end of file diff --git a/aiocron/__init__.py b/aiocron/__init__.py index de9df35..a8e4a47 100644 --- a/aiocron/__init__.py +++ b/aiocron/__init__.py @@ -3,22 +3,24 @@ from datetime import datetime from functools import wraps, partial from tzlocal import get_localzone -from uuid import uuid4 +from uuid import uuid4, UUID import time import asyncio import sys import inspect +import typing as tp +import zoneinfo -async def null_callback(*args): +async def null_callback(*args: tp.Any) -> tp.Tuple[tp.Any, ...]: return args -def wrap_func(func): +def wrap_func(func: tp.Callable[..., tp.Union[tp.Any, tp.Awaitable[tp.Any]]]) -> tp.Callable[..., tp.Awaitable[tp.Any]]: """wrap in a coroutine""" @wraps(func) - async def wrapper(*args, **kwargs): + async def wrapper(*args: tp.Any, **kwargs: tp.Any) -> tp.Any: result = func(*args, **kwargs) if inspect.isawaitable(result): result = await result @@ -30,15 +32,15 @@ async def wrapper(*args, **kwargs): class Cron(object): def __init__( self, - spec, - func=None, - args=(), - kwargs=None, - start=False, - uuid=None, - loop=None, - tz=None, - ): + spec: str, + func: tp.Optional[tp.Callable[..., tp.Union[tp.Any, tp.Awaitable[tp.Any]]]] = None, + args: tp.Tuple[tp.Any, ...] = (), + kwargs: tp.Optional[tp.Mapping[str, tp.Any]] = None, + start: bool = False, + uuid: tp.Optional[UUID] = None, + loop: tp.Optional[asyncio.AbstractEventLoop] = None, + tz: tp.Optional[zoneinfo.ZoneInfo] = None, + ) -> None: self.spec = spec if func is not None: kwargs = kwargs or {} @@ -46,34 +48,36 @@ def __init__( else: self.func = null_callback self.tz = get_localzone() if tz is None else tz - self.cron = wrap_func(self.func) + self.cron: tp.Callable[..., tp.Awaitable[tp.Any]] = wrap_func(self.func) self.auto_start = start self.uuid = uuid if uuid is not None else uuid4() - self.handle = self.future = self.cronsim = None + self.handle = None + self.future: tp.Optional[asyncio.Future] = None + self.cronsim: tp.Optional[CronSim] = None self.loop = loop if loop is not None else asyncio.get_event_loop() if start and self.func is not null_callback: self.handle = self.loop.call_soon_threadsafe(self.start) - def start(self): + def start(self) -> None: """Start scheduling""" self.stop() self.initialize() self.handle = self.loop.call_at(self.get_next(), self.call_next) - def stop(self): + def stop(self) -> None: """Stop scheduling""" if self.handle is not None: self.handle.cancel() self.handle = self.future = self.cronsim = None - async def next(self, *args): + async def next(self, *args: tp.Any) -> tp.Any: """yield from .next()""" self.initialize() self.future = asyncio.Future(loop=self.loop) self.handle = self.loop.call_at(self.get_next(), self.call_func, *args) return await self.future - def initialize(self): + def initialize(self) -> None: """Initialize cronsim and related times""" if self.cronsim is None: self.time = time.time() @@ -81,11 +85,11 @@ def initialize(self): self.loop_time = self.loop.time() self.cronsim = CronSim(self.spec, self.datetime) - def get_next(self): + def get_next(self) -> float: """Return next iteration time related to loop time""" return self.loop_time + (next(self.cronsim).timestamp() - self.time) - def call_next(self): + def call_next(self) -> None: """Set next hop in the loop. Call task""" if self.handle is not None: self.handle.cancel() @@ -93,7 +97,7 @@ def call_next(self): self.handle = self.loop.call_at(next_time, self.call_next) self.call_func() - def call_func(self, *args, **kwargs): + def call_func(self, *args: tp.Any, **kwargs: tp.Any) -> None: """Called. Take care of exceptions using gather""" """Check the version of python installed""" if sys.version_info[0:2] >= (3, 10): @@ -105,7 +109,7 @@ def call_func(self, *args, **kwargs): self.cron(*args, **kwargs), loop=self.loop, return_exceptions=True ).add_done_callback(self.set_result) - def set_result(self, result): + def set_result(self, result: asyncio.Future) -> None: """Set future's result if needed (can be an exception). Else raise if needed.""" result = result.result()[0] @@ -118,7 +122,7 @@ def set_result(self, result): elif isinstance(result, Exception): raise result - def __call__(self, func): + def __call__(self, func: tp.Callable[..., tp.Awaitable[tp.Any]]) -> 'Cron': """Used as a decorator""" self.func = func self.cron = wrap_func(func) @@ -126,14 +130,22 @@ def __call__(self, func): self.loop.call_soon_threadsafe(self.start) return self - def __str__(self): + def __str__(self) -> str: return "{0.spec} {0.func}".format(self) - def __repr__(self): + def __repr__(self) -> str: return "".format(self) -def crontab(spec, func=None, args=(), kwargs=None, start=True, loop=None, tz=None): +def crontab( + spec: str, + func: tp.Optional[tp.Callable[..., tp.Union[tp.Any, tp.Awaitable[tp.Any]]]] = None, + args: tp.Tuple[tp.Any, ...] = (), + kwargs: tp.Optional[tp.Mapping[str, tp.Any]] = None, + start: bool = False, + loop: tp.Optional[asyncio.AbstractEventLoop] = None, + tz: tp.Optional[zoneinfo.ZoneInfo] = None, +) -> Cron: return Cron( spec, func=func, args=args, kwargs=kwargs, start=start, loop=loop, tz=tz ) diff --git a/aiocron/__main__.py b/aiocron/__main__.py index 5b812e4..65e6261 100644 --- a/aiocron/__main__.py +++ b/aiocron/__main__.py @@ -6,7 +6,7 @@ import argparse -def main(): +def main() -> None: parser = argparse.ArgumentParser() parser.prog = "python -m aiocron" parser.add_argument( diff --git a/pyproject.toml b/pyproject.toml index 39ea351..3ad1785 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "License :: OSI Approved :: MIT License", "Topic :: Software Development :: Libraries :: Python Modules", ] @@ -47,7 +48,6 @@ build-backend = "setuptools.build_meta" [tool.setuptools] packages = ["aiocron"] - [tool.pytest.ini_options] addopts = "--cov aiocron --cov-report term-missing" testpaths = ["tests"] diff --git a/uv.lock b/uv.lock index 9fde648..432de62 100644 --- a/uv.lock +++ b/uv.lock @@ -11,17 +11,14 @@ name = "aiocron" version = "1.9.dev0" source = { editable = "." } dependencies = [ - { name = "croniter" }, + { name = "cronsim" }, + { name = "python-dateutil" }, { name = "tox" }, { name = "tox-uv" }, { name = "tzlocal" }, ] [package.optional-dependencies] -coverage = [ - { name = "pytest" }, - { name = "pytest-cov" }, -] test = [ { name = "coverage" }, { name = "pytest" }, @@ -31,11 +28,10 @@ test = [ [package.metadata] requires-dist = [ { name = "coverage", marker = "extra == 'test'", specifier = ">=7.6.1" }, - { name = "croniter", specifier = ">=3.0.3" }, - { name = "pytest", marker = "extra == 'coverage'", specifier = ">=8.3.3" }, + { name = "cronsim", specifier = ">=2.6" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.3" }, - { name = "pytest-cov", marker = "extra == 'coverage'", specifier = ">=5.0.0" }, { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=5.0.0" }, + { name = "python-dateutil", specifier = ">=2.9.0" }, { name = "tox", specifier = ">=4.21.1" }, { name = "tox-uv", specifier = ">=1.13.0" }, { name = "tzlocal", specifier = ">=5.2" }, @@ -143,16 +139,12 @@ toml = [ ] [[package]] -name = "croniter" -version = "3.0.3" +name = "cronsim" +version = "2.6" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-dateutil" }, - { name = "pytz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/44/7a/14b0b14ab0203e2c79493cf487829dc294d5c44bedc810ab2f4a97fc9ff4/croniter-3.0.3.tar.gz", hash = "sha256:34117ec1741f10a7bd0ec3ad7d8f0eb8fa457a2feb9be32e6a2250e158957668", size = 53088 } +sdist = { url = "https://files.pythonhosted.org/packages/5b/d8/cfb8d51a51f6076ffa09902c02978c7db9764cca78f4ee832e691d20f44b/cronsim-2.6.tar.gz", hash = "sha256:5aab98716ef90ab5ac6be294b2c3965dbf76dc869f048846a0af74ebb506c10d", size = 20315 } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/6a/f2f68e0f9cf702b6d055ab53cab0d8c100f04e86228ca500a8ca9de94b58/croniter-3.0.3-py2.py3-none-any.whl", hash = "sha256:b3bd11f270dc54ccd1f2397b813436015a86d30ffc5a7a9438eec1ed916f2101", size = 22422 }, + { url = "https://files.pythonhosted.org/packages/8c/dd/9c40c4e0f4d3cb6cf52eb335e9cc1fa140c1f3a87146fb6987f465b069da/cronsim-2.6-py3-none-any.whl", hash = "sha256:5e153ff8ed64da7ee8d5caac470dbeda8024ab052c3010b1be149772b4801835", size = 13500 }, ] [[package]] @@ -273,15 +265,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, ] -[[package]] -name = "pytz" -version = "2024.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/31/3c70bf7603cc2dca0f19bdc53b4537a797747a58875b552c8c413d963a3f/pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", size = 319692 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 }, -] - [[package]] name = "six" version = "1.16.0"