hackbou-push-notification 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: hackbou-push-notification
3
+ Version: 0.1.0
4
+ Summary: Client Python / Django pour l'API Push Notification
5
+ Author-email: hackbou <hackbou@users.noreply.github.com>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/hardhacklife/push-notification-python-sdk
8
+ Project-URL: Documentation, https://github.com/hardhacklife/push-notification-python-sdk#readme
9
+ Project-URL: Repository, https://github.com/hardhacklife/push-notification-python-sdk
10
+ Project-URL: Issues, https://github.com/hardhacklife/push-notification-python-sdk/issues
11
+ Keywords: push-notification,push,notifications,django,fcm,apns
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: Django
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Typing :: Typed
21
+ Classifier: Topic :: Communications
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ Requires-Dist: httpx>=0.27
25
+ Requires-Dist: typing_extensions>=4.5; python_version < "3.11"
26
+ Provides-Extra: django
27
+ Requires-Dist: Django>=4.2; extra == "django"
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=8.0; extra == "dev"
30
+ Requires-Dist: build; extra == "dev"
31
+ Requires-Dist: twine; extra == "dev"
32
+
33
+ # hackbou-push-notification
34
+
35
+ Client Python / Django pour l’API **Push Notification**.
36
+
37
+ Typage : PEP 561 (`py.typed`) + `TypedDict` pour les réponses.
38
+
39
+ **Import :** `push_notification`
40
+ **Package PyPI :** `hackbou-push-notification`
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install hackbou-push-notification
46
+
47
+ # avec helpers Django
48
+ pip install "hackbou-push-notification[django]"
49
+ ```
50
+
51
+ ## Usage Python
52
+
53
+ ```python
54
+ from push_notification import PushNotificationClient, Device, NotificationCreateResult
55
+
56
+ with PushNotificationClient(
57
+ api_key="pf_live_…",
58
+ base_url="https://push.example.com",
59
+ ) as push:
60
+ result: NotificationCreateResult = push.send_notification(
61
+ title="Nouveau cours",
62
+ body="Le support est disponible.",
63
+ user_ids=["student_123"],
64
+ data={"screen": "course"},
65
+ )
66
+ devices: list[Device] = push.list_devices(limit=50)
67
+ page = push.list_devices_page(q="expo", limit=25, offset=0)
68
+ ```
69
+
70
+ ## Usage Django
71
+
72
+ `settings.py` :
73
+
74
+ ```python
75
+ PUSH_NOTIFICATION_API_KEY = env("PUSH_NOTIFICATION_API_KEY")
76
+ PUSH_NOTIFICATION_BASE_URL = env("PUSH_NOTIFICATION_BASE_URL", default="http://127.0.0.1:8787")
77
+ PUSH_NOTIFICATION_TIMEOUT = 15
78
+ ```
79
+
80
+ ```python
81
+ from push_notification.django import send_to_user, get_client
82
+
83
+ send_to_user(
84
+ "student_123",
85
+ title="Nouveau cours",
86
+ body="Un support vient d’être publié.",
87
+ data={"course_id": "math_101"},
88
+ )
89
+
90
+ client = get_client()
91
+ client.login_user("student_123", device_ids=["…"])
92
+ client.set_user_tags("student_123", {"language": "fr"})
93
+ ```
94
+
95
+ ## API du client
96
+
97
+ | Méthode | Endpoint |
98
+ |---------|----------|
99
+ | `register_device(platform=, token=)` | `POST /v1/devices` |
100
+ | `list_devices(q=, limit=, offset=)` | `GET /v1/devices` → `list[Device]` |
101
+ | `list_devices_page(…)` | idem → `{items,total,limit,offset,has_more}` |
102
+ | `delete_device(id)` | `DELETE /v1/devices/{id}` |
103
+ | `login` / `login_user` | `POST /v1/users/…/login` |
104
+ | `set_tags` / `set_user_tags` | `PUT /v1/users/…/tags` |
105
+ | `list_users` / `list_users_page` | `GET /v1/users` |
106
+ | `send_notification(…)` | `POST /v1/notifications` |
107
+ | `get_notification(id)` | `GET /v1/notifications/{id}` |
108
+ | `list_notifications` / `_page` | `GET /v1/notifications` |
109
+ | `health_live()` / `health_ready()` | healthchecks |
110
+
111
+ Les listes acceptent l’enveloppe paginée **et** un ancien tableau brut (compat).
112
+
113
+ ## Tests
114
+
115
+ ```bash
116
+ pip install -e ".[dev]"
117
+ pytest
118
+ ```
119
+
120
+ ## Licence
121
+
122
+ Apache-2.0
@@ -0,0 +1,90 @@
1
+ # hackbou-push-notification
2
+
3
+ Client Python / Django pour l’API **Push Notification**.
4
+
5
+ Typage : PEP 561 (`py.typed`) + `TypedDict` pour les réponses.
6
+
7
+ **Import :** `push_notification`
8
+ **Package PyPI :** `hackbou-push-notification`
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ pip install hackbou-push-notification
14
+
15
+ # avec helpers Django
16
+ pip install "hackbou-push-notification[django]"
17
+ ```
18
+
19
+ ## Usage Python
20
+
21
+ ```python
22
+ from push_notification import PushNotificationClient, Device, NotificationCreateResult
23
+
24
+ with PushNotificationClient(
25
+ api_key="pf_live_…",
26
+ base_url="https://push.example.com",
27
+ ) as push:
28
+ result: NotificationCreateResult = push.send_notification(
29
+ title="Nouveau cours",
30
+ body="Le support est disponible.",
31
+ user_ids=["student_123"],
32
+ data={"screen": "course"},
33
+ )
34
+ devices: list[Device] = push.list_devices(limit=50)
35
+ page = push.list_devices_page(q="expo", limit=25, offset=0)
36
+ ```
37
+
38
+ ## Usage Django
39
+
40
+ `settings.py` :
41
+
42
+ ```python
43
+ PUSH_NOTIFICATION_API_KEY = env("PUSH_NOTIFICATION_API_KEY")
44
+ PUSH_NOTIFICATION_BASE_URL = env("PUSH_NOTIFICATION_BASE_URL", default="http://127.0.0.1:8787")
45
+ PUSH_NOTIFICATION_TIMEOUT = 15
46
+ ```
47
+
48
+ ```python
49
+ from push_notification.django import send_to_user, get_client
50
+
51
+ send_to_user(
52
+ "student_123",
53
+ title="Nouveau cours",
54
+ body="Un support vient d’être publié.",
55
+ data={"course_id": "math_101"},
56
+ )
57
+
58
+ client = get_client()
59
+ client.login_user("student_123", device_ids=["…"])
60
+ client.set_user_tags("student_123", {"language": "fr"})
61
+ ```
62
+
63
+ ## API du client
64
+
65
+ | Méthode | Endpoint |
66
+ |---------|----------|
67
+ | `register_device(platform=, token=)` | `POST /v1/devices` |
68
+ | `list_devices(q=, limit=, offset=)` | `GET /v1/devices` → `list[Device]` |
69
+ | `list_devices_page(…)` | idem → `{items,total,limit,offset,has_more}` |
70
+ | `delete_device(id)` | `DELETE /v1/devices/{id}` |
71
+ | `login` / `login_user` | `POST /v1/users/…/login` |
72
+ | `set_tags` / `set_user_tags` | `PUT /v1/users/…/tags` |
73
+ | `list_users` / `list_users_page` | `GET /v1/users` |
74
+ | `send_notification(…)` | `POST /v1/notifications` |
75
+ | `get_notification(id)` | `GET /v1/notifications/{id}` |
76
+ | `list_notifications` / `_page` | `GET /v1/notifications` |
77
+ | `health_live()` / `health_ready()` | healthchecks |
78
+
79
+ Les listes acceptent l’enveloppe paginée **et** un ancien tableau brut (compat).
80
+
81
+ ## Tests
82
+
83
+ ```bash
84
+ pip install -e ".[dev]"
85
+ pytest
86
+ ```
87
+
88
+ ## Licence
89
+
90
+ Apache-2.0
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: hackbou-push-notification
3
+ Version: 0.1.0
4
+ Summary: Client Python / Django pour l'API Push Notification
5
+ Author-email: hackbou <hackbou@users.noreply.github.com>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/hardhacklife/push-notification-python-sdk
8
+ Project-URL: Documentation, https://github.com/hardhacklife/push-notification-python-sdk#readme
9
+ Project-URL: Repository, https://github.com/hardhacklife/push-notification-python-sdk
10
+ Project-URL: Issues, https://github.com/hardhacklife/push-notification-python-sdk/issues
11
+ Keywords: push-notification,push,notifications,django,fcm,apns
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: Django
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Typing :: Typed
21
+ Classifier: Topic :: Communications
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ Requires-Dist: httpx>=0.27
25
+ Requires-Dist: typing_extensions>=4.5; python_version < "3.11"
26
+ Provides-Extra: django
27
+ Requires-Dist: Django>=4.2; extra == "django"
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=8.0; extra == "dev"
30
+ Requires-Dist: build; extra == "dev"
31
+ Requires-Dist: twine; extra == "dev"
32
+
33
+ # hackbou-push-notification
34
+
35
+ Client Python / Django pour l’API **Push Notification**.
36
+
37
+ Typage : PEP 561 (`py.typed`) + `TypedDict` pour les réponses.
38
+
39
+ **Import :** `push_notification`
40
+ **Package PyPI :** `hackbou-push-notification`
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install hackbou-push-notification
46
+
47
+ # avec helpers Django
48
+ pip install "hackbou-push-notification[django]"
49
+ ```
50
+
51
+ ## Usage Python
52
+
53
+ ```python
54
+ from push_notification import PushNotificationClient, Device, NotificationCreateResult
55
+
56
+ with PushNotificationClient(
57
+ api_key="pf_live_…",
58
+ base_url="https://push.example.com",
59
+ ) as push:
60
+ result: NotificationCreateResult = push.send_notification(
61
+ title="Nouveau cours",
62
+ body="Le support est disponible.",
63
+ user_ids=["student_123"],
64
+ data={"screen": "course"},
65
+ )
66
+ devices: list[Device] = push.list_devices(limit=50)
67
+ page = push.list_devices_page(q="expo", limit=25, offset=0)
68
+ ```
69
+
70
+ ## Usage Django
71
+
72
+ `settings.py` :
73
+
74
+ ```python
75
+ PUSH_NOTIFICATION_API_KEY = env("PUSH_NOTIFICATION_API_KEY")
76
+ PUSH_NOTIFICATION_BASE_URL = env("PUSH_NOTIFICATION_BASE_URL", default="http://127.0.0.1:8787")
77
+ PUSH_NOTIFICATION_TIMEOUT = 15
78
+ ```
79
+
80
+ ```python
81
+ from push_notification.django import send_to_user, get_client
82
+
83
+ send_to_user(
84
+ "student_123",
85
+ title="Nouveau cours",
86
+ body="Un support vient d’être publié.",
87
+ data={"course_id": "math_101"},
88
+ )
89
+
90
+ client = get_client()
91
+ client.login_user("student_123", device_ids=["…"])
92
+ client.set_user_tags("student_123", {"language": "fr"})
93
+ ```
94
+
95
+ ## API du client
96
+
97
+ | Méthode | Endpoint |
98
+ |---------|----------|
99
+ | `register_device(platform=, token=)` | `POST /v1/devices` |
100
+ | `list_devices(q=, limit=, offset=)` | `GET /v1/devices` → `list[Device]` |
101
+ | `list_devices_page(…)` | idem → `{items,total,limit,offset,has_more}` |
102
+ | `delete_device(id)` | `DELETE /v1/devices/{id}` |
103
+ | `login` / `login_user` | `POST /v1/users/…/login` |
104
+ | `set_tags` / `set_user_tags` | `PUT /v1/users/…/tags` |
105
+ | `list_users` / `list_users_page` | `GET /v1/users` |
106
+ | `send_notification(…)` | `POST /v1/notifications` |
107
+ | `get_notification(id)` | `GET /v1/notifications/{id}` |
108
+ | `list_notifications` / `_page` | `GET /v1/notifications` |
109
+ | `health_live()` / `health_ready()` | healthchecks |
110
+
111
+ Les listes acceptent l’enveloppe paginée **et** un ancien tableau brut (compat).
112
+
113
+ ## Tests
114
+
115
+ ```bash
116
+ pip install -e ".[dev]"
117
+ pytest
118
+ ```
119
+
120
+ ## Licence
121
+
122
+ Apache-2.0
@@ -0,0 +1,18 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.py
4
+ hackbou_push_notification.egg-info/PKG-INFO
5
+ hackbou_push_notification.egg-info/SOURCES.txt
6
+ hackbou_push_notification.egg-info/dependency_links.txt
7
+ hackbou_push_notification.egg-info/requires.txt
8
+ hackbou_push_notification.egg-info/top_level.txt
9
+ push_notification/__init__.py
10
+ push_notification/_parse.py
11
+ push_notification/client.py
12
+ push_notification/django.py
13
+ push_notification/exceptions.py
14
+ push_notification/models.py
15
+ push_notification/py.typed
16
+ tests/test_client.py
17
+ tests/test_exceptions.py
18
+ tests/test_parse.py
@@ -0,0 +1,12 @@
1
+ httpx>=0.27
2
+
3
+ [:python_version < "3.11"]
4
+ typing_extensions>=4.5
5
+
6
+ [dev]
7
+ pytest>=8.0
8
+ build
9
+ twine
10
+
11
+ [django]
12
+ Django>=4.2
@@ -0,0 +1,27 @@
1
+ """Client Python PushNotification."""
2
+
3
+ from .client import PushNotificationClient
4
+ from .exceptions import PushNotificationError
5
+ from .models import (
6
+ DeliverySummary,
7
+ Device,
8
+ NotificationCreateResult,
9
+ NotificationDetail,
10
+ Page,
11
+ TagsResponse,
12
+ User,
13
+ )
14
+
15
+ __all__ = [
16
+ "PushNotificationClient",
17
+ "PushNotificationError",
18
+ "Device",
19
+ "User",
20
+ "Page",
21
+ "TagsResponse",
22
+ "DeliverySummary",
23
+ "NotificationCreateResult",
24
+ "NotificationDetail",
25
+ "__version__",
26
+ ]
27
+ __version__ = "0.1.1"
@@ -0,0 +1,127 @@
1
+ """Helpers de parsing défensifs — ne jamais faire confiance au JSON brut."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, cast
6
+
7
+ from .exceptions import PushNotificationError
8
+ from .models import Device, JsonDict, Page, User
9
+
10
+
11
+ def _require_mapping(value: Any, label: str) -> JsonDict:
12
+ if not isinstance(value, dict):
13
+ raise PushNotificationError(
14
+ f"Réponse {label} invalide (objet attendu)",
15
+ code="invalid_response",
16
+ )
17
+ return cast(JsonDict, value)
18
+
19
+
20
+ def _require_str(obj: JsonDict, key: str, label: str) -> str:
21
+ val = obj.get(key)
22
+ if not isinstance(val, str) or not val:
23
+ raise PushNotificationError(
24
+ f"Réponse {label} sans champ '{key}'",
25
+ code="invalid_response",
26
+ )
27
+ return val
28
+
29
+
30
+ def parse_device(raw: Any) -> Device:
31
+ obj = _require_mapping(raw, "appareil")
32
+ device: Device = {
33
+ "id": _require_str(obj, "id", "appareil"),
34
+ "platform": str(obj.get("platform") or "unknown"),
35
+ "status": str(obj.get("status") or "unknown"),
36
+ "user_id": obj.get("user_id") if isinstance(obj.get("user_id"), str) else None,
37
+ "created_at": str(obj.get("created_at") or ""),
38
+ "updated_at": str(obj.get("updated_at") or ""),
39
+ }
40
+ if "last_seen_at" in obj:
41
+ device["last_seen_at"] = (
42
+ obj["last_seen_at"] if isinstance(obj["last_seen_at"], str) else None
43
+ )
44
+ if "external_user_id" in obj:
45
+ device["external_user_id"] = (
46
+ obj["external_user_id"]
47
+ if isinstance(obj["external_user_id"], str)
48
+ else None
49
+ )
50
+ if isinstance(obj.get("token_preview"), str):
51
+ device["token_preview"] = obj["token_preview"]
52
+ if isinstance(obj.get("token_kind"), str):
53
+ device["token_kind"] = obj["token_kind"]
54
+ if isinstance(obj.get("push_ready"), bool):
55
+ device["push_ready"] = obj["push_ready"]
56
+ return device
57
+
58
+
59
+ def parse_user(raw: Any) -> User:
60
+ obj = _require_mapping(raw, "utilisateur")
61
+ return {
62
+ "id": _require_str(obj, "id", "utilisateur"),
63
+ "external_id": _require_str(obj, "external_id", "utilisateur"),
64
+ "created_at": str(obj.get("created_at") or ""),
65
+ "updated_at": str(obj.get("updated_at") or ""),
66
+ }
67
+
68
+
69
+ def parse_tags(raw: Any) -> dict[str, str]:
70
+ obj = _require_mapping(raw, "tags")
71
+ tags_raw = obj.get("tags", {})
72
+ if not isinstance(tags_raw, dict):
73
+ return {}
74
+ out: dict[str, str] = {}
75
+ for key, value in tags_raw.items():
76
+ if isinstance(key, str) and isinstance(value, str):
77
+ out[key] = value
78
+ elif isinstance(key, str) and value is not None:
79
+ out[key] = str(value)
80
+ return out
81
+
82
+
83
+ def parse_page(raw: Any, *, item_label: str = "item") -> Page:
84
+ """
85
+ Accepte l’enveloppe Page moderne **ou** un tableau legacy.
86
+ Toujours retourne {items,total,limit,offset,has_more}.
87
+ """
88
+ if isinstance(raw, list):
89
+ items = list(raw)
90
+ return {
91
+ "items": items,
92
+ "total": len(items),
93
+ "limit": len(items) or 25,
94
+ "offset": 0,
95
+ "has_more": False,
96
+ }
97
+
98
+ obj = _require_mapping(raw, f"page {item_label}")
99
+ items_raw = obj.get("items")
100
+ if not isinstance(items_raw, list):
101
+ # Ancien shape accidentel : objet unique
102
+ raise PushNotificationError(
103
+ f"Réponse page {item_label} sans 'items'",
104
+ code="invalid_response",
105
+ )
106
+
107
+ total = obj.get("total")
108
+ limit = obj.get("limit")
109
+ offset = obj.get("offset")
110
+ has_more = obj.get("has_more")
111
+
112
+ total_i = int(total) if isinstance(total, (int, float)) else len(items_raw)
113
+ limit_i = int(limit) if isinstance(limit, (int, float)) else max(len(items_raw), 1)
114
+ offset_i = int(offset) if isinstance(offset, (int, float)) else 0
115
+ has_more_b = (
116
+ bool(has_more)
117
+ if isinstance(has_more, bool)
118
+ else offset_i + len(items_raw) < total_i
119
+ )
120
+
121
+ return {
122
+ "items": items_raw,
123
+ "total": total_i,
124
+ "limit": limit_i,
125
+ "offset": offset_i,
126
+ "has_more": has_more_b,
127
+ }