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
|
||||||
IronNotify SDK for Python - Event notifications and alerts
|
|
||||||
|
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