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.
- hackbou_push_notification-0.1.0/PKG-INFO +122 -0
- hackbou_push_notification-0.1.0/README.md +90 -0
- hackbou_push_notification-0.1.0/hackbou_push_notification.egg-info/PKG-INFO +122 -0
- hackbou_push_notification-0.1.0/hackbou_push_notification.egg-info/SOURCES.txt +18 -0
- hackbou_push_notification-0.1.0/hackbou_push_notification.egg-info/dependency_links.txt +1 -0
- hackbou_push_notification-0.1.0/hackbou_push_notification.egg-info/requires.txt +12 -0
- hackbou_push_notification-0.1.0/hackbou_push_notification.egg-info/top_level.txt +1 -0
- hackbou_push_notification-0.1.0/push_notification/__init__.py +27 -0
- hackbou_push_notification-0.1.0/push_notification/_parse.py +127 -0
- hackbou_push_notification-0.1.0/push_notification/client.py +459 -0
- hackbou_push_notification-0.1.0/push_notification/django.py +101 -0
- hackbou_push_notification-0.1.0/push_notification/exceptions.py +55 -0
- hackbou_push_notification-0.1.0/push_notification/models.py +83 -0
- hackbou_push_notification-0.1.0/push_notification/py.typed +1 -0
- hackbou_push_notification-0.1.0/pyproject.toml +51 -0
- hackbou_push_notification-0.1.0/setup.cfg +4 -0
- hackbou_push_notification-0.1.0/setup.py +3 -0
- hackbou_push_notification-0.1.0/tests/test_client.py +189 -0
- hackbou_push_notification-0.1.0/tests/test_exceptions.py +33 -0
- hackbou_push_notification-0.1.0/tests/test_parse.py +83 -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
push_notification
|
|
@@ -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
|
+
}
|