zakadi 0.0.1__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.
- zakadi-0.0.1/.gitignore +6 -0
- zakadi-0.0.1/CHANGELOG.md +3 -0
- zakadi-0.0.1/LICENSE +5 -0
- zakadi-0.0.1/PKG-INFO +39 -0
- zakadi-0.0.1/README.md +14 -0
- zakadi-0.0.1/pyproject.toml +34 -0
- zakadi-0.0.1/src/zakadi/__init__.py +41 -0
- zakadi-0.0.1/src/zakadi/protocol.py +195 -0
- zakadi-0.0.1/src/zakadi/py.typed +0 -0
- zakadi-0.0.1/tests/test_protocol.py +37 -0
zakadi-0.0.1/.gitignore
ADDED
zakadi-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Copyright (c) 2026 Moses Gameli. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This is a pre-release package. The licence under which the Zakadi SDKs and this
|
|
4
|
+
package are distributed has not been finalised; until it is, no rights are
|
|
5
|
+
granted beyond downloading and evaluating the package. See https://zakadi.dev.
|
zakadi-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: zakadi
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Zakadi face-liveness API client for Python (pre-release): shared protocol constants now, the server-side client for sessions, results and webhook verification next.
|
|
5
|
+
Project-URL: Homepage, https://zakadi.dev
|
|
6
|
+
Project-URL: Documentation, https://docs.zakadi.dev
|
|
7
|
+
Project-URL: Repository, https://github.com/zakadihq/zakadi-python
|
|
8
|
+
Project-URL: Issues, https://github.com/zakadihq/zakadi-python/issues
|
|
9
|
+
Author: Moses Gameli
|
|
10
|
+
License: Copyright (c) 2026 Moses Gameli. All rights reserved.
|
|
11
|
+
|
|
12
|
+
This is a pre-release package. The licence under which the Zakadi SDKs and this
|
|
13
|
+
package are distributed has not been finalised; until it is, no rights are
|
|
14
|
+
granted beyond downloading and evaluating the package. See https://zakadi.dev.
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Keywords: biometrics,identity-verification,kyc,liveness,zakadi
|
|
17
|
+
Classifier: Development Status :: 1 - Planning
|
|
18
|
+
Classifier: Intended Audience :: Developers
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# zakadi
|
|
27
|
+
|
|
28
|
+
Zakadi face-liveness API client for Python. Zakadi is an active face liveness check delivered as a short automated video call; relying parties create sessions and read verdicts server to server.
|
|
29
|
+
|
|
30
|
+
Pre-release. This version ships the shared protocol constants (session states and statuses, decisions and bands, SDK error codes, terminal states, close codes, challenge kinds, webhook event types) with type annotations. The client for sessions, results, evidence and webhook signature verification follows in a later release; its surface is:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from zakadi import Zakadi
|
|
34
|
+
client = Zakadi(api_key="zk_live_...", base_url="https://api.zakadi.dev")
|
|
35
|
+
s = client.sessions.create(user_ref="cust-88213", locale="en-NG", channel="android")
|
|
36
|
+
r = client.sessions.result(s.session_id)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Links: https://zakadi.dev (documentation), https://github.com/zakadihq/zakadi-python (source).
|
zakadi-0.0.1/README.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# zakadi
|
|
2
|
+
|
|
3
|
+
Zakadi face-liveness API client for Python. Zakadi is an active face liveness check delivered as a short automated video call; relying parties create sessions and read verdicts server to server.
|
|
4
|
+
|
|
5
|
+
Pre-release. This version ships the shared protocol constants (session states and statuses, decisions and bands, SDK error codes, terminal states, close codes, challenge kinds, webhook event types) with type annotations. The client for sessions, results, evidence and webhook signature verification follows in a later release; its surface is:
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from zakadi import Zakadi
|
|
9
|
+
client = Zakadi(api_key="zk_live_...", base_url="https://api.zakadi.dev")
|
|
10
|
+
s = client.sessions.create(user_ref="cust-88213", locale="en-NG", channel="android")
|
|
11
|
+
r = client.sessions.result(s.session_id)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Links: https://zakadi.dev (documentation), https://github.com/zakadihq/zakadi-python (source).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zakadi"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Zakadi face-liveness API client for Python (pre-release): shared protocol constants now, the server-side client for sessions, results and webhook verification next."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
authors = [{ name = "Moses Gameli" }]
|
|
13
|
+
keywords = ["zakadi", "liveness", "biometrics", "identity-verification", "kyc"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 1 - Planning",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
+
"Topic :: Security",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
]
|
|
22
|
+
dependencies = []
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://zakadi.dev"
|
|
26
|
+
Documentation = "https://docs.zakadi.dev"
|
|
27
|
+
Repository = "https://github.com/zakadihq/zakadi-python"
|
|
28
|
+
Issues = "https://github.com/zakadihq/zakadi-python/issues"
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.wheel]
|
|
31
|
+
packages = ["src/zakadi"]
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.sdist]
|
|
34
|
+
include = ["src/zakadi", "tests", "README.md", "CHANGELOG.md", "LICENSE", "pyproject.toml"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Zakadi face-liveness API client for Python.
|
|
2
|
+
|
|
3
|
+
Pre-release. This version ships the shared protocol constants of the Zakadi
|
|
4
|
+
protocol (``zakadi.v1``); the server-side client (sessions, results, webhook
|
|
5
|
+
verification) follows in a later release. See https://zakadi.dev.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from zakadi.protocol import (
|
|
9
|
+
SUBPROTOCOL,
|
|
10
|
+
Band,
|
|
11
|
+
ChallengeKind,
|
|
12
|
+
CloseCode,
|
|
13
|
+
Decision,
|
|
14
|
+
EndOutcome,
|
|
15
|
+
EndReason,
|
|
16
|
+
ErrorCode,
|
|
17
|
+
SessionState,
|
|
18
|
+
SessionStatus,
|
|
19
|
+
TerminalState,
|
|
20
|
+
WEBHOOK_EVENTS,
|
|
21
|
+
terminal_state_for_end,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__version__ = "0.0.1"
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"__version__",
|
|
28
|
+
"SUBPROTOCOL",
|
|
29
|
+
"Band",
|
|
30
|
+
"ChallengeKind",
|
|
31
|
+
"CloseCode",
|
|
32
|
+
"Decision",
|
|
33
|
+
"EndOutcome",
|
|
34
|
+
"EndReason",
|
|
35
|
+
"ErrorCode",
|
|
36
|
+
"SessionState",
|
|
37
|
+
"SessionStatus",
|
|
38
|
+
"TerminalState",
|
|
39
|
+
"WEBHOOK_EVENTS",
|
|
40
|
+
"terminal_state_for_end",
|
|
41
|
+
]
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Constants shared by every Zakadi client and server (protocol ``zakadi.v1``)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum, IntEnum
|
|
6
|
+
|
|
7
|
+
#: The WebSocket subprotocol every Zakadi client requests.
|
|
8
|
+
SUBPROTOCOL = "zakadi.v1"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SessionState(str, Enum):
|
|
12
|
+
"""Client-side session states, in lifecycle order."""
|
|
13
|
+
|
|
14
|
+
IDLE = "idle"
|
|
15
|
+
CONSENT = "consent"
|
|
16
|
+
PERMISSION = "permission"
|
|
17
|
+
CONNECTING = "connecting"
|
|
18
|
+
ACTIVE = "active"
|
|
19
|
+
ENDED = "ended"
|
|
20
|
+
ERROR = "error"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SessionStatus(str, Enum):
|
|
24
|
+
"""Server-side session status as returned by the Zakadi API."""
|
|
25
|
+
|
|
26
|
+
CREATED = "created"
|
|
27
|
+
CONNECTED = "connected"
|
|
28
|
+
IN_PROGRESS = "in_progress"
|
|
29
|
+
VERIFYING = "verifying"
|
|
30
|
+
PASSED = "passed"
|
|
31
|
+
FAILED = "failed"
|
|
32
|
+
INCONCLUSIVE = "inconclusive"
|
|
33
|
+
EXPIRED = "expired"
|
|
34
|
+
ABORTED = "aborted"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Decision(str, Enum):
|
|
38
|
+
"""Verdict decision in a result."""
|
|
39
|
+
|
|
40
|
+
PASS = "pass"
|
|
41
|
+
FAIL = "fail"
|
|
42
|
+
INCONCLUSIVE = "inconclusive"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Band(str, Enum):
|
|
46
|
+
"""Assurance band of a session."""
|
|
47
|
+
|
|
48
|
+
A = "A"
|
|
49
|
+
B = "B"
|
|
50
|
+
C = "C"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ErrorCode(str, Enum):
|
|
54
|
+
"""SDK error codes. Runtime conditions only; API misuse raises a usage error."""
|
|
55
|
+
|
|
56
|
+
CONSENT_DECLINED = "consent_declined"
|
|
57
|
+
CANCELLED = "cancelled"
|
|
58
|
+
PERMISSION_DENIED = "permission_denied"
|
|
59
|
+
UNSUPPORTED_DEVICE = "unsupported_device"
|
|
60
|
+
SDK_DISABLED = "sdk_disabled"
|
|
61
|
+
PACK_UNAVAILABLE = "pack_unavailable"
|
|
62
|
+
NETWORK_UNAVAILABLE = "network_unavailable"
|
|
63
|
+
AUTH_ERROR = "auth_error"
|
|
64
|
+
SESSION_EXPIRED = "session_expired"
|
|
65
|
+
SESSION_USED = "session_used"
|
|
66
|
+
MAX_DURATION = "max_duration"
|
|
67
|
+
ADMISSION_REJECTED = "admission_rejected"
|
|
68
|
+
NETWORK_FLOOR = "network_floor"
|
|
69
|
+
PROTOCOL_ERROR = "protocol_error"
|
|
70
|
+
INTERRUPTED = "interrupted"
|
|
71
|
+
CAPTURE_ERROR = "capture_error"
|
|
72
|
+
ENCODER_ERROR = "encoder_error"
|
|
73
|
+
INTERNAL = "internal"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TerminalState(str, Enum):
|
|
77
|
+
"""Terminal UI states of a session on the client."""
|
|
78
|
+
|
|
79
|
+
COMPLETED = "completed"
|
|
80
|
+
INCOMPLETE = "incomplete"
|
|
81
|
+
DISCONNECTED = "disconnected"
|
|
82
|
+
NETWORK_FLOOR = "network_floor"
|
|
83
|
+
CANCELLED = "cancelled"
|
|
84
|
+
ERROR = "error"
|
|
85
|
+
UNSUPPORTED_DEVICE = "unsupported_device"
|
|
86
|
+
PERMISSION_DENIED = "permission_denied"
|
|
87
|
+
INTERRUPTED = "interrupted"
|
|
88
|
+
SDK_DISABLED = "sdk_disabled"
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def cue(self) -> str:
|
|
92
|
+
"""The prompt-pack cue the SDK may play locally for this state."""
|
|
93
|
+
return _TERMINAL_CUES[self]
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def offers_redial(self) -> bool:
|
|
97
|
+
"""Whether the SDK offers a redial action in this state."""
|
|
98
|
+
return self in _REDIAL_STATES
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
_TERMINAL_CUES: dict[TerminalState, str] = {
|
|
102
|
+
TerminalState.COMPLETED: "done.thanks",
|
|
103
|
+
TerminalState.INCOMPLETE: "fail.one_more_step",
|
|
104
|
+
TerminalState.DISCONNECTED: "net.dropped",
|
|
105
|
+
TerminalState.NETWORK_FLOOR: "net.slow",
|
|
106
|
+
TerminalState.CANCELLED: "end.cancelled",
|
|
107
|
+
TerminalState.ERROR: "end.error",
|
|
108
|
+
TerminalState.UNSUPPORTED_DEVICE: "end.unsupported",
|
|
109
|
+
TerminalState.PERMISSION_DENIED: "end.permission",
|
|
110
|
+
TerminalState.INTERRUPTED: "end.interrupted",
|
|
111
|
+
TerminalState.SDK_DISABLED: "end.disabled",
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_REDIAL_STATES = frozenset(
|
|
115
|
+
{
|
|
116
|
+
TerminalState.INCOMPLETE,
|
|
117
|
+
TerminalState.DISCONNECTED,
|
|
118
|
+
TerminalState.NETWORK_FLOOR,
|
|
119
|
+
TerminalState.ERROR,
|
|
120
|
+
TerminalState.INTERRUPTED,
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class EndOutcome(str, Enum):
|
|
126
|
+
"""Outcome carried by the server's ``end`` message."""
|
|
127
|
+
|
|
128
|
+
COMPLETED = "completed"
|
|
129
|
+
ABORTED = "aborted"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class EndReason(str, Enum):
|
|
133
|
+
"""Reason carried by the server's ``end`` message."""
|
|
134
|
+
|
|
135
|
+
OK = "ok"
|
|
136
|
+
FLOOR_BREACHED = "floor_breached"
|
|
137
|
+
MAX_DURATION = "max_duration"
|
|
138
|
+
USER_CANCEL = "user_cancel"
|
|
139
|
+
ATTEMPTS_EXHAUSTED = "attempts_exhausted"
|
|
140
|
+
SERVER_ERROR = "server_error"
|
|
141
|
+
ADMISSION = "admission"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def terminal_state_for_end(reason: EndReason) -> TerminalState:
|
|
145
|
+
"""Map an ``end`` reason to the terminal UI state the SDK shows."""
|
|
146
|
+
return _END_TO_TERMINAL[reason]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
_END_TO_TERMINAL: dict[EndReason, TerminalState] = {
|
|
150
|
+
EndReason.OK: TerminalState.COMPLETED,
|
|
151
|
+
EndReason.ATTEMPTS_EXHAUSTED: TerminalState.INCOMPLETE,
|
|
152
|
+
EndReason.MAX_DURATION: TerminalState.INCOMPLETE,
|
|
153
|
+
EndReason.FLOOR_BREACHED: TerminalState.NETWORK_FLOOR,
|
|
154
|
+
EndReason.USER_CANCEL: TerminalState.CANCELLED,
|
|
155
|
+
EndReason.SERVER_ERROR: TerminalState.ERROR,
|
|
156
|
+
EndReason.ADMISSION: TerminalState.ERROR,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class CloseCode(IntEnum):
|
|
161
|
+
"""Application-level WebSocket close codes."""
|
|
162
|
+
|
|
163
|
+
NORMAL = 1000
|
|
164
|
+
TOKEN_INVALID = 4001
|
|
165
|
+
TOKEN_EXPIRED = 4002
|
|
166
|
+
SESSION_NOT_FOUND = 4003
|
|
167
|
+
SESSION_USED = 4004
|
|
168
|
+
UNSUPPORTED_CAPABILITIES = 4005
|
|
169
|
+
PROTOCOL_VIOLATION = 4006
|
|
170
|
+
MEDIA_FLOOR_BREACHED = 4007
|
|
171
|
+
ADMISSION_REJECTED = 4008
|
|
172
|
+
MAX_DURATION_EXCEEDED = 4009
|
|
173
|
+
CANCELLED_BY_USER = 4010
|
|
174
|
+
INTERNAL_ERROR = 4011
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class ChallengeKind(str, Enum):
|
|
178
|
+
"""Challenge kinds the server may issue."""
|
|
179
|
+
|
|
180
|
+
HEAD_TURN = "head_turn"
|
|
181
|
+
DISTANCE = "distance"
|
|
182
|
+
FINGERS = "fingers"
|
|
183
|
+
DIGITS = "digits"
|
|
184
|
+
BLINK = "blink"
|
|
185
|
+
EXPRESSION = "expression"
|
|
186
|
+
HAND_OVER_FACE = "hand_over_face"
|
|
187
|
+
LOOK_PROFILE = "look_profile"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
#: Webhook event types delivered to relying parties.
|
|
191
|
+
WEBHOOK_EVENTS: tuple[str, ...] = (
|
|
192
|
+
"zakadi.session.completed",
|
|
193
|
+
"zakadi.session.aborted",
|
|
194
|
+
"zakadi.session.expired",
|
|
195
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from zakadi import (
|
|
4
|
+
SUBPROTOCOL,
|
|
5
|
+
CloseCode,
|
|
6
|
+
EndReason,
|
|
7
|
+
ErrorCode,
|
|
8
|
+
TerminalState,
|
|
9
|
+
__version__,
|
|
10
|
+
terminal_state_for_end,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProtocolTests(unittest.TestCase):
|
|
15
|
+
def test_subprotocol(self) -> None:
|
|
16
|
+
self.assertEqual(SUBPROTOCOL, "zakadi.v1")
|
|
17
|
+
self.assertEqual(__version__, "0.0.1")
|
|
18
|
+
|
|
19
|
+
def test_error_codes(self) -> None:
|
|
20
|
+
self.assertEqual(len(ErrorCode), 18)
|
|
21
|
+
self.assertEqual(ErrorCode("interrupted"), ErrorCode.INTERRUPTED)
|
|
22
|
+
with self.assertRaises(ValueError):
|
|
23
|
+
ErrorCode("not_a_code")
|
|
24
|
+
|
|
25
|
+
def test_redial(self) -> None:
|
|
26
|
+
self.assertTrue(TerminalState.INCOMPLETE.offers_redial)
|
|
27
|
+
self.assertFalse(TerminalState.COMPLETED.offers_redial)
|
|
28
|
+
self.assertEqual(TerminalState.INCOMPLETE.cue, "fail.one_more_step")
|
|
29
|
+
|
|
30
|
+
def test_end_mapping(self) -> None:
|
|
31
|
+
self.assertEqual(terminal_state_for_end(EndReason.OK), TerminalState.COMPLETED)
|
|
32
|
+
self.assertEqual(terminal_state_for_end(EndReason.MAX_DURATION), TerminalState.INCOMPLETE)
|
|
33
|
+
self.assertEqual(CloseCode.CANCELLED_BY_USER, 4010)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
unittest.main()
|