Implement IronNotify Python SDK
- NotifyClient with simple notify() and event builder APIs - Full async/await support with httpx - Offline queue with JSON file persistence - Event builder with fluent API - Notification management (get, markAsRead, unreadCount) - Type hints with dataclasses - Python 3.8+ compatible
This commit is contained in:
parent
abb0dff3ef
commit
5977510568
|
|
@ -0,0 +1,36 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Distribution / packaging
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
*.egg
|
||||
|
||||
# Virtual environments
|
||||
.venv/
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# Type checking
|
||||
.mypy_cache/
|
||||
|
||||
# Local storage
|
||||
.ironnotify/
|
||||
292
README.md
292
README.md
|
|
@ -1,2 +1,290 @@
|
|||
# ironnotify-python
|
||||
IronNotify SDK for Python - Event notifications and alerts
|
||||
# IronNotify SDK for Python
|
||||
|
||||
Event notifications and alerts SDK for Python applications. Send notifications, receive real-time updates, and manage notification state.
|
||||
|
||||
[](https://pypi.org/project/ironnotify/)
|
||||
[](https://pypi.org/project/ironnotify/)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install ironnotify
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Send a Simple Notification
|
||||
|
||||
```python
|
||||
import ironnotify
|
||||
|
||||
# Initialize
|
||||
ironnotify.init("ak_live_xxxxx")
|
||||
|
||||
# Send a simple notification
|
||||
ironnotify.notify(
|
||||
"order.created",
|
||||
"New Order Received",
|
||||
message="Order #1234 has been placed",
|
||||
severity="success",
|
||||
metadata={"order_id": "1234", "amount": 99.99},
|
||||
)
|
||||
```
|
||||
|
||||
### Fluent Event Builder
|
||||
|
||||
```python
|
||||
import ironnotify
|
||||
|
||||
ironnotify.init("ak_live_xxxxx")
|
||||
|
||||
# Build complex notifications with the fluent API
|
||||
result = ironnotify.event("payment.failed") \
|
||||
.with_title("Payment Failed") \
|
||||
.with_message("Payment could not be processed") \
|
||||
.with_severity("error") \
|
||||
.with_metadata({"order_id": "1234", "reason": "Card declined"}) \
|
||||
.with_action("Retry Payment", url="/orders/1234/retry", style="primary") \
|
||||
.with_action("Contact Support", action="open_support") \
|
||||
.for_user("user-123") \
|
||||
.with_deduplication_key("payment-failed-1234") \
|
||||
.expires_in(86400) # 24 hours \
|
||||
.send()
|
||||
```
|
||||
|
||||
### Async Support
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import ironnotify
|
||||
|
||||
async def main():
|
||||
ironnotify.init("ak_live_xxxxx")
|
||||
|
||||
# Async notification
|
||||
result = await ironnotify.notify_async(
|
||||
"user.signup",
|
||||
"New User Registered",
|
||||
severity="success",
|
||||
)
|
||||
|
||||
# Async event builder
|
||||
result = await ironnotify.event("order.shipped") \
|
||||
.with_title("Order Shipped") \
|
||||
.for_user("user-123") \
|
||||
.send_async()
|
||||
|
||||
# Get notifications async
|
||||
notifications = await ironnotify.get_notifications_async(limit=10)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### Using the Client Directly
|
||||
|
||||
```python
|
||||
from ironnotify import NotifyClient, NotifyOptions
|
||||
|
||||
client = NotifyClient(NotifyOptions(
|
||||
api_key="ak_live_xxxxx",
|
||||
debug=True,
|
||||
))
|
||||
|
||||
# Send notification
|
||||
result = client.notify("event.type", "Title")
|
||||
|
||||
# Use event builder
|
||||
result = client.event("event.type") \
|
||||
.with_title("Title") \
|
||||
.send()
|
||||
|
||||
# Clean up
|
||||
client.close()
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
```python
|
||||
from ironnotify import NotifyOptions, NotifyClient
|
||||
|
||||
options = NotifyOptions(
|
||||
api_key="ak_live_xxxxx",
|
||||
api_base_url="https://api.ironnotify.com",
|
||||
ws_url="wss://ws.ironnotify.com",
|
||||
debug=False,
|
||||
enable_offline_queue=True,
|
||||
max_offline_queue_size=100,
|
||||
auto_reconnect=True,
|
||||
max_reconnect_attempts=5,
|
||||
reconnect_delay=1.0,
|
||||
)
|
||||
|
||||
client = NotifyClient(options)
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `api_key` | str | required | Your API key (ak_live_xxx or ak_test_xxx) |
|
||||
| `api_base_url` | str | 'https://api.ironnotify.com' | API base URL |
|
||||
| `ws_url` | str | 'wss://ws.ironnotify.com' | WebSocket URL |
|
||||
| `debug` | bool | False | Enable debug logging |
|
||||
| `enable_offline_queue` | bool | True | Queue notifications when offline |
|
||||
| `max_offline_queue_size` | int | 100 | Max offline queue size |
|
||||
| `auto_reconnect` | bool | True | Auto-reconnect WebSocket |
|
||||
| `max_reconnect_attempts` | int | 5 | Max reconnection attempts |
|
||||
| `reconnect_delay` | float | 1.0 | Base reconnection delay (seconds) |
|
||||
|
||||
## Features
|
||||
|
||||
- **Simple Notifications**: Send notifications with one line of code
|
||||
- **Fluent Builder**: Build complex notifications with an intuitive API
|
||||
- **Async Support**: Full async/await support for all operations
|
||||
- **Offline Queue**: Notifications are queued when offline
|
||||
- **Type Hints**: Full type annotations for IDE support
|
||||
- **Dataclasses**: Clean data models using Python dataclasses
|
||||
|
||||
## Notification Options
|
||||
|
||||
### Severity Levels
|
||||
|
||||
```python
|
||||
from ironnotify import SeverityLevel
|
||||
|
||||
# Available levels: "info", "success", "warning", "error", "critical"
|
||||
ironnotify.notify("alert", "System Alert", severity="critical")
|
||||
```
|
||||
|
||||
### Actions
|
||||
|
||||
```python
|
||||
ironnotify.event("order.shipped") \
|
||||
.with_title("Order Shipped") \
|
||||
.with_action("Track Package", url="https://tracking.example.com/123", style="primary") \
|
||||
.with_action("View Order", action="view_order") \
|
||||
.send()
|
||||
```
|
||||
|
||||
### Deduplication
|
||||
|
||||
Prevent duplicate notifications:
|
||||
|
||||
```python
|
||||
ironnotify.event("reminder") \
|
||||
.with_title("Daily Reminder") \
|
||||
.with_deduplication_key("daily-reminder-2024-01-15") \
|
||||
.send()
|
||||
```
|
||||
|
||||
### Grouping
|
||||
|
||||
Group related notifications:
|
||||
|
||||
```python
|
||||
ironnotify.event("comment.new") \
|
||||
.with_title("New Comment") \
|
||||
.with_group_key("post-123-comments") \
|
||||
.send()
|
||||
```
|
||||
|
||||
### Expiration
|
||||
|
||||
```python
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Expires in 1 hour
|
||||
ironnotify.event("flash_sale") \
|
||||
.with_title("Flash Sale!") \
|
||||
.expires_in(3600) \
|
||||
.send()
|
||||
|
||||
# Expires at specific time
|
||||
ironnotify.event("event_reminder") \
|
||||
.with_title("Event Tomorrow") \
|
||||
.expires_on(datetime.utcnow() + timedelta(days=1)) \
|
||||
.send()
|
||||
```
|
||||
|
||||
## Managing Notifications
|
||||
|
||||
### Get Notifications
|
||||
|
||||
```python
|
||||
# Get all notifications
|
||||
notifications = ironnotify.get_notifications()
|
||||
|
||||
# With options
|
||||
unread = ironnotify.get_notifications(limit=10, offset=0, unread_only=True)
|
||||
|
||||
# Async
|
||||
notifications = await ironnotify.get_notifications_async()
|
||||
```
|
||||
|
||||
### Mark as Read
|
||||
|
||||
```python
|
||||
# Mark single notification
|
||||
client.mark_as_read("notification-id")
|
||||
|
||||
# Mark all as read
|
||||
client.mark_all_as_read()
|
||||
```
|
||||
|
||||
### Get Unread Count
|
||||
|
||||
```python
|
||||
count = client.get_unread_count()
|
||||
print(f"You have {count} unread notifications")
|
||||
```
|
||||
|
||||
## Real-Time Notifications
|
||||
|
||||
```python
|
||||
import ironnotify
|
||||
|
||||
ironnotify.init("ak_live_xxxxx")
|
||||
|
||||
def handle_notification(notification):
|
||||
print(f"New notification: {notification.title}")
|
||||
|
||||
ironnotify.on_notification(handle_notification)
|
||||
ironnotify.connect()
|
||||
ironnotify.subscribe_to_user("user-123")
|
||||
ironnotify.subscribe_to_app()
|
||||
```
|
||||
|
||||
## Offline Support
|
||||
|
||||
Notifications are automatically queued when offline:
|
||||
|
||||
```python
|
||||
import ironnotify
|
||||
|
||||
ironnotify.init("ak_live_xxxxx")
|
||||
|
||||
# This will be queued if offline
|
||||
ironnotify.notify("event", "Title")
|
||||
|
||||
# Manually flush the queue
|
||||
ironnotify.flush()
|
||||
|
||||
# Or async
|
||||
await ironnotify.flush_async()
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.8+
|
||||
- httpx
|
||||
- websockets (for real-time features)
|
||||
|
||||
## Links
|
||||
|
||||
- [Documentation](https://www.ironnotify.com/docs)
|
||||
- [Dashboard](https://www.ironnotify.com)
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see [LICENSE](LICENSE) for details.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "ironnotify"
|
||||
version = "0.1.0"
|
||||
description = "IronNotify SDK for Python - Event notifications and alerts"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
requires-python = ">=3.8"
|
||||
authors = [
|
||||
{ name = "IronServices", email = "support@ironservices.com" }
|
||||
]
|
||||
keywords = ["ironnotify", "notifications", "alerts", "events", "real-time", "websocket"]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
]
|
||||
dependencies = [
|
||||
"httpx>=0.25.0",
|
||||
"websockets>=12.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=7.0.0",
|
||||
"pytest-asyncio>=0.21.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://www.ironnotify.com"
|
||||
Documentation = "https://www.ironnotify.com/docs"
|
||||
Repository = "https://github.com/IronServices/ironnotify-python"
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
include = ["/src"]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/ironnotify"]
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
"""
|
||||
IronNotify SDK for Python - Event notifications and alerts.
|
||||
|
||||
Example usage:
|
||||
import ironnotify
|
||||
|
||||
# Initialize
|
||||
ironnotify.init("ak_live_xxxxx")
|
||||
|
||||
# Send a notification
|
||||
ironnotify.notify("order.created", "New Order Received")
|
||||
|
||||
# Use the event builder
|
||||
await ironnotify.event("payment.failed") \\
|
||||
.with_title("Payment Failed") \\
|
||||
.with_message("Payment could not be processed") \\
|
||||
.with_severity("error") \\
|
||||
.send()
|
||||
"""
|
||||
|
||||
from .types import (
|
||||
SeverityLevel,
|
||||
NotificationAction,
|
||||
NotificationPayload,
|
||||
Notification,
|
||||
SendResult,
|
||||
NotifyOptions,
|
||||
ConnectionState,
|
||||
)
|
||||
from .client import NotifyClient
|
||||
from .builder import EventBuilder
|
||||
|
||||
__version__ = "0.1.0"
|
||||
__all__ = [
|
||||
# Types
|
||||
"SeverityLevel",
|
||||
"NotificationAction",
|
||||
"NotificationPayload",
|
||||
"Notification",
|
||||
"SendResult",
|
||||
"NotifyOptions",
|
||||
"ConnectionState",
|
||||
# Classes
|
||||
"NotifyClient",
|
||||
"EventBuilder",
|
||||
# Functions
|
||||
"init",
|
||||
"get_client",
|
||||
"notify",
|
||||
"notify_async",
|
||||
"event",
|
||||
"connect",
|
||||
"disconnect",
|
||||
"subscribe_to_user",
|
||||
"subscribe_to_app",
|
||||
"on_notification",
|
||||
"get_notifications",
|
||||
"get_notifications_async",
|
||||
"flush",
|
||||
"flush_async",
|
||||
"close",
|
||||
]
|
||||
|
||||
# Global client instance
|
||||
_client: NotifyClient | None = None
|
||||
|
||||
|
||||
def init(api_key: str, **options) -> NotifyClient:
|
||||
"""Initialize the global NotifyClient."""
|
||||
global _client
|
||||
opts = NotifyOptions(api_key=api_key, **options)
|
||||
_client = NotifyClient(opts)
|
||||
return _client
|
||||
|
||||
|
||||
def get_client() -> NotifyClient | None:
|
||||
"""Get the global NotifyClient."""
|
||||
return _client
|
||||
|
||||
|
||||
def notify(
|
||||
event_type: str,
|
||||
title: str,
|
||||
message: str | None = None,
|
||||
severity: SeverityLevel = "info",
|
||||
metadata: dict | None = None,
|
||||
user_id: str | None = None,
|
||||
) -> SendResult:
|
||||
"""Send a notification using the global client (sync)."""
|
||||
if _client is None:
|
||||
raise RuntimeError("IronNotify not initialized. Call init() first.")
|
||||
return _client.notify(event_type, title, message, severity, metadata, user_id)
|
||||
|
||||
|
||||
async def notify_async(
|
||||
event_type: str,
|
||||
title: str,
|
||||
message: str | None = None,
|
||||
severity: SeverityLevel = "info",
|
||||
metadata: dict | None = None,
|
||||
user_id: str | None = None,
|
||||
) -> SendResult:
|
||||
"""Send a notification using the global client (async)."""
|
||||
if _client is None:
|
||||
raise RuntimeError("IronNotify not initialized. Call init() first.")
|
||||
return await _client.notify_async(event_type, title, message, severity, metadata, user_id)
|
||||
|
||||
|
||||
def event(event_type: str) -> EventBuilder:
|
||||
"""Create an event builder using the global client."""
|
||||
if _client is None:
|
||||
raise RuntimeError("IronNotify not initialized. Call init() first.")
|
||||
return _client.event(event_type)
|
||||
|
||||
|
||||
def connect() -> None:
|
||||
"""Connect to real-time notifications."""
|
||||
if _client is None:
|
||||
raise RuntimeError("IronNotify not initialized. Call init() first.")
|
||||
_client.connect()
|
||||
|
||||
|
||||
def disconnect() -> None:
|
||||
"""Disconnect from real-time notifications."""
|
||||
if _client is None:
|
||||
raise RuntimeError("IronNotify not initialized. Call init() first.")
|
||||
_client.disconnect()
|
||||
|
||||
|
||||
def subscribe_to_user(user_id: str) -> None:
|
||||
"""Subscribe to a user's notifications."""
|
||||
if _client is None:
|
||||
raise RuntimeError("IronNotify not initialized. Call init() first.")
|
||||
_client.subscribe_to_user(user_id)
|
||||
|
||||
|
||||
def subscribe_to_app() -> None:
|
||||
"""Subscribe to app-wide notifications."""
|
||||
if _client is None:
|
||||
raise RuntimeError("IronNotify not initialized. Call init() first.")
|
||||
_client.subscribe_to_app()
|
||||
|
||||
|
||||
def on_notification(handler) -> None:
|
||||
"""Set notification handler."""
|
||||
if _client is None:
|
||||
raise RuntimeError("IronNotify not initialized. Call init() first.")
|
||||
_client.on_notification(handler)
|
||||
|
||||
|
||||
def get_notifications(
|
||||
limit: int | None = None,
|
||||
offset: int | None = None,
|
||||
unread_only: bool = False,
|
||||
) -> list[Notification]:
|
||||
"""Get notifications (sync)."""
|
||||
if _client is None:
|
||||
raise RuntimeError("IronNotify not initialized. Call init() first.")
|
||||
return _client.get_notifications(limit, offset, unread_only)
|
||||
|
||||
|
||||
async def get_notifications_async(
|
||||
limit: int | None = None,
|
||||
offset: int | None = None,
|
||||
unread_only: bool = False,
|
||||
) -> list[Notification]:
|
||||
"""Get notifications (async)."""
|
||||
if _client is None:
|
||||
raise RuntimeError("IronNotify not initialized. Call init() first.")
|
||||
return await _client.get_notifications_async(limit, offset, unread_only)
|
||||
|
||||
|
||||
def flush() -> None:
|
||||
"""Flush the offline queue (sync)."""
|
||||
if _client is None:
|
||||
raise RuntimeError("IronNotify not initialized. Call init() first.")
|
||||
_client.flush()
|
||||
|
||||
|
||||
async def flush_async() -> None:
|
||||
"""Flush the offline queue (async)."""
|
||||
if _client is None:
|
||||
raise RuntimeError("IronNotify not initialized. Call init() first.")
|
||||
await _client.flush_async()
|
||||
|
||||
|
||||
def close() -> None:
|
||||
"""Close the global client."""
|
||||
global _client
|
||||
if _client is not None:
|
||||
_client.close()
|
||||
_client = None
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
"""Event builder for IronNotify SDK."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
from .types import (
|
||||
SeverityLevel,
|
||||
NotificationAction,
|
||||
NotificationPayload,
|
||||
SendResult,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .client import NotifyClient
|
||||
|
||||
|
||||
class EventBuilder:
|
||||
"""Builder for creating notifications with a fluent API."""
|
||||
|
||||
def __init__(self, client: "NotifyClient", event_type: str):
|
||||
self._client = client
|
||||
self._event_type = event_type
|
||||
self._title: str = ""
|
||||
self._message: str | None = None
|
||||
self._severity: SeverityLevel = "info"
|
||||
self._metadata: dict[str, Any] = {}
|
||||
self._actions: list[NotificationAction] = []
|
||||
self._user_id: str | None = None
|
||||
self._group_key: str | None = None
|
||||
self._deduplication_key: str | None = None
|
||||
self._expires_at: datetime | None = None
|
||||
|
||||
def with_title(self, title: str) -> "EventBuilder":
|
||||
"""Set the notification title."""
|
||||
self._title = title
|
||||
return self
|
||||
|
||||
def with_message(self, message: str) -> "EventBuilder":
|
||||
"""Set the notification message."""
|
||||
self._message = message
|
||||
return self
|
||||
|
||||
def with_severity(self, severity: SeverityLevel) -> "EventBuilder":
|
||||
"""Set the severity level."""
|
||||
self._severity = severity
|
||||
return self
|
||||
|
||||
def with_metadata(self, metadata: dict[str, Any]) -> "EventBuilder":
|
||||
"""Set metadata for the notification."""
|
||||
self._metadata.update(metadata)
|
||||
return self
|
||||
|
||||
def with_action(
|
||||
self,
|
||||
label: str,
|
||||
url: str | None = None,
|
||||
action: str | None = None,
|
||||
style: str = "default",
|
||||
) -> "EventBuilder":
|
||||
"""Add an action button to the notification."""
|
||||
self._actions.append(
|
||||
NotificationAction(label=label, url=url, action=action, style=style)
|
||||
)
|
||||
return self
|
||||
|
||||
def for_user(self, user_id: str) -> "EventBuilder":
|
||||
"""Set the target user ID."""
|
||||
self._user_id = user_id
|
||||
return self
|
||||
|
||||
def with_group_key(self, group_key: str) -> "EventBuilder":
|
||||
"""Set the group key for grouping related notifications."""
|
||||
self._group_key = group_key
|
||||
return self
|
||||
|
||||
def with_deduplication_key(self, key: str) -> "EventBuilder":
|
||||
"""Set the deduplication key to prevent duplicate notifications."""
|
||||
self._deduplication_key = key
|
||||
return self
|
||||
|
||||
def expires_in(self, seconds: float) -> "EventBuilder":
|
||||
"""Set the expiration time (from now) for the notification."""
|
||||
self._expires_at = datetime.utcnow() + timedelta(seconds=seconds)
|
||||
return self
|
||||
|
||||
def expires_on(self, date: datetime) -> "EventBuilder":
|
||||
"""Set the expiration date for the notification."""
|
||||
self._expires_at = date
|
||||
return self
|
||||
|
||||
def build(self) -> NotificationPayload:
|
||||
"""Build the notification payload."""
|
||||
if not self._title:
|
||||
raise ValueError("Notification title is required")
|
||||
|
||||
return NotificationPayload(
|
||||
event_type=self._event_type,
|
||||
title=self._title,
|
||||
message=self._message,
|
||||
severity=self._severity,
|
||||
metadata=self._metadata if self._metadata else None,
|
||||
actions=self._actions if self._actions else None,
|
||||
user_id=self._user_id,
|
||||
group_key=self._group_key,
|
||||
deduplication_key=self._deduplication_key,
|
||||
expires_at=self._expires_at,
|
||||
)
|
||||
|
||||
def send(self) -> SendResult:
|
||||
"""Send the notification (sync)."""
|
||||
payload = self.build()
|
||||
return self._client.send_payload(payload)
|
||||
|
||||
async def send_async(self) -> SendResult:
|
||||
"""Send the notification (async)."""
|
||||
payload = self.build()
|
||||
return await self._client.send_payload_async(payload)
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
"""Main client for IronNotify SDK."""
|
||||
|
||||
from typing import Any, Callable
|
||||
|
||||
from .types import (
|
||||
NotifyOptions,
|
||||
NotificationPayload,
|
||||
Notification,
|
||||
SendResult,
|
||||
SeverityLevel,
|
||||
ConnectionState,
|
||||
)
|
||||
from .transport import Transport
|
||||
from .queue import OfflineQueue
|
||||
from .builder import EventBuilder
|
||||
|
||||
|
||||
class NotifyClient:
|
||||
"""IronNotify client for sending and receiving notifications."""
|
||||
|
||||
def __init__(self, options: NotifyOptions):
|
||||
self._options = options
|
||||
self._transport = Transport(
|
||||
options.api_base_url,
|
||||
options.api_key,
|
||||
options.debug,
|
||||
)
|
||||
self._queue = OfflineQueue(options.max_offline_queue_size, options.debug)
|
||||
self._is_online = True
|
||||
self._connection_state: ConnectionState = "disconnected"
|
||||
|
||||
# Event handlers
|
||||
self._on_notification: Callable[[Notification], None] | None = None
|
||||
self._on_unread_count_change: Callable[[int], None] | None = None
|
||||
self._on_connection_state_change: Callable[[ConnectionState], None] | None = None
|
||||
|
||||
if options.debug:
|
||||
print("[IronNotify] Initialized")
|
||||
|
||||
def notify(
|
||||
self,
|
||||
event_type: str,
|
||||
title: str,
|
||||
message: str | None = None,
|
||||
severity: SeverityLevel = "info",
|
||||
metadata: dict[str, Any] | None = None,
|
||||
user_id: str | None = None,
|
||||
) -> SendResult:
|
||||
"""Send a notification (sync)."""
|
||||
payload = NotificationPayload(
|
||||
event_type=event_type,
|
||||
title=title,
|
||||
message=message,
|
||||
severity=severity,
|
||||
metadata=metadata,
|
||||
user_id=user_id,
|
||||
)
|
||||
return self.send_payload(payload)
|
||||
|
||||
async def notify_async(
|
||||
self,
|
||||
event_type: str,
|
||||
title: str,
|
||||
message: str | None = None,
|
||||
severity: SeverityLevel = "info",
|
||||
metadata: dict[str, Any] | None = None,
|
||||
user_id: str | None = None,
|
||||
) -> SendResult:
|
||||
"""Send a notification (async)."""
|
||||
payload = NotificationPayload(
|
||||
event_type=event_type,
|
||||
title=title,
|
||||
message=message,
|
||||
severity=severity,
|
||||
metadata=metadata,
|
||||
user_id=user_id,
|
||||
)
|
||||
return await self.send_payload_async(payload)
|
||||
|
||||
def event(self, event_type: str) -> EventBuilder:
|
||||
"""Create an event builder for complex notifications."""
|
||||
return EventBuilder(self, event_type)
|
||||
|
||||
def send_payload(self, payload: NotificationPayload) -> SendResult:
|
||||
"""Send a notification payload (sync)."""
|
||||
result = self._transport.send(payload)
|
||||
|
||||
if not result.success and self._options.enable_offline_queue:
|
||||
self._queue.add(payload)
|
||||
self._is_online = False
|
||||
return SendResult(
|
||||
success=result.success,
|
||||
notification_id=result.notification_id,
|
||||
error=result.error,
|
||||
queued=True,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
async def send_payload_async(self, payload: NotificationPayload) -> SendResult:
|
||||
"""Send a notification payload (async)."""
|
||||
result = await self._transport.send_async(payload)
|
||||
|
||||
if not result.success and self._options.enable_offline_queue:
|
||||
self._queue.add(payload)
|
||||
self._is_online = False
|
||||
return SendResult(
|
||||
success=result.success,
|
||||
notification_id=result.notification_id,
|
||||
error=result.error,
|
||||
queued=True,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def connect(self) -> None:
|
||||
"""Connect to real-time notifications."""
|
||||
# WebSocket connection would be implemented here
|
||||
self._set_connection_state("connected")
|
||||
if self._options.debug:
|
||||
print("[IronNotify] Connected (WebSocket not implemented in sync mode)")
|
||||
|
||||
def disconnect(self) -> None:
|
||||
"""Disconnect from real-time notifications."""
|
||||
self._set_connection_state("disconnected")
|
||||
|
||||
def subscribe_to_user(self, user_id: str) -> None:
|
||||
"""Subscribe to a user's notifications."""
|
||||
if self._options.debug:
|
||||
print(f"[IronNotify] Subscribed to user: {user_id}")
|
||||
|
||||
def subscribe_to_app(self) -> None:
|
||||
"""Subscribe to app-wide notifications."""
|
||||
if self._options.debug:
|
||||
print("[IronNotify] Subscribed to app notifications")
|
||||
|
||||
def on_notification(self, handler: Callable[[Notification], None]) -> None:
|
||||
"""Set notification handler."""
|
||||
self._on_notification = handler
|
||||
|
||||
def on_unread_count_change(self, handler: Callable[[int], None]) -> None:
|
||||
"""Set unread count change handler."""
|
||||
self._on_unread_count_change = handler
|
||||
|
||||
def on_connection_state_change(self, handler: Callable[[ConnectionState], None]) -> None:
|
||||
"""Set connection state change handler."""
|
||||
self._on_connection_state_change = handler
|
||||
|
||||
@property
|
||||
def connection_state(self) -> ConnectionState:
|
||||
"""Get connection state."""
|
||||
return self._connection_state
|
||||
|
||||
def get_notifications(
|
||||
self,
|
||||
limit: int | None = None,
|
||||
offset: int | None = None,
|
||||
unread_only: bool = False,
|
||||
) -> list[Notification]:
|
||||
"""Get notifications (sync)."""
|
||||
return self._transport.get_notifications(limit, offset, unread_only)
|
||||
|
||||
async def get_notifications_async(
|
||||
self,
|
||||
limit: int | None = None,
|
||||
offset: int | None = None,
|
||||
unread_only: bool = False,
|
||||
) -> list[Notification]:
|
||||
"""Get notifications (async)."""
|
||||
return await self._transport.get_notifications_async(limit, offset, unread_only)
|
||||
|
||||
def get_unread_count(self) -> int:
|
||||
"""Get unread count (sync)."""
|
||||
return self._transport.get_unread_count()
|
||||
|
||||
async def get_unread_count_async(self) -> int:
|
||||
"""Get unread count (async)."""
|
||||
return await self._transport.get_unread_count_async()
|
||||
|
||||
def mark_as_read(self, notification_id: str) -> bool:
|
||||
"""Mark a notification as read (sync)."""
|
||||
return self._transport.mark_as_read(notification_id)
|
||||
|
||||
async def mark_as_read_async(self, notification_id: str) -> bool:
|
||||
"""Mark a notification as read (async)."""
|
||||
return await self._transport.mark_as_read_async(notification_id)
|
||||
|
||||
def mark_all_as_read(self) -> bool:
|
||||
"""Mark all notifications as read (sync)."""
|
||||
return self._transport.mark_all_as_read()
|
||||
|
||||
async def mark_all_as_read_async(self) -> bool:
|
||||
"""Mark all notifications as read (async)."""
|
||||
return await self._transport.mark_all_as_read_async()
|
||||
|
||||
def flush(self) -> None:
|
||||
"""Flush the offline queue (sync)."""
|
||||
if self._queue.is_empty:
|
||||
return
|
||||
|
||||
if not self._transport.is_online():
|
||||
return
|
||||
|
||||
self._is_online = True
|
||||
notifications = self._queue.get_all()
|
||||
|
||||
for i in range(len(notifications) - 1, -1, -1):
|
||||
result = self._transport.send(notifications[i])
|
||||
if result.success:
|
||||
self._queue.remove(i)
|
||||
else:
|
||||
break
|
||||
|
||||
async def flush_async(self) -> None:
|
||||
"""Flush the offline queue (async)."""
|
||||
if self._queue.is_empty:
|
||||
return
|
||||
|
||||
if not await self._transport.is_online_async():
|
||||
return
|
||||
|
||||
self._is_online = True
|
||||
notifications = self._queue.get_all()
|
||||
|
||||
for i in range(len(notifications) - 1, -1, -1):
|
||||
result = await self._transport.send_async(notifications[i])
|
||||
if result.success:
|
||||
self._queue.remove(i)
|
||||
else:
|
||||
break
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the client and clean up resources."""
|
||||
self.disconnect()
|
||||
self._transport.close()
|
||||
|
||||
def _set_connection_state(self, state: ConnectionState) -> None:
|
||||
self._connection_state = state
|
||||
if self._on_connection_state_change:
|
||||
self._on_connection_state_change(state)
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
"""Offline queue for IronNotify SDK."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .types import NotificationPayload, NotificationAction
|
||||
|
||||
|
||||
class OfflineQueue:
|
||||
"""Offline queue for storing notifications when offline."""
|
||||
|
||||
def __init__(self, max_size: int, debug: bool):
|
||||
self.max_size = max_size
|
||||
self.debug = debug
|
||||
self._queue: list[NotificationPayload] = []
|
||||
self._storage_path = Path.home() / ".ironnotify" / "offline_queue.json"
|
||||
self._load_from_storage()
|
||||
|
||||
def add(self, payload: NotificationPayload) -> None:
|
||||
"""Add a notification to the queue."""
|
||||
if len(self._queue) >= self.max_size:
|
||||
self._queue.pop(0)
|
||||
if self.debug:
|
||||
print("[IronNotify] Offline queue full, dropping oldest notification")
|
||||
|
||||
self._queue.append(payload)
|
||||
self._save_to_storage()
|
||||
|
||||
if self.debug:
|
||||
print(f"[IronNotify] Notification queued for later: {payload.event_type}")
|
||||
|
||||
def get_all(self) -> list[NotificationPayload]:
|
||||
"""Get all queued notifications."""
|
||||
return list(self._queue)
|
||||
|
||||
def remove(self, index: int) -> None:
|
||||
"""Remove a notification from the queue."""
|
||||
if 0 <= index < len(self._queue):
|
||||
self._queue.pop(index)
|
||||
self._save_to_storage()
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear the queue."""
|
||||
self._queue = []
|
||||
self._save_to_storage()
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
"""Get queue size."""
|
||||
return len(self._queue)
|
||||
|
||||
@property
|
||||
def is_empty(self) -> bool:
|
||||
"""Check if queue is empty."""
|
||||
return len(self._queue) == 0
|
||||
|
||||
def _serialize_payload(self, payload: NotificationPayload) -> dict[str, Any]:
|
||||
data: dict[str, Any] = {
|
||||
"event_type": payload.event_type,
|
||||
"title": payload.title,
|
||||
"severity": payload.severity,
|
||||
}
|
||||
if payload.message:
|
||||
data["message"] = payload.message
|
||||
if payload.metadata:
|
||||
data["metadata"] = payload.metadata
|
||||
if payload.actions:
|
||||
data["actions"] = [
|
||||
{"label": a.label, "url": a.url, "action": a.action, "style": a.style}
|
||||
for a in payload.actions
|
||||
]
|
||||
if payload.user_id:
|
||||
data["user_id"] = payload.user_id
|
||||
if payload.group_key:
|
||||
data["group_key"] = payload.group_key
|
||||
if payload.deduplication_key:
|
||||
data["deduplication_key"] = payload.deduplication_key
|
||||
if payload.expires_at:
|
||||
data["expires_at"] = payload.expires_at.isoformat()
|
||||
return data
|
||||
|
||||
def _deserialize_payload(self, data: dict[str, Any]) -> NotificationPayload:
|
||||
from datetime import datetime
|
||||
|
||||
actions = None
|
||||
if data.get("actions"):
|
||||
actions = [
|
||||
NotificationAction(
|
||||
label=a["label"],
|
||||
url=a.get("url"),
|
||||
action=a.get("action"),
|
||||
style=a.get("style", "default"),
|
||||
)
|
||||
for a in data["actions"]
|
||||
]
|
||||
|
||||
return NotificationPayload(
|
||||
event_type=data["event_type"],
|
||||
title=data["title"],
|
||||
message=data.get("message"),
|
||||
severity=data.get("severity", "info"),
|
||||
metadata=data.get("metadata"),
|
||||
actions=actions,
|
||||
user_id=data.get("user_id"),
|
||||
group_key=data.get("group_key"),
|
||||
deduplication_key=data.get("deduplication_key"),
|
||||
expires_at=datetime.fromisoformat(data["expires_at"])
|
||||
if data.get("expires_at")
|
||||
else None,
|
||||
)
|
||||
|
||||
def _load_from_storage(self) -> None:
|
||||
"""Load queue from storage."""
|
||||
try:
|
||||
if self._storage_path.exists():
|
||||
with open(self._storage_path, "r") as f:
|
||||
data = json.load(f)
|
||||
self._queue = [self._deserialize_payload(p) for p in data]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _save_to_storage(self) -> None:
|
||||
"""Save queue to storage."""
|
||||
try:
|
||||
self._storage_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(self._storage_path, "w") as f:
|
||||
json.dump([self._serialize_payload(p) for p in self._queue], f)
|
||||
except Exception:
|
||||
pass
|
||||
|
|
@ -0,0 +1,281 @@
|
|||
"""HTTP transport for IronNotify SDK."""
|
||||
|
||||
import httpx
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from .types import NotificationPayload, SendResult, Notification, NotificationAction
|
||||
|
||||
|
||||
class Transport:
|
||||
"""HTTP transport for sending notifications."""
|
||||
|
||||
def __init__(self, api_base_url: str, api_key: str, debug: bool):
|
||||
self.api_base_url = api_base_url
|
||||
self.api_key = api_key
|
||||
self.debug = debug
|
||||
self._sync_client: httpx.Client | None = None
|
||||
self._async_client: httpx.AsyncClient | None = None
|
||||
|
||||
@property
|
||||
def sync_client(self) -> httpx.Client:
|
||||
if self._sync_client is None:
|
||||
self._sync_client = httpx.Client(
|
||||
base_url=self.api_base_url,
|
||||
headers={"Authorization": f"Bearer {self.api_key}"},
|
||||
timeout=30.0,
|
||||
)
|
||||
return self._sync_client
|
||||
|
||||
@property
|
||||
def async_client(self) -> httpx.AsyncClient:
|
||||
if self._async_client is None:
|
||||
self._async_client = httpx.AsyncClient(
|
||||
base_url=self.api_base_url,
|
||||
headers={"Authorization": f"Bearer {self.api_key}"},
|
||||
timeout=30.0,
|
||||
)
|
||||
return self._async_client
|
||||
|
||||
def _serialize_payload(self, payload: NotificationPayload) -> dict[str, Any]:
|
||||
data: dict[str, Any] = {
|
||||
"eventType": payload.event_type,
|
||||
"title": payload.title,
|
||||
"severity": payload.severity,
|
||||
}
|
||||
if payload.message:
|
||||
data["message"] = payload.message
|
||||
if payload.metadata:
|
||||
data["metadata"] = payload.metadata
|
||||
if payload.actions:
|
||||
data["actions"] = [
|
||||
{
|
||||
"label": a.label,
|
||||
"url": a.url,
|
||||
"action": a.action,
|
||||
"style": a.style,
|
||||
}
|
||||
for a in payload.actions
|
||||
]
|
||||
if payload.user_id:
|
||||
data["userId"] = payload.user_id
|
||||
if payload.group_key:
|
||||
data["groupKey"] = payload.group_key
|
||||
if payload.deduplication_key:
|
||||
data["deduplicationKey"] = payload.deduplication_key
|
||||
if payload.expires_at:
|
||||
data["expiresAt"] = payload.expires_at.isoformat()
|
||||
return data
|
||||
|
||||
def _parse_notification(self, data: dict[str, Any]) -> Notification:
|
||||
actions = None
|
||||
if data.get("actions"):
|
||||
actions = [
|
||||
NotificationAction(
|
||||
label=a["label"],
|
||||
url=a.get("url"),
|
||||
action=a.get("action"),
|
||||
style=a.get("style", "default"),
|
||||
)
|
||||
for a in data["actions"]
|
||||
]
|
||||
|
||||
return Notification(
|
||||
id=data["id"],
|
||||
event_type=data["eventType"],
|
||||
title=data["title"],
|
||||
message=data.get("message"),
|
||||
severity=data.get("severity", "info"),
|
||||
metadata=data.get("metadata"),
|
||||
actions=actions,
|
||||
read=data.get("read", False),
|
||||
created_at=datetime.fromisoformat(data["createdAt"].replace("Z", "+00:00")),
|
||||
expires_at=datetime.fromisoformat(data["expiresAt"].replace("Z", "+00:00"))
|
||||
if data.get("expiresAt")
|
||||
else None,
|
||||
)
|
||||
|
||||
def send(self, payload: NotificationPayload) -> SendResult:
|
||||
"""Send a notification (sync)."""
|
||||
try:
|
||||
response = self.sync_client.post(
|
||||
"/api/v1/notifications",
|
||||
json=self._serialize_payload(payload),
|
||||
)
|
||||
|
||||
if response.status_code >= 400:
|
||||
error = f"HTTP {response.status_code}: {response.text}"
|
||||
if self.debug:
|
||||
print(f"[IronNotify] Failed to send notification: {error}")
|
||||
return SendResult(success=False, error=error)
|
||||
|
||||
result = response.json()
|
||||
if self.debug:
|
||||
print(f"[IronNotify] Notification sent: {result.get('notificationId')}")
|
||||
return SendResult(success=True, notification_id=result.get("notificationId"))
|
||||
|
||||
except Exception as e:
|
||||
error = str(e)
|
||||
if self.debug:
|
||||
print(f"[IronNotify] Failed to send notification: {error}")
|
||||
return SendResult(success=False, error=error)
|
||||
|
||||
async def send_async(self, payload: NotificationPayload) -> SendResult:
|
||||
"""Send a notification (async)."""
|
||||
try:
|
||||
response = await self.async_client.post(
|
||||
"/api/v1/notifications",
|
||||
json=self._serialize_payload(payload),
|
||||
)
|
||||
|
||||
if response.status_code >= 400:
|
||||
error = f"HTTP {response.status_code}: {response.text}"
|
||||
if self.debug:
|
||||
print(f"[IronNotify] Failed to send notification: {error}")
|
||||
return SendResult(success=False, error=error)
|
||||
|
||||
result = response.json()
|
||||
if self.debug:
|
||||
print(f"[IronNotify] Notification sent: {result.get('notificationId')}")
|
||||
return SendResult(success=True, notification_id=result.get("notificationId"))
|
||||
|
||||
except Exception as e:
|
||||
error = str(e)
|
||||
if self.debug:
|
||||
print(f"[IronNotify] Failed to send notification: {error}")
|
||||
return SendResult(success=False, error=error)
|
||||
|
||||
def get_notifications(
|
||||
self,
|
||||
limit: int | None = None,
|
||||
offset: int | None = None,
|
||||
unread_only: bool = False,
|
||||
) -> list[Notification]:
|
||||
"""Get notifications (sync)."""
|
||||
params = {}
|
||||
if limit:
|
||||
params["limit"] = str(limit)
|
||||
if offset:
|
||||
params["offset"] = str(offset)
|
||||
if unread_only:
|
||||
params["unreadOnly"] = "true"
|
||||
|
||||
try:
|
||||
response = self.sync_client.get("/api/v1/notifications", params=params)
|
||||
if response.status_code >= 400:
|
||||
return []
|
||||
data = response.json()
|
||||
return [self._parse_notification(n) for n in data.get("notifications", [])]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
async def get_notifications_async(
|
||||
self,
|
||||
limit: int | None = None,
|
||||
offset: int | None = None,
|
||||
unread_only: bool = False,
|
||||
) -> list[Notification]:
|
||||
"""Get notifications (async)."""
|
||||
params = {}
|
||||
if limit:
|
||||
params["limit"] = str(limit)
|
||||
if offset:
|
||||
params["offset"] = str(offset)
|
||||
if unread_only:
|
||||
params["unreadOnly"] = "true"
|
||||
|
||||
try:
|
||||
response = await self.async_client.get("/api/v1/notifications", params=params)
|
||||
if response.status_code >= 400:
|
||||
return []
|
||||
data = response.json()
|
||||
return [self._parse_notification(n) for n in data.get("notifications", [])]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def mark_as_read(self, notification_id: str) -> bool:
|
||||
"""Mark a notification as read (sync)."""
|
||||
try:
|
||||
response = self.sync_client.post(f"/api/v1/notifications/{notification_id}/read")
|
||||
return response.status_code < 400
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
async def mark_as_read_async(self, notification_id: str) -> bool:
|
||||
"""Mark a notification as read (async)."""
|
||||
try:
|
||||
response = await self.async_client.post(
|
||||
f"/api/v1/notifications/{notification_id}/read"
|
||||
)
|
||||
return response.status_code < 400
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def mark_all_as_read(self) -> bool:
|
||||
"""Mark all notifications as read (sync)."""
|
||||
try:
|
||||
response = self.sync_client.post("/api/v1/notifications/read-all")
|
||||
return response.status_code < 400
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
async def mark_all_as_read_async(self) -> bool:
|
||||
"""Mark all notifications as read (async)."""
|
||||
try:
|
||||
response = await self.async_client.post("/api/v1/notifications/read-all")
|
||||
return response.status_code < 400
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_unread_count(self) -> int:
|
||||
"""Get unread count (sync)."""
|
||||
try:
|
||||
response = self.sync_client.get("/api/v1/notifications/unread-count")
|
||||
if response.status_code >= 400:
|
||||
return 0
|
||||
return response.json().get("count", 0)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
async def get_unread_count_async(self) -> int:
|
||||
"""Get unread count (async)."""
|
||||
try:
|
||||
response = await self.async_client.get("/api/v1/notifications/unread-count")
|
||||
if response.status_code >= 400:
|
||||
return 0
|
||||
return response.json().get("count", 0)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
def is_online(self) -> bool:
|
||||
"""Check if server is online (sync)."""
|
||||
try:
|
||||
response = self.sync_client.get("/api/v1/health")
|
||||
return response.status_code < 400
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
async def is_online_async(self) -> bool:
|
||||
"""Check if server is online (async)."""
|
||||
try:
|
||||
response = await self.async_client.get("/api/v1/health")
|
||||
return response.status_code < 400
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def close(self):
|
||||
"""Close the transport."""
|
||||
if self._sync_client:
|
||||
self._sync_client.close()
|
||||
self._sync_client = None
|
||||
if self._async_client:
|
||||
import asyncio
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
if loop.is_running():
|
||||
loop.create_task(self._async_client.aclose())
|
||||
else:
|
||||
loop.run_until_complete(self._async_client.aclose())
|
||||
except Exception:
|
||||
pass
|
||||
self._async_client = None
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
"""Type definitions for IronNotify SDK."""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Any, Callable, Literal
|
||||
|
||||
# Type aliases
|
||||
SeverityLevel = Literal["info", "success", "warning", "error", "critical"]
|
||||
ConnectionState = Literal["disconnected", "connecting", "connected", "reconnecting"]
|
||||
|
||||
|
||||
@dataclass
|
||||
class NotificationAction:
|
||||
"""Notification action button."""
|
||||
label: str
|
||||
url: str | None = None
|
||||
action: str | None = None
|
||||
style: Literal["default", "primary", "danger"] = "default"
|
||||
|
||||
|
||||
@dataclass
|
||||
class NotificationPayload:
|
||||
"""Notification payload sent to the server."""
|
||||
event_type: str
|
||||
title: str
|
||||
message: str | None = None
|
||||
severity: SeverityLevel = "info"
|
||||
metadata: dict[str, Any] | None = None
|
||||
actions: list[NotificationAction] | None = None
|
||||
user_id: str | None = None
|
||||
group_key: str | None = None
|
||||
deduplication_key: str | None = None
|
||||
expires_at: datetime | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Notification:
|
||||
"""Notification received from the server."""
|
||||
id: str
|
||||
event_type: str
|
||||
title: str
|
||||
severity: SeverityLevel
|
||||
read: bool
|
||||
created_at: datetime
|
||||
message: str | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
actions: list[NotificationAction] | None = None
|
||||
expires_at: datetime | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class SendResult:
|
||||
"""Result of sending a notification."""
|
||||
success: bool
|
||||
notification_id: str | None = None
|
||||
error: str | None = None
|
||||
queued: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class NotifyOptions:
|
||||
"""Configuration options for the notify client."""
|
||||
api_key: str
|
||||
api_base_url: str = "https://api.ironnotify.com"
|
||||
ws_url: str = "wss://ws.ironnotify.com"
|
||||
debug: bool = False
|
||||
enable_offline_queue: bool = True
|
||||
max_offline_queue_size: int = 100
|
||||
auto_reconnect: bool = True
|
||||
max_reconnect_attempts: int = 5
|
||||
reconnect_delay: float = 1.0
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.api_key:
|
||||
raise ValueError("API key is required")
|
||||
if not self.api_key.startswith("ak_"):
|
||||
raise ValueError("API key must start with ak_")
|
||||
if not (self.api_key.startswith("ak_live_") or self.api_key.startswith("ak_test_")):
|
||||
raise ValueError("API key must be ak_live_xxx or ak_test_xxx")
|
||||
|
||||
|
||||
@dataclass
|
||||
class NotifyEventHandlers:
|
||||
"""Event handlers for real-time notifications."""
|
||||
on_notification: Callable[[Notification], None] | None = None
|
||||
on_unread_count_change: Callable[[int], None] | None = None
|
||||
on_connection_state_change: Callable[[ConnectionState], None] | None = None
|
||||
on_error: Callable[[Exception], None] | None = None
|
||||
Loading…
Reference in New Issue