grelmicro 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. grelmicro-0.1.0/.gitignore +15 -0
  2. grelmicro-0.1.0/.pre-commit-config.yaml +52 -0
  3. grelmicro-0.1.0/.vscode/settings.json +37 -0
  4. grelmicro-0.1.0/PKG-INFO +45 -0
  5. grelmicro-0.1.0/README.md +3 -0
  6. grelmicro-0.1.0/docs/sync/leaderelection.md +6 -0
  7. grelmicro-0.1.0/docs/sync/lock.md +12 -0
  8. grelmicro-0.1.0/docs/sync.md +1 -0
  9. grelmicro-0.1.0/docs/task.md +1 -0
  10. grelmicro-0.1.0/examples/single_file_app.py +116 -0
  11. grelmicro-0.1.0/grelmicro/__init__.py +1 -0
  12. grelmicro-0.1.0/grelmicro/abc/__init__.py +1 -0
  13. grelmicro-0.1.0/grelmicro/abc/lock.py +95 -0
  14. grelmicro-0.1.0/grelmicro/abc/lockbackend.py +84 -0
  15. grelmicro-0.1.0/grelmicro/abc/synchronization.py +21 -0
  16. grelmicro-0.1.0/grelmicro/abc/task.py +31 -0
  17. grelmicro-0.1.0/grelmicro/backends/__init__.py +5 -0
  18. grelmicro-0.1.0/grelmicro/backends/memory/__init__.py +9 -0
  19. grelmicro-0.1.0/grelmicro/backends/memory/lock.py +72 -0
  20. grelmicro-0.1.0/grelmicro/backends/postgres/__init__.py +8 -0
  21. grelmicro-0.1.0/grelmicro/backends/postgres/lock.py +134 -0
  22. grelmicro-0.1.0/grelmicro/backends/redis/__init__.py +8 -0
  23. grelmicro-0.1.0/grelmicro/backends/redis/lock.py +77 -0
  24. grelmicro-0.1.0/grelmicro/backends/redis/pyproject.toml +147 -0
  25. grelmicro-0.1.0/grelmicro/backends/registry.py +30 -0
  26. grelmicro-0.1.0/grelmicro/errors.py +26 -0
  27. grelmicro-0.1.0/grelmicro/py.typed +0 -0
  28. grelmicro-0.1.0/grelmicro/sync/__init__.py +7 -0
  29. grelmicro-0.1.0/grelmicro/sync/_utils.py +34 -0
  30. grelmicro-0.1.0/grelmicro/sync/errors.py +49 -0
  31. grelmicro-0.1.0/grelmicro/sync/leaderelection.py +374 -0
  32. grelmicro-0.1.0/grelmicro/sync/lock.py +312 -0
  33. grelmicro-0.1.0/grelmicro/task/__init__.py +6 -0
  34. grelmicro-0.1.0/grelmicro/task/_interval.py +86 -0
  35. grelmicro-0.1.0/grelmicro/task/_utils.py +41 -0
  36. grelmicro-0.1.0/grelmicro/task/errors.py +25 -0
  37. grelmicro-0.1.0/grelmicro/task/manager.py +89 -0
  38. grelmicro-0.1.0/grelmicro/task/router.py +131 -0
  39. grelmicro-0.1.0/grelmicro/types.py +5 -0
  40. grelmicro-0.1.0/pyproject.toml +142 -0
  41. grelmicro-0.1.0/tests/__init__.py +1 -0
  42. grelmicro-0.1.0/tests/backends/__init__.py +1 -0
  43. grelmicro-0.1.0/tests/backends/test_lock.py +262 -0
  44. grelmicro-0.1.0/tests/backends/test_postgres.py +45 -0
  45. grelmicro-0.1.0/tests/backends/test_registry.py +72 -0
  46. grelmicro-0.1.0/tests/conftest.py +9 -0
  47. grelmicro-0.1.0/tests/sync/__init__.py +1 -0
  48. grelmicro-0.1.0/tests/sync/test_leaderelection.py +446 -0
  49. grelmicro-0.1.0/tests/sync/test_lock.py +496 -0
  50. grelmicro-0.1.0/tests/sync/utils.py +23 -0
  51. grelmicro-0.1.0/tests/task/__init__.py +1 -0
  52. grelmicro-0.1.0/tests/task/samples.py +86 -0
  53. grelmicro-0.1.0/tests/task/test_interval.py +123 -0
  54. grelmicro-0.1.0/tests/task/test_manager.py +81 -0
  55. grelmicro-0.1.0/tests/task/test_router.py +172 -0
  56. grelmicro-0.1.0/uv.lock +1058 -0
@@ -0,0 +1,15 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Coverage
13
+ .coverage
14
+ coverage.xml
15
+ junittest.xml
@@ -0,0 +1,52 @@
1
+ # See https://pre-commit.com for more information
2
+ # See https://pre-commit.com/hooks.html for more hooks
3
+ default_language_version:
4
+ python: python3.11
5
+ repos:
6
+
7
+ - repo: https://github.com/pre-commit/pre-commit-hooks
8
+ rev: v4.6.0
9
+ hooks:
10
+ - id: end-of-file-fixer
11
+ - id: check-toml
12
+ - id: check-yaml
13
+ - id: check-added-large-files
14
+ - id: trailing-whitespace
15
+
16
+ - repo: https://github.com/astral-sh/uv-pre-commit
17
+ rev: 0.5.2
18
+ hooks:
19
+ - id: uv-lock
20
+
21
+ - repo: https://github.com/astral-sh/ruff-pre-commit
22
+ rev: v0.7.3
23
+ hooks:
24
+ - id: ruff
25
+ args: [ --fix ]
26
+ - id: ruff-format
27
+
28
+ - repo: https://github.com/codespell-project/codespell
29
+ rev: v2.3.0
30
+ hooks:
31
+ - id: codespell
32
+
33
+ - repo: local
34
+ hooks:
35
+ - id: mypy
36
+ name: mypy
37
+ description: "Run 'mypy' for static type checking"
38
+ entry: uv run mypy
39
+ language: system
40
+ types_or: [python, pyi]
41
+ args: ["--ignore-missing-imports", "--scripts-are-modules"]
42
+ require_serial: true
43
+ - id: pytest
44
+ name: pytest
45
+ description: "Run 'pytest' for testing and code coverage"
46
+ entry: uv run pytest
47
+ language: system
48
+ pass_filenames: false
49
+
50
+ ci:
51
+ autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
52
+ autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate
@@ -0,0 +1,37 @@
1
+ {
2
+ "editor.rulers": [88, 100],
3
+ "files.exclude": {
4
+ "**/.git": true,
5
+ "**/.DS_Store": true,
6
+ "**/Thumbs.db": true,
7
+ "**/__pycache__": true,
8
+ "**/.venv": true,
9
+ "**/.mypy_cache": true,
10
+ "**/.pytest_cache": true,
11
+ "**/.ruff_cache": true,
12
+ ".coverage": true,
13
+ },
14
+ "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
15
+ "python.terminal.activateEnvInCurrentTerminal": true,
16
+ "python.terminal.activateEnvironment": true,
17
+
18
+ "[python]": {
19
+ "editor.formatOnSave": true,
20
+ "editor.codeActionsOnSave": {
21
+ "source.fixAll": "explicit",
22
+ "source.organizeImports": "explicit"
23
+ },
24
+ "editor.defaultFormatter": "charliermarsh.ruff"
25
+ },
26
+ "mypy-type-checker.importStrategy": "fromEnvironment",
27
+
28
+ "python.testing.pytestEnabled": true,
29
+ "python.testing.unittestEnabled": false,
30
+ "python.testing.pytestArgs": [
31
+ "tests",
32
+ "--no-cov"
33
+ ],
34
+ "python.analysis.inlayHints.pytestParameters": true,
35
+ "files.trimTrailingWhitespace": true,
36
+ "terminal.integrated.scrollback": 10000,
37
+ }
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.3
2
+ Name: grelmicro
3
+ Version: 0.1.0
4
+ Summary: Grelmicro is a tool-kit designed to streamline the development cloud-native applications with FastAPI.
5
+ Author-email: Loïc Gremaud <grelinfo@gmail.com>
6
+ License: MIT
7
+ Classifier: Development Status :: 1 - Planning
8
+ Classifier: Environment :: Web Environment
9
+ Classifier: Framework :: AsyncIO
10
+ Classifier: Framework :: FastAPI
11
+ Classifier: Framework :: Pydantic
12
+ Classifier: Framework :: Pydantic :: 2
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Information Technology
15
+ Classifier: Intended Audience :: System Administrators
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Topic :: Internet
26
+ Classifier: Topic :: Software Development
27
+ Classifier: Topic :: Software Development :: Libraries
28
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
29
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
30
+ Classifier: Typing :: Typed
31
+ Requires-Python: >=3.11
32
+ Requires-Dist: anyio>=4.0.0
33
+ Requires-Dist: fast-depends>=2.0.0
34
+ Requires-Dist: pydantic>=2.0.0
35
+ Provides-Extra: cli
36
+ Requires-Dist: typer>=0.12.3; extra == 'cli'
37
+ Provides-Extra: postgres
38
+ Requires-Dist: asyncpg>=0.30.0; extra == 'postgres'
39
+ Provides-Extra: redis
40
+ Requires-Dist: redis>=5.0.0; extra == 'redis'
41
+ Description-Content-Type: text/markdown
42
+
43
+ # Grelmicro
44
+
45
+ Grelmicro is a tool-kit designed to streamline the development cloud-native applications with FastAPI.
@@ -0,0 +1,3 @@
1
+ # Grelmicro
2
+
3
+ Grelmicro is a tool-kit designed to streamline the development cloud-native applications with FastAPI.
@@ -0,0 +1,6 @@
1
+ # Leader Election
2
+
3
+ It is using a distributed lock in order to guarantee that only one worker is the leader at any given time.
4
+
5
+ It needs to be started as a background task that runs indefinitely and is responsible for acquiring and renewing
6
+ the distributed lock. The lock is automatically released when the task is cancelled or stopped.
@@ -0,0 +1,12 @@
1
+ # Lock
2
+
3
+ Specifications:
4
+ - Async: The lock must be acquired and released asynchronously.
5
+ - Distributed: The lock must be distributed across multiple workers.
6
+ - Reentrant: The lock must allow the same token to acquire it multiple times to extend the lease.
7
+ - Expiring: The lock must have a timeout to auto-release after an interval to prevent deadlocks.
8
+ - Non-blocking: Lock operations must not block the async event loop.
9
+ - Vendor-agnostic: Must support multiple backends (Redis, Postgres, ConfigMap, etc.).
10
+
11
+ Notes:
12
+ - Not Thread-Safe: This lock is not thread-safe. It is designed to be used in async tasks within the same event loop.
@@ -0,0 +1 @@
1
+ # Synchronization Primitives Module
@@ -0,0 +1 @@
1
+ # Task Module
@@ -0,0 +1,116 @@
1
+ """Example of a single file app with FastAPI, Grelmicro tasks, locks, and leader election."""
2
+
3
+ import time
4
+ from contextlib import asynccontextmanager
5
+ from typing import Annotated
6
+
7
+ import anyio
8
+ import typer
9
+ from fast_depends import Depends
10
+ from fastapi import FastAPI
11
+
12
+ from grelmicro.backends.memory import MemoryLockBackend
13
+ from grelmicro.sync.leaderelection import LeaderElection
14
+ from grelmicro.sync.lock import LeasedLock
15
+ from grelmicro.task import TaskManager
16
+
17
+ backend = MemoryLockBackend()
18
+ task = TaskManager()
19
+
20
+
21
+ @asynccontextmanager
22
+ async def lifespan(app):
23
+ async with backend, task:
24
+ typer.echo("App started")
25
+ yield
26
+ typer.echo("App stopped")
27
+
28
+
29
+ app = FastAPI(lifespan=lifespan)
30
+
31
+ leased_lock_10sec = LeasedLock(
32
+ name="leased_lock_10sec",
33
+ lease_duration=10,
34
+ backend=backend,
35
+ )
36
+ leased_lock_5sec = LeasedLock(
37
+ name="leased_lock_5sec",
38
+ lease_duration=5,
39
+ backend=backend,
40
+ )
41
+
42
+ leader_election = LeaderElection(name="simple-leader", backend=backend)
43
+
44
+ task.add_task(leader_election)
45
+
46
+
47
+ @task.interval(interval=1)
48
+ def sync_func_with_no_param():
49
+ typer.echo("sync_with_no_param")
50
+
51
+
52
+ @task.interval(interval=2)
53
+ async def async_func_with_no_param():
54
+ typer.echo("async_with_no_param")
55
+
56
+
57
+ def sync_dependency():
58
+ return "sync_dependency"
59
+
60
+
61
+ @task.interval(interval=3)
62
+ def sync_func_with_sync_dependency(
63
+ sync_dependency: Annotated[str, Depends(sync_dependency)],
64
+ ):
65
+ typer.echo(sync_dependency)
66
+
67
+
68
+ async def async_dependency():
69
+ yield "async_with_async_dependency"
70
+
71
+
72
+ @task.interval(interval=4)
73
+ async def async_func_with_async_dependency(
74
+ async_dependency: Annotated[str, Depends(async_dependency)],
75
+ ):
76
+ typer.echo(async_dependency)
77
+
78
+
79
+ @task.interval(interval=15, sync=leased_lock_10sec)
80
+ def sync_func_with_leased_lock_10sec():
81
+ typer.echo("sync_func_with_leased_lock_10sec")
82
+ time.sleep(9)
83
+
84
+
85
+ @task.interval(interval=15, sync=leased_lock_10sec)
86
+ async def async_func_with_leased_lock_10sec():
87
+ typer.echo("async_func_with_leased_lock_10sec")
88
+ await anyio.sleep(9)
89
+
90
+
91
+ @task.interval(interval=15, sync=leased_lock_5sec)
92
+ def sync_func_with_sync_dependency_and_leased_lock_5sec(
93
+ sync_dependency: Annotated[str, Depends(sync_dependency)],
94
+ ):
95
+ typer.echo(sync_dependency)
96
+ time.sleep(4)
97
+
98
+
99
+ @task.interval(interval=15, sync=leased_lock_5sec)
100
+ async def async_func_with_async_dependency_and_leased_lock_5sec(
101
+ async_dependency: Annotated[str, Depends(async_dependency)],
102
+ ):
103
+ typer.echo(async_dependency)
104
+ await anyio.sleep(4)
105
+
106
+
107
+ @task.interval(interval=15, sync=leader_election)
108
+ def sync_func_with_leader_election():
109
+ typer.echo("sync_func_with_leader_election")
110
+ time.sleep(30)
111
+
112
+
113
+ @task.interval(interval=15, sync=leader_election)
114
+ async def async_func_with_leader_election():
115
+ typer.echo("async_func_with_leader_election")
116
+ await anyio.sleep(30)
@@ -0,0 +1 @@
1
+ """Grelmicro Cloud-Native Toolkit."""
@@ -0,0 +1 @@
1
+ """Grelmicro Abstract Base Classes or Protocols Module."""
@@ -0,0 +1,95 @@
1
+ """Grelmicro Lock API."""
2
+
3
+ from types import TracebackType
4
+ from typing import Annotated, Protocol, Self
5
+ from uuid import UUID
6
+
7
+ from pydantic import BaseModel, ConfigDict
8
+ from typing_extensions import Doc
9
+
10
+ from grelmicro.abc.synchronization import Synchronization
11
+
12
+
13
+ class BaseLockConfig(BaseModel):
14
+ """Base Lock Config."""
15
+
16
+ model_config = ConfigDict(frozen=True, extra="forbid")
17
+
18
+ name: Annotated[
19
+ str,
20
+ Doc("The name of the resource to lock."),
21
+ ]
22
+ worker: Annotated[
23
+ str | UUID,
24
+ Doc("The worker identity.\n\nBy default, use a UUIDv1."),
25
+ ]
26
+
27
+
28
+ class BaseLock(Synchronization, Protocol):
29
+ """Base Lock Protocol."""
30
+
31
+ async def __aenter__(self) -> Self:
32
+ """Acquire the lock.
33
+
34
+ Raises:
35
+ LockAcquireError: If the lock cannot be acquired due to an error on the backend.
36
+ """
37
+ ...
38
+
39
+ async def __aexit__(
40
+ self,
41
+ exc_type: type[BaseException] | None,
42
+ exc_value: BaseException | None,
43
+ traceback: TracebackType | None,
44
+ ) -> None:
45
+ """Release the lock.
46
+
47
+ Raises:
48
+ LockNotOwnedError: If the lock is not owned by the current token.
49
+ LockReleaseError: If the lock cannot be released due to an error on the backend.
50
+
51
+ """
52
+ ...
53
+
54
+ @property
55
+ def config(self) -> BaseLockConfig:
56
+ """Return the config."""
57
+ ...
58
+
59
+ async def acquire(self) -> None:
60
+ """Acquire the lock.
61
+
62
+ Raises:
63
+ LockAcquireError: If the lock cannot be acquired due to an error on the backend.
64
+
65
+ """
66
+ ...
67
+
68
+ async def acquire_nowait(self) -> None:
69
+ """
70
+ Acquire the lock, without blocking.
71
+
72
+ Raises:
73
+ WouldBlock: If the lock cannot be acquired without blocking.
74
+ LockAcquireError: If the lock cannot be acquired due to an error on the backend.
75
+
76
+ """
77
+ ...
78
+
79
+ async def release(self) -> None:
80
+ """Release the lock.
81
+
82
+ Raises:
83
+ LockNotOwnedError: If the lock is not owned by the current token.
84
+ LockReleaseError: If the lock cannot be released due to an error on the backend.
85
+
86
+ """
87
+ ...
88
+
89
+ async def locked(self) -> bool:
90
+ """Check if the lock is currently held."""
91
+ ...
92
+
93
+ async def owned(self) -> bool:
94
+ """Check if the lock is currently held by the current token."""
95
+ ...
@@ -0,0 +1,84 @@
1
+ """Grelmicro Lock Backend API."""
2
+
3
+ from types import TracebackType
4
+ from typing import Protocol, Self
5
+
6
+
7
+ class LockBackend(Protocol):
8
+ """Lock Backend Protocol.
9
+
10
+ This is the low level API for the distributed lock backend that is platform agnostic.
11
+ """
12
+
13
+ async def __aenter__(self) -> Self:
14
+ """Open the lock backend."""
15
+ ...
16
+
17
+ async def __aexit__(
18
+ self,
19
+ exc_type: type[BaseException] | None,
20
+ exc_value: BaseException | None,
21
+ traceback: TracebackType | None,
22
+ ) -> None:
23
+ """Close the lock backend."""
24
+ ...
25
+
26
+ async def acquire(self, *, name: str, token: str, duration: float) -> bool:
27
+ """Acquire the lock.
28
+
29
+ Args:
30
+ name: The name of the lock.
31
+ token: The token to acquire the lock.
32
+ duration: The duration in seconds to hold the lock.
33
+
34
+ Returns:
35
+ True if the lock is acquired, False if the lock is already acquired by another token.
36
+
37
+ Raises:
38
+ Exception: Any exception can be raised if the lock cannot be acquired.
39
+ """
40
+ ...
41
+
42
+ async def release(self, *, name: str, token: str) -> bool:
43
+ """Release a lock.
44
+
45
+ Args:
46
+ name: The name of the lock.
47
+ token: The token to release the lock.
48
+
49
+ Returns:
50
+ True if the lock was released, False otherwise.
51
+
52
+ Raises:
53
+ Exception: Any exception can be raised if the lock cannot be released.
54
+ """
55
+ ...
56
+
57
+ async def locked(self, *, name: str) -> bool:
58
+ """Check if the lock is acquired.
59
+
60
+ Args:
61
+ name: The name of the lock.
62
+
63
+ Returns:
64
+ True if the lock is acquired, False otherwise.
65
+
66
+ Raises:
67
+ Exception: Any exception can be raised if the lock status cannot be checked.
68
+ """
69
+ ...
70
+
71
+ async def owned(self, *, name: str, token: str) -> bool:
72
+ """Check if the lock is owned.
73
+
74
+ Args:
75
+ name: The name of the lock.
76
+ token: The token to check.
77
+
78
+ Returns:
79
+ True if the lock is owned by the token, False otherwise.
80
+
81
+ Raises:
82
+ Exception: Any exception can be raised if the lock status cannot be checked.
83
+ """
84
+ ...
@@ -0,0 +1,21 @@
1
+ """Grelmicro Synchronization API."""
2
+
3
+ from types import TracebackType
4
+ from typing import Protocol, Self, runtime_checkable
5
+
6
+
7
+ @runtime_checkable
8
+ class Synchronization(Protocol):
9
+ """Synchronization Primitive Protocol."""
10
+
11
+ async def __aenter__(self) -> Self:
12
+ """Enter the synchronization primitive."""
13
+
14
+ async def __aexit__(
15
+ self,
16
+ exc_type: type[BaseException] | None,
17
+ exc_val: BaseException | None,
18
+ exc_tb: TracebackType | None,
19
+ ) -> bool | None:
20
+ """Exit the synchronization primitive."""
21
+ ...
@@ -0,0 +1,31 @@
1
+ """Task API."""
2
+
3
+ from typing import Protocol
4
+
5
+ from anyio import TASK_STATUS_IGNORED
6
+ from anyio.abc import TaskStatus
7
+ from typing_extensions import runtime_checkable
8
+
9
+
10
+ @runtime_checkable
11
+ class Task(Protocol):
12
+ """Task Protocol.
13
+
14
+ A task that runs in background in the async event loop.
15
+ """
16
+
17
+ @property
18
+ def name(self) -> str:
19
+ """Name to uniquely identify the task."""
20
+ ...
21
+
22
+ async def __call__(
23
+ self,
24
+ *,
25
+ task_status: TaskStatus[None] = TASK_STATUS_IGNORED,
26
+ ) -> None:
27
+ """Run the task.
28
+
29
+ This is the entry point of the task to be run in the async event loop.
30
+ """
31
+ ...
@@ -0,0 +1,5 @@
1
+ """Grelmicro Backends Module.
2
+
3
+ This module provides technology-agnostic, low-level implementations for various database and broker
4
+ systems.
5
+ """
@@ -0,0 +1,9 @@
1
+ """Memory Backend.
2
+
3
+ This backend is used for testing purposes and should not be used in production. Memory backends are
4
+ not persistent and are not shared between different workers.
5
+ """
6
+
7
+ from grelmicro.backends.memory.lock import MemoryLockBackend
8
+
9
+ __all__ = ["MemoryLockBackend"]
@@ -0,0 +1,72 @@
1
+ """Memory Lock Backend."""
2
+
3
+ from time import monotonic
4
+ from types import TracebackType
5
+ from typing import Annotated, Self
6
+
7
+ from typing_extensions import Doc
8
+
9
+ from grelmicro.abc.lockbackend import LockBackend
10
+ from grelmicro.backends.registry import loaded_backends
11
+
12
+
13
+ class MemoryLockBackend(LockBackend):
14
+ """Memory Lock Backend.
15
+
16
+ This is not a backend with a real distributed lock. It is a local lock that can be used for
17
+ testing purposes or for locking operations that are executed in the same AnyIO event loop.
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ *,
23
+ auto_register: Annotated[
24
+ bool,
25
+ Doc("Automatically register the lock backend in the backend registry."),
26
+ ] = True,
27
+ ) -> None:
28
+ """Initialize the lock backend."""
29
+ self._locks: dict[str, tuple[str | None, float]] = {}
30
+ if auto_register:
31
+ loaded_backends["lock"] = self
32
+
33
+ async def __aenter__(self) -> Self:
34
+ """Enter the lock backend."""
35
+ return self
36
+
37
+ async def __aexit__(
38
+ self,
39
+ exc_type: type[BaseException] | None,
40
+ exc_value: BaseException | None,
41
+ traceback: TracebackType | None,
42
+ ) -> None:
43
+ """Exit the lock backend."""
44
+ self._locks.clear()
45
+
46
+ async def acquire(self, *, name: str, token: str, duration: float) -> bool:
47
+ """Acquire the lock."""
48
+ current_token, expire_at = self._locks.get(name, (None, 0))
49
+ if current_token is None or current_token == token or expire_at < monotonic():
50
+ self._locks[name] = (token, monotonic() + duration)
51
+ return True
52
+ return False
53
+
54
+ async def release(self, *, name: str, token: str) -> bool:
55
+ """Release the lock."""
56
+ current_token, expire_at = self._locks.get(name, (None, 0))
57
+ if current_token == token and expire_at >= monotonic():
58
+ del self._locks[name]
59
+ return True
60
+ if current_token and expire_at < monotonic():
61
+ del self._locks[name]
62
+ return False
63
+
64
+ async def locked(self, *, name: str) -> bool:
65
+ """Check if the lock is acquired."""
66
+ current_token, expire_at = self._locks.get(name, (None, 0))
67
+ return current_token is not None and expire_at >= monotonic()
68
+
69
+ async def owned(self, *, name: str, token: str) -> bool:
70
+ """Check if the lock is owned."""
71
+ current_token, expire_at = self._locks.get(name, (None, 0))
72
+ return current_token == token and expire_at >= monotonic()
@@ -0,0 +1,8 @@
1
+ """PostgreSQL Backend.
2
+
3
+ To use this backend, please install `grelmicro[postgres]` or manually install `asyncpg`.
4
+ """
5
+
6
+ from grelmicro.backends.postgres.lock import PostgresLockBackend
7
+
8
+ __all__ = ["PostgresLockBackend"]