aailab-tools 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.
- aailab_tools-0.1.0/PKG-INFO +53 -0
- aailab_tools-0.1.0/README.md +33 -0
- aailab_tools-0.1.0/pyproject.toml +37 -0
- aailab_tools-0.1.0/setup.cfg +4 -0
- aailab_tools-0.1.0/src/aailab_tools/__init__.py +3 -0
- aailab_tools-0.1.0/src/aailab_tools/notify.py +94 -0
- aailab_tools-0.1.0/src/aailab_tools.egg-info/PKG-INFO +53 -0
- aailab_tools-0.1.0/src/aailab_tools.egg-info/SOURCES.txt +10 -0
- aailab_tools-0.1.0/src/aailab_tools.egg-info/dependency_links.txt +1 -0
- aailab_tools-0.1.0/src/aailab_tools.egg-info/requires.txt +1 -0
- aailab_tools-0.1.0/src/aailab_tools.egg-info/top_level.txt +1 -0
- aailab_tools-0.1.0/tests/test_notify.py +122 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aailab-tools
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Utilities for interacting with the AAILab blade dashboard.
|
|
5
|
+
Author: AAILab
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/vrbaj/aailab-tools
|
|
8
|
+
Keywords: decorator,notification,email,blade-dashboard
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: requests>=2.31.0
|
|
20
|
+
|
|
21
|
+
# aailab-tools
|
|
22
|
+
|
|
23
|
+
`aailab-tools` provides a `notify()` decorator that reports the completion status
|
|
24
|
+
of a function to the `blade_dashboard` notification API.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install aailab-tools
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
The decorator reads its configuration from environment variables:
|
|
35
|
+
|
|
36
|
+
- `SERVER_NOTIFY`: full notification endpoint URL, for example
|
|
37
|
+
`http://127.0.0.1:8000/api/notify`
|
|
38
|
+
- `API_TOKEN`: API token expected by `blade_dashboard server`
|
|
39
|
+
- `AAILAB_MACHINE_ID`: optional machine identifier; if omitted, hostname is used
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from aailab_tools import notify
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@notify("user@example.com", "admin@example.com")
|
|
48
|
+
def run_job():
|
|
49
|
+
return 42
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
On success, the server receives status `0`. If the function raises an exception,
|
|
53
|
+
the exception text is sent as the status and the exception is re-raised.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# aailab-tools
|
|
2
|
+
|
|
3
|
+
`aailab-tools` provides a `notify()` decorator that reports the completion status
|
|
4
|
+
of a function to the `blade_dashboard` notification API.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install aailab-tools
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Configuration
|
|
13
|
+
|
|
14
|
+
The decorator reads its configuration from environment variables:
|
|
15
|
+
|
|
16
|
+
- `SERVER_NOTIFY`: full notification endpoint URL, for example
|
|
17
|
+
`http://127.0.0.1:8000/api/notify`
|
|
18
|
+
- `API_TOKEN`: API token expected by `blade_dashboard server`
|
|
19
|
+
- `AAILAB_MACHINE_ID`: optional machine identifier; if omitted, hostname is used
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from aailab_tools import notify
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@notify("user@example.com", "admin@example.com")
|
|
28
|
+
def run_job():
|
|
29
|
+
return 42
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
On success, the server receives status `0`. If the function raises an exception,
|
|
33
|
+
the exception text is sent as the status and the exception is re-raised.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aailab-tools"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Utilities for interacting with the AAILab blade dashboard."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"requests>=2.31.0",
|
|
13
|
+
]
|
|
14
|
+
authors = [
|
|
15
|
+
{ name = "AAILab" },
|
|
16
|
+
]
|
|
17
|
+
license = { text = "MIT" }
|
|
18
|
+
keywords = ["decorator", "notification", "email", "blade-dashboard"]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 3 - Alpha",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Programming Language :: Python :: 3.14",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/vrbaj/aailab-tools"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools]
|
|
34
|
+
package-dir = {"" = "src"}
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
where = ["src"]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import inspect
|
|
5
|
+
import os
|
|
6
|
+
import socket
|
|
7
|
+
from typing import Any, Callable, TypeVar, cast
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NotificationConfigurationError(RuntimeError):
|
|
15
|
+
"""Raised when required notification configuration is missing."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _machine_id() -> str:
|
|
19
|
+
return os.environ.get("AAILAB_MACHINE_ID") or socket.gethostname()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _validate_emails(emails: tuple[str, ...]) -> list[str]:
|
|
23
|
+
cleaned = [email.strip() for email in emails if email.strip()]
|
|
24
|
+
if not cleaned:
|
|
25
|
+
raise ValueError("notify() requires at least one email address")
|
|
26
|
+
|
|
27
|
+
invalid = [email for email in cleaned if "@" not in email]
|
|
28
|
+
if invalid:
|
|
29
|
+
raise ValueError(f"invalid email address(es): {', '.join(invalid)}")
|
|
30
|
+
|
|
31
|
+
return cleaned
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _notify_server(task_name: str, status: int | str, emails: list[str]) -> None:
|
|
35
|
+
endpoint = os.environ.get("SERVER_NOTIFY", "").strip()
|
|
36
|
+
api_token = os.environ.get("API_TOKEN", "").strip()
|
|
37
|
+
|
|
38
|
+
if not endpoint:
|
|
39
|
+
raise NotificationConfigurationError("SERVER_NOTIFY is not configured")
|
|
40
|
+
if not api_token:
|
|
41
|
+
raise NotificationConfigurationError("API_TOKEN is not configured")
|
|
42
|
+
|
|
43
|
+
response = requests.post(
|
|
44
|
+
endpoint,
|
|
45
|
+
headers={
|
|
46
|
+
"Content-Type": "application/json",
|
|
47
|
+
"X-API-TOKEN": api_token,
|
|
48
|
+
},
|
|
49
|
+
json={
|
|
50
|
+
"machine_id": _machine_id(),
|
|
51
|
+
"task_name": task_name,
|
|
52
|
+
"status": status,
|
|
53
|
+
"emails": emails,
|
|
54
|
+
},
|
|
55
|
+
timeout=15,
|
|
56
|
+
)
|
|
57
|
+
response.raise_for_status()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def notify(*emails: str) -> Callable[[F], F]:
|
|
61
|
+
"""Notify the blade_dashboard server when the decorated function finishes."""
|
|
62
|
+
|
|
63
|
+
recipients = _validate_emails(emails)
|
|
64
|
+
|
|
65
|
+
def decorator(func: F) -> F:
|
|
66
|
+
task_name = func.__qualname__
|
|
67
|
+
|
|
68
|
+
@functools.wraps(func)
|
|
69
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
70
|
+
try:
|
|
71
|
+
result = func(*args, **kwargs)
|
|
72
|
+
except Exception as exc:
|
|
73
|
+
_notify_server(task_name=task_name, status=str(exc), emails=recipients)
|
|
74
|
+
raise
|
|
75
|
+
|
|
76
|
+
_notify_server(task_name=task_name, status=0, emails=recipients)
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
@functools.wraps(func)
|
|
80
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
81
|
+
try:
|
|
82
|
+
result = await cast(Any, func)(*args, **kwargs)
|
|
83
|
+
except Exception as exc:
|
|
84
|
+
_notify_server(task_name=task_name, status=str(exc), emails=recipients)
|
|
85
|
+
raise
|
|
86
|
+
|
|
87
|
+
_notify_server(task_name=task_name, status=0, emails=recipients)
|
|
88
|
+
return result
|
|
89
|
+
|
|
90
|
+
if inspect.iscoroutinefunction(func):
|
|
91
|
+
return cast(F, async_wrapper)
|
|
92
|
+
return cast(F, sync_wrapper)
|
|
93
|
+
|
|
94
|
+
return decorator
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aailab-tools
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Utilities for interacting with the AAILab blade dashboard.
|
|
5
|
+
Author: AAILab
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/vrbaj/aailab-tools
|
|
8
|
+
Keywords: decorator,notification,email,blade-dashboard
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: requests>=2.31.0
|
|
20
|
+
|
|
21
|
+
# aailab-tools
|
|
22
|
+
|
|
23
|
+
`aailab-tools` provides a `notify()` decorator that reports the completion status
|
|
24
|
+
of a function to the `blade_dashboard` notification API.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install aailab-tools
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
The decorator reads its configuration from environment variables:
|
|
35
|
+
|
|
36
|
+
- `SERVER_NOTIFY`: full notification endpoint URL, for example
|
|
37
|
+
`http://127.0.0.1:8000/api/notify`
|
|
38
|
+
- `API_TOKEN`: API token expected by `blade_dashboard server`
|
|
39
|
+
- `AAILAB_MACHINE_ID`: optional machine identifier; if omitted, hostname is used
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from aailab_tools import notify
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@notify("user@example.com", "admin@example.com")
|
|
48
|
+
def run_job():
|
|
49
|
+
return 42
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
On success, the server receives status `0`. If the function raises an exception,
|
|
53
|
+
the exception text is sent as the status and the exception is re-raised.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/aailab_tools/__init__.py
|
|
4
|
+
src/aailab_tools/notify.py
|
|
5
|
+
src/aailab_tools.egg-info/PKG-INFO
|
|
6
|
+
src/aailab_tools.egg-info/SOURCES.txt
|
|
7
|
+
src/aailab_tools.egg-info/dependency_links.txt
|
|
8
|
+
src/aailab_tools.egg-info/requires.txt
|
|
9
|
+
src/aailab_tools.egg-info/top_level.txt
|
|
10
|
+
tests/test_notify.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.31.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aailab_tools
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
from aailab_tools import notify
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class NotifyTests(unittest.TestCase):
|
|
8
|
+
def setUp(self):
|
|
9
|
+
self._env = {}
|
|
10
|
+
|
|
11
|
+
def tearDown(self):
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
for key, previous in self._env.items():
|
|
15
|
+
if previous is None:
|
|
16
|
+
os.environ.pop(key, None)
|
|
17
|
+
else:
|
|
18
|
+
os.environ[key] = previous
|
|
19
|
+
|
|
20
|
+
def set_env(self, key, value):
|
|
21
|
+
import os
|
|
22
|
+
|
|
23
|
+
if key not in self._env:
|
|
24
|
+
self._env[key] = os.environ.get(key)
|
|
25
|
+
os.environ[key] = value
|
|
26
|
+
|
|
27
|
+
def patch_post(self, fake_post):
|
|
28
|
+
from unittest.mock import patch
|
|
29
|
+
|
|
30
|
+
return patch("aailab_tools.notify.requests.post", fake_post)
|
|
31
|
+
|
|
32
|
+
def test_notify_sends_success_payload(self):
|
|
33
|
+
captured = {}
|
|
34
|
+
|
|
35
|
+
class FakeResponse:
|
|
36
|
+
def raise_for_status(self):
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
def fake_post(url, headers, json, timeout):
|
|
40
|
+
captured["url"] = url
|
|
41
|
+
captured["headers"] = headers
|
|
42
|
+
captured["json"] = json
|
|
43
|
+
captured["timeout"] = timeout
|
|
44
|
+
return FakeResponse()
|
|
45
|
+
|
|
46
|
+
self.set_env("SERVER_NOTIFY", "http://example.test/api/notify")
|
|
47
|
+
self.set_env("API_TOKEN", "secret-token")
|
|
48
|
+
self.set_env("AAILAB_MACHINE_ID", "blade-01")
|
|
49
|
+
|
|
50
|
+
with self.patch_post(fake_post):
|
|
51
|
+
@notify("user@example.com")
|
|
52
|
+
def compute():
|
|
53
|
+
return 123
|
|
54
|
+
|
|
55
|
+
self.assertEqual(compute(), 123)
|
|
56
|
+
|
|
57
|
+
self.assertEqual(captured["url"], "http://example.test/api/notify")
|
|
58
|
+
self.assertEqual(captured["headers"]["X-API-TOKEN"], "secret-token")
|
|
59
|
+
self.assertEqual(
|
|
60
|
+
captured["json"],
|
|
61
|
+
{
|
|
62
|
+
"machine_id": "blade-01",
|
|
63
|
+
"task_name": "NotifyTests.test_notify_sends_success_payload.<locals>.compute",
|
|
64
|
+
"status": 0,
|
|
65
|
+
"emails": ["user@example.com"],
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
self.assertEqual(captured["timeout"], 15)
|
|
69
|
+
|
|
70
|
+
def test_notify_sends_exception_text_and_reraises(self):
|
|
71
|
+
captured = {}
|
|
72
|
+
|
|
73
|
+
class FakeResponse:
|
|
74
|
+
def raise_for_status(self):
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
def fake_post(url, headers, json, timeout):
|
|
78
|
+
captured["json"] = json
|
|
79
|
+
return FakeResponse()
|
|
80
|
+
|
|
81
|
+
self.set_env("SERVER_NOTIFY", "http://example.test/api/notify")
|
|
82
|
+
self.set_env("API_TOKEN", "secret-token")
|
|
83
|
+
self.set_env("AAILAB_MACHINE_ID", "blade-01")
|
|
84
|
+
|
|
85
|
+
with self.patch_post(fake_post):
|
|
86
|
+
@notify("user@example.com")
|
|
87
|
+
def compute():
|
|
88
|
+
raise RuntimeError("boom")
|
|
89
|
+
|
|
90
|
+
with self.assertRaisesRegex(RuntimeError, "boom"):
|
|
91
|
+
compute()
|
|
92
|
+
|
|
93
|
+
self.assertEqual(captured["json"]["status"], "boom")
|
|
94
|
+
self.assertEqual(captured["json"]["emails"], ["user@example.com"])
|
|
95
|
+
|
|
96
|
+
def test_notify_supports_async_functions(self):
|
|
97
|
+
captured = {}
|
|
98
|
+
|
|
99
|
+
class FakeResponse:
|
|
100
|
+
def raise_for_status(self):
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
def fake_post(url, headers, json, timeout):
|
|
104
|
+
captured["json"] = json
|
|
105
|
+
return FakeResponse()
|
|
106
|
+
|
|
107
|
+
self.set_env("SERVER_NOTIFY", "http://example.test/api/notify")
|
|
108
|
+
self.set_env("API_TOKEN", "secret-token")
|
|
109
|
+
self.set_env("AAILAB_MACHINE_ID", "blade-01")
|
|
110
|
+
|
|
111
|
+
with self.patch_post(fake_post):
|
|
112
|
+
@notify("user@example.com")
|
|
113
|
+
async def compute():
|
|
114
|
+
return "ok"
|
|
115
|
+
|
|
116
|
+
self.assertEqual(asyncio.run(compute()), "ok")
|
|
117
|
+
|
|
118
|
+
self.assertEqual(captured["json"]["status"], 0)
|
|
119
|
+
|
|
120
|
+
def test_notify_rejects_invalid_emails(self):
|
|
121
|
+
with self.assertRaisesRegex(ValueError, "invalid email"):
|
|
122
|
+
notify("invalid-email")
|