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.
- grelmicro-0.1.0/.gitignore +15 -0
- grelmicro-0.1.0/.pre-commit-config.yaml +52 -0
- grelmicro-0.1.0/.vscode/settings.json +37 -0
- grelmicro-0.1.0/PKG-INFO +45 -0
- grelmicro-0.1.0/README.md +3 -0
- grelmicro-0.1.0/docs/sync/leaderelection.md +6 -0
- grelmicro-0.1.0/docs/sync/lock.md +12 -0
- grelmicro-0.1.0/docs/sync.md +1 -0
- grelmicro-0.1.0/docs/task.md +1 -0
- grelmicro-0.1.0/examples/single_file_app.py +116 -0
- grelmicro-0.1.0/grelmicro/__init__.py +1 -0
- grelmicro-0.1.0/grelmicro/abc/__init__.py +1 -0
- grelmicro-0.1.0/grelmicro/abc/lock.py +95 -0
- grelmicro-0.1.0/grelmicro/abc/lockbackend.py +84 -0
- grelmicro-0.1.0/grelmicro/abc/synchronization.py +21 -0
- grelmicro-0.1.0/grelmicro/abc/task.py +31 -0
- grelmicro-0.1.0/grelmicro/backends/__init__.py +5 -0
- grelmicro-0.1.0/grelmicro/backends/memory/__init__.py +9 -0
- grelmicro-0.1.0/grelmicro/backends/memory/lock.py +72 -0
- grelmicro-0.1.0/grelmicro/backends/postgres/__init__.py +8 -0
- grelmicro-0.1.0/grelmicro/backends/postgres/lock.py +134 -0
- grelmicro-0.1.0/grelmicro/backends/redis/__init__.py +8 -0
- grelmicro-0.1.0/grelmicro/backends/redis/lock.py +77 -0
- grelmicro-0.1.0/grelmicro/backends/redis/pyproject.toml +147 -0
- grelmicro-0.1.0/grelmicro/backends/registry.py +30 -0
- grelmicro-0.1.0/grelmicro/errors.py +26 -0
- grelmicro-0.1.0/grelmicro/py.typed +0 -0
- grelmicro-0.1.0/grelmicro/sync/__init__.py +7 -0
- grelmicro-0.1.0/grelmicro/sync/_utils.py +34 -0
- grelmicro-0.1.0/grelmicro/sync/errors.py +49 -0
- grelmicro-0.1.0/grelmicro/sync/leaderelection.py +374 -0
- grelmicro-0.1.0/grelmicro/sync/lock.py +312 -0
- grelmicro-0.1.0/grelmicro/task/__init__.py +6 -0
- grelmicro-0.1.0/grelmicro/task/_interval.py +86 -0
- grelmicro-0.1.0/grelmicro/task/_utils.py +41 -0
- grelmicro-0.1.0/grelmicro/task/errors.py +25 -0
- grelmicro-0.1.0/grelmicro/task/manager.py +89 -0
- grelmicro-0.1.0/grelmicro/task/router.py +131 -0
- grelmicro-0.1.0/grelmicro/types.py +5 -0
- grelmicro-0.1.0/pyproject.toml +142 -0
- grelmicro-0.1.0/tests/__init__.py +1 -0
- grelmicro-0.1.0/tests/backends/__init__.py +1 -0
- grelmicro-0.1.0/tests/backends/test_lock.py +262 -0
- grelmicro-0.1.0/tests/backends/test_postgres.py +45 -0
- grelmicro-0.1.0/tests/backends/test_registry.py +72 -0
- grelmicro-0.1.0/tests/conftest.py +9 -0
- grelmicro-0.1.0/tests/sync/__init__.py +1 -0
- grelmicro-0.1.0/tests/sync/test_leaderelection.py +446 -0
- grelmicro-0.1.0/tests/sync/test_lock.py +496 -0
- grelmicro-0.1.0/tests/sync/utils.py +23 -0
- grelmicro-0.1.0/tests/task/__init__.py +1 -0
- grelmicro-0.1.0/tests/task/samples.py +86 -0
- grelmicro-0.1.0/tests/task/test_interval.py +123 -0
- grelmicro-0.1.0/tests/task/test_manager.py +81 -0
- grelmicro-0.1.0/tests/task/test_router.py +172 -0
- grelmicro-0.1.0/uv.lock +1058 -0
|
@@ -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
|
+
}
|
grelmicro-0.1.0/PKG-INFO
ADDED
|
@@ -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,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,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()
|