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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from .notify import notify
2
+
3
+ __all__ = ["notify"]
@@ -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
+ 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")