easyid-python 1.0.2__py3-none-any.whl

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.
easyid/__init__.py ADDED
@@ -0,0 +1,39 @@
1
+ """EasyID Python SDK."""
2
+
3
+ from .bank import BankService, BankVerify4Result
4
+ from .billing import BillingRecord, BillingRecordsResult, BillingService, BillingBalanceResult
5
+ from .client import EasyID
6
+ from .error import APIError, is_api_error
7
+ from .face import CompareResult, FaceService, FaceVerifyResult, LivenessResult
8
+ from .idcard import IDCardOCRResult, IDCardService, IDCardVerifyResult
9
+ from .phone import PhoneService, PhoneStatusResult, PhoneVerify3Result
10
+ from .risk import RiskDetails, RiskScoreResult, RiskService, StoreFingerprintResult
11
+
12
+ __version__ = EasyID.version
13
+
14
+ __all__ = [
15
+ "APIError",
16
+ "BankService",
17
+ "BankVerify4Result",
18
+ "BillingBalanceResult",
19
+ "BillingRecord",
20
+ "BillingRecordsResult",
21
+ "BillingService",
22
+ "CompareResult",
23
+ "EasyID",
24
+ "FaceService",
25
+ "FaceVerifyResult",
26
+ "IDCardOCRResult",
27
+ "IDCardService",
28
+ "IDCardVerifyResult",
29
+ "LivenessResult",
30
+ "PhoneService",
31
+ "PhoneStatusResult",
32
+ "PhoneVerify3Result",
33
+ "RiskDetails",
34
+ "RiskScoreResult",
35
+ "RiskService",
36
+ "StoreFingerprintResult",
37
+ "__version__",
38
+ "is_api_error",
39
+ ]
easyid/bank.py ADDED
@@ -0,0 +1,48 @@
1
+ """Bank APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Optional
7
+
8
+ from .transport import Transport
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class BankVerify4Result:
13
+ result: bool
14
+ match: bool
15
+ bank_name: str
16
+ supplier: str
17
+ score: float
18
+ masked_bank_card: str
19
+ card_type: str
20
+
21
+
22
+ class BankService:
23
+ def __init__(self, transport: Transport) -> None:
24
+ self._transport = transport
25
+
26
+ def verify4(
27
+ self,
28
+ *,
29
+ name: str,
30
+ id_number: str,
31
+ bank_card: str,
32
+ mobile: Optional[str] = None,
33
+ trace_id: Optional[str] = None,
34
+ ) -> BankVerify4Result:
35
+ body = {
36
+ "name": name,
37
+ "id_number": id_number,
38
+ "bank_card": bank_card,
39
+ "mobile": mobile,
40
+ "trace_id": trace_id,
41
+ }
42
+ data = self._transport.request_json(
43
+ "POST",
44
+ "/v1/bank/verify4",
45
+ body={key: value for key, value in body.items() if value is not None},
46
+ )
47
+ return BankVerify4Result(**data)
48
+
easyid/billing.py ADDED
@@ -0,0 +1,62 @@
1
+ """Billing APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import List
7
+
8
+ from .transport import Transport
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class BillingBalanceResult:
13
+ app_id: str
14
+ available_cents: int
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class BillingRecord:
19
+ id: int
20
+ app_id: str
21
+ request_id: str
22
+ change_cents: int
23
+ balance_before: int
24
+ balance_after: int
25
+ reason: str
26
+ operator: str
27
+ created_at: int
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class BillingRecordsResult:
32
+ total: int
33
+ page: int
34
+ records: List[BillingRecord]
35
+
36
+
37
+ class BillingService:
38
+ def __init__(self, transport: Transport) -> None:
39
+ self._transport = transport
40
+
41
+ def balance(self, *, app_id: str) -> BillingBalanceResult:
42
+ data = self._transport.request_json(
43
+ "GET",
44
+ "/v1/billing/balance",
45
+ query={"app_id": app_id},
46
+ )
47
+ return BillingBalanceResult(**data)
48
+
49
+ def records(self, *, app_id: str, page: int = 1, page_size: int = 20) -> BillingRecordsResult:
50
+ if page <= 0:
51
+ page = 1
52
+ if page_size <= 0:
53
+ page_size = 20
54
+ else:
55
+ page_size = min(page_size, 100)
56
+ data = self._transport.request_json(
57
+ "GET",
58
+ "/v1/billing/records",
59
+ query={"app_id": app_id, "page": page, "page_size": page_size},
60
+ )
61
+ records = [BillingRecord(**record) for record in data.get("records", [])]
62
+ return BillingRecordsResult(total=data["total"], page=data["page"], records=records)
easyid/client.py ADDED
@@ -0,0 +1,56 @@
1
+ """Public client entrypoint for the EasyID SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Optional
7
+
8
+ import requests
9
+
10
+ from .bank import BankService
11
+ from .billing import BillingService
12
+ from .face import FaceService
13
+ from .idcard import IDCardService
14
+ from .phone import PhoneService
15
+ from .risk import RiskService
16
+ from .transport import DEFAULT_BASE_URL, SDK_VERSION, Transport
17
+
18
+ _KEY_ID_RE = re.compile(r"^ak_[0-9a-f]+$")
19
+
20
+
21
+ class EasyID:
22
+ """EasyID API client.
23
+
24
+ The client is safe to reuse across requests.
25
+ """
26
+
27
+ version = SDK_VERSION
28
+
29
+ def __init__(
30
+ self,
31
+ key_id: str,
32
+ secret: str,
33
+ *,
34
+ base_url: str = DEFAULT_BASE_URL,
35
+ timeout: float = 30.0,
36
+ session: Optional[requests.Session] = None,
37
+ ) -> None:
38
+ if not _KEY_ID_RE.fullmatch(key_id):
39
+ raise ValueError(f"easyid: key_id must match ak_<hex>, got: {key_id}")
40
+ if not secret:
41
+ raise ValueError("easyid: secret must not be empty")
42
+
43
+ self._transport = Transport(
44
+ key_id=key_id,
45
+ secret=secret,
46
+ base_url=base_url,
47
+ timeout=timeout,
48
+ session=session,
49
+ )
50
+ self.idcard = IDCardService(self._transport)
51
+ self.phone = PhoneService(self._transport)
52
+ self.face = FaceService(self._transport)
53
+ self.bank = BankService(self._transport)
54
+ self.risk = RiskService(self._transport)
55
+ self.billing = BillingService(self._transport)
56
+
easyid/error.py ADDED
@@ -0,0 +1,20 @@
1
+ """Error types for the EasyID SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class APIError(Exception):
7
+ """Business error returned by the EasyID API."""
8
+
9
+ def __init__(self, code: int, message: str, request_id: str) -> None:
10
+ self.code = code
11
+ self.message = message
12
+ self.request_id = request_id
13
+ super().__init__(f"easyid: code={code} message={message} request_id={request_id}")
14
+
15
+
16
+ def is_api_error(exc: BaseException) -> bool:
17
+ """Return True when *exc* is an :class:`APIError`."""
18
+
19
+ return isinstance(exc, APIError)
20
+
easyid/face.py ADDED
@@ -0,0 +1,85 @@
1
+ """Face APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Optional
7
+
8
+ from .transport import FileInput, Transport, make_multipart_part
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class LivenessResult:
13
+ liveness: bool
14
+ score: float
15
+ method: str
16
+ frames_analyzed: int
17
+ attack_type: Optional[str]
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class CompareResult:
22
+ match: bool
23
+ score: float
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class FaceVerifyResult:
28
+ result: bool
29
+ supplier: str
30
+ score: float
31
+
32
+
33
+ class FaceService:
34
+ def __init__(self, transport: Transport) -> None:
35
+ self._transport = transport
36
+
37
+ def liveness(
38
+ self,
39
+ *,
40
+ media: FileInput,
41
+ mode: Optional[str] = None,
42
+ filename: Optional[str] = None,
43
+ ) -> LivenessResult:
44
+ fields = {}
45
+ if mode is not None:
46
+ fields["mode"] = mode
47
+ data = self._transport.request_multipart(
48
+ "/v1/face/liveness",
49
+ fields=fields,
50
+ files=[make_multipart_part("media", media, filename)],
51
+ )
52
+ return LivenessResult(**data)
53
+
54
+ def compare(
55
+ self,
56
+ *,
57
+ image1: FileInput,
58
+ image2: FileInput,
59
+ filename1: Optional[str] = None,
60
+ filename2: Optional[str] = None,
61
+ ) -> CompareResult:
62
+ data = self._transport.request_multipart(
63
+ "/v1/face/compare",
64
+ files=[
65
+ make_multipart_part("image1", image1, filename1),
66
+ make_multipart_part("image2", image2, filename2),
67
+ ],
68
+ )
69
+ return CompareResult(**data)
70
+
71
+ def verify(
72
+ self,
73
+ *,
74
+ id_number: str,
75
+ media_key: Optional[str] = None,
76
+ callback_url: Optional[str] = None,
77
+ ) -> FaceVerifyResult:
78
+ body = {"id_number": id_number}
79
+ if media_key is not None:
80
+ body["media_key"] = media_key
81
+ if callback_url is not None:
82
+ body["callback_url"] = callback_url
83
+ data = self._transport.request_json("POST", "/v1/face/verify", body=body)
84
+ return FaceVerifyResult(**data)
85
+
easyid/idcard.py ADDED
@@ -0,0 +1,77 @@
1
+ """ID card APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Dict, Optional
7
+
8
+ from .transport import FileInput, Transport, make_multipart_part
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class IDCardVerifyResult:
13
+ result: bool
14
+ match: bool
15
+ supplier: str
16
+ score: float
17
+ raw: Optional[Dict[str, Any]] = None
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class IDCardOCRResult:
22
+ side: str
23
+ name: str = ""
24
+ id_number: str = ""
25
+ gender: str = ""
26
+ nation: str = ""
27
+ birth: str = ""
28
+ address: str = ""
29
+ issue: str = ""
30
+ valid: str = ""
31
+
32
+
33
+ class IDCardService:
34
+ def __init__(self, transport: Transport) -> None:
35
+ self._transport = transport
36
+
37
+ def verify2(self, *, name: str, id_number: str, trace_id: Optional[str] = None) -> IDCardVerifyResult:
38
+ data = self._transport.request_json(
39
+ "POST",
40
+ "/v1/idcard/verify2",
41
+ body=_compact({"name": name, "id_number": id_number, "trace_id": trace_id}),
42
+ )
43
+ return IDCardVerifyResult(**data)
44
+
45
+ def verify3(
46
+ self,
47
+ *,
48
+ name: str,
49
+ id_number: str,
50
+ mobile: str,
51
+ trace_id: Optional[str] = None,
52
+ ) -> IDCardVerifyResult:
53
+ data = self._transport.request_json(
54
+ "POST",
55
+ "/v1/idcard/verify3",
56
+ body=_compact(
57
+ {
58
+ "name": name,
59
+ "id_number": id_number,
60
+ "mobile": mobile,
61
+ "trace_id": trace_id,
62
+ }
63
+ ),
64
+ )
65
+ return IDCardVerifyResult(**data)
66
+
67
+ def ocr(self, *, side: str, image: FileInput, filename: Optional[str] = None) -> IDCardOCRResult:
68
+ data = self._transport.request_multipart(
69
+ "/v1/ocr/idcard",
70
+ fields={"side": side},
71
+ files=[make_multipart_part("image", image, filename)],
72
+ )
73
+ return IDCardOCRResult(**data)
74
+
75
+
76
+ def _compact(payload: Dict[str, Any]) -> Dict[str, Any]:
77
+ return {key: value for key, value in payload.items() if value is not None}
easyid/phone.py ADDED
@@ -0,0 +1,44 @@
1
+ """Phone APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from .transport import Transport
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class PhoneStatusResult:
12
+ status: str
13
+ carrier: str
14
+ province: str
15
+ roaming: bool
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class PhoneVerify3Result:
20
+ result: bool
21
+ match: bool
22
+ supplier: str
23
+ score: float
24
+
25
+
26
+ class PhoneService:
27
+ def __init__(self, transport: Transport) -> None:
28
+ self._transport = transport
29
+
30
+ def status(self, *, phone: str) -> PhoneStatusResult:
31
+ data = self._transport.request_json(
32
+ "GET",
33
+ "/v1/phone/status",
34
+ query={"phone": phone},
35
+ )
36
+ return PhoneStatusResult(**data)
37
+
38
+ def verify3(self, *, name: str, id_number: str, mobile: str) -> PhoneVerify3Result:
39
+ data = self._transport.request_json(
40
+ "POST",
41
+ "/v1/phone/verify3",
42
+ body={"name": name, "id_number": id_number, "mobile": mobile},
43
+ )
44
+ return PhoneVerify3Result(**data)
easyid/py.typed ADDED
@@ -0,0 +1 @@
1
+
easyid/risk.py ADDED
@@ -0,0 +1,82 @@
1
+ """Risk APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ from .transport import Transport
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class RiskDetails:
13
+ rule_score: Optional[int]
14
+ ml_score: Optional[int]
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class RiskScoreResult:
19
+ risk_score: int
20
+ reasons: List[str]
21
+ recommendation: str
22
+ details: RiskDetails
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class StoreFingerprintResult:
27
+ device_id: str
28
+ stored: bool
29
+
30
+
31
+ class RiskService:
32
+ def __init__(self, transport: Transport) -> None:
33
+ self._transport = transport
34
+
35
+ def score(
36
+ self,
37
+ *,
38
+ ip: Optional[str] = None,
39
+ device_fingerprint: Optional[str] = None,
40
+ device_id: Optional[str] = None,
41
+ phone: Optional[str] = None,
42
+ email: Optional[str] = None,
43
+ user_agent: Optional[str] = None,
44
+ action: Optional[str] = None,
45
+ amount: Optional[int] = None,
46
+ context: Optional[Dict[str, Any]] = None,
47
+ ) -> RiskScoreResult:
48
+ body = {
49
+ "ip": ip,
50
+ "device_fingerprint": device_fingerprint,
51
+ "device_id": device_id,
52
+ "phone": phone,
53
+ "email": email,
54
+ "user_agent": user_agent,
55
+ "action": action,
56
+ "amount": amount,
57
+ "context": context,
58
+ }
59
+ data = self._transport.request_json(
60
+ "POST",
61
+ "/v1/risk/score",
62
+ body={key: value for key, value in body.items() if value is not None},
63
+ )
64
+ details = data.get("details", {})
65
+ return RiskScoreResult(
66
+ risk_score=data["risk_score"],
67
+ reasons=list(data.get("reasons", [])),
68
+ recommendation=data["recommendation"],
69
+ details=RiskDetails(
70
+ rule_score=details.get("rule_score"),
71
+ ml_score=details.get("ml_score"),
72
+ ),
73
+ )
74
+
75
+ def store_fingerprint(self, *, device_id: str, fingerprint: Dict[str, Any]) -> StoreFingerprintResult:
76
+ data = self._transport.request_json(
77
+ "POST",
78
+ "/v1/device/fingerprint",
79
+ body={"device_id": device_id, "fingerprint": fingerprint},
80
+ )
81
+ return StoreFingerprintResult(**data)
82
+
easyid/signer.py ADDED
@@ -0,0 +1,24 @@
1
+ """Signing helpers for the EasyID SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import hmac
7
+ from typing import Mapping, Optional
8
+
9
+
10
+ def sign(secret: str, timestamp: str, query: Optional[Mapping[str, str]], body: bytes) -> str:
11
+ """Return the lowercase hex HMAC-SHA256 signature for a request."""
12
+
13
+ parts = []
14
+ if query:
15
+ for key in sorted(query):
16
+ parts.append(f"{key}={query[key]}")
17
+ payload = "&".join(parts)
18
+ if body:
19
+ body_text = body.decode("utf-8")
20
+ payload = f"{payload}&{body_text}" if payload else body_text
21
+ to_sign = f"{timestamp}\n{payload}"
22
+ digest = hmac.new(secret.encode("utf-8"), to_sign.encode("utf-8"), hashlib.sha256)
23
+ return digest.hexdigest()
24
+
easyid/transport.py ADDED
@@ -0,0 +1,202 @@
1
+ """HTTP transport for the EasyID SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import time
8
+ from dataclasses import dataclass
9
+ from typing import Any, BinaryIO, Dict, Iterable, Mapping, Optional, Tuple, Union
10
+ from urllib.parse import urlencode
11
+
12
+ import requests
13
+
14
+ from .error import APIError
15
+ from .signer import sign
16
+
17
+ DEFAULT_BASE_URL = "https://api.easyid.com"
18
+ MAX_RESPONSE_BYTES = 10 << 20
19
+ SDK_VERSION = "1.0.0"
20
+
21
+ FileInput = Union[bytes, bytearray, BinaryIO]
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class MultipartPart:
26
+ field_name: str
27
+ filename: str
28
+ content: bytes
29
+ content_type: str = "application/octet-stream"
30
+
31
+
32
+ class Transport:
33
+ """Low-level HTTP transport shared by all services."""
34
+
35
+ def __init__(
36
+ self,
37
+ *,
38
+ key_id: str,
39
+ secret: str,
40
+ base_url: str = DEFAULT_BASE_URL,
41
+ timeout: float = 30.0,
42
+ session: Optional[requests.Session] = None,
43
+ user_agent: Optional[str] = None,
44
+ ) -> None:
45
+ self.key_id = key_id
46
+ self.secret = secret
47
+ self.base_url = base_url.rstrip("/")
48
+ self.timeout = timeout
49
+ self.session = session or requests.Session()
50
+ self.user_agent = user_agent or f"easyid-python/{SDK_VERSION}"
51
+
52
+ def request_json(
53
+ self,
54
+ method: str,
55
+ path: str,
56
+ *,
57
+ query: Optional[Mapping[str, Any]] = None,
58
+ body: Optional[Mapping[str, Any]] = None,
59
+ ) -> Any:
60
+ body_bytes = b""
61
+ if body is not None:
62
+ body_bytes = json.dumps(
63
+ body,
64
+ ensure_ascii=False,
65
+ separators=(",", ":"),
66
+ ).encode("utf-8")
67
+ return self._send(
68
+ method=method,
69
+ path=path,
70
+ query=self._normalize_query(query),
71
+ body_bytes=body_bytes,
72
+ content_type="application/json",
73
+ )
74
+
75
+ def request_multipart(
76
+ self,
77
+ path: str,
78
+ *,
79
+ fields: Optional[Mapping[str, str]] = None,
80
+ files: Optional[Iterable[MultipartPart]] = None,
81
+ ) -> Any:
82
+ request = requests.Request(
83
+ method="POST",
84
+ url=self.base_url + path,
85
+ data=fields or {},
86
+ files=[
87
+ (
88
+ part.field_name,
89
+ (part.filename, part.content, part.content_type),
90
+ )
91
+ for part in (files or [])
92
+ ],
93
+ )
94
+ prepared = self.session.prepare_request(request)
95
+ body = prepared.body
96
+ if body is None:
97
+ body_bytes = b""
98
+ elif isinstance(body, bytes):
99
+ body_bytes = body
100
+ else:
101
+ body_bytes = body.encode("utf-8")
102
+ return self._send(
103
+ method="POST",
104
+ path=path,
105
+ query=None,
106
+ body_bytes=body_bytes,
107
+ content_type=prepared.headers["Content-Type"],
108
+ )
109
+
110
+ def _send(
111
+ self,
112
+ *,
113
+ method: str,
114
+ path: str,
115
+ query: Optional[Dict[str, str]],
116
+ body_bytes: bytes,
117
+ content_type: str,
118
+ ) -> Any:
119
+ timestamp = str(int(time.time()))
120
+ signature = sign(self.secret, timestamp, query, body_bytes)
121
+
122
+ url = self.base_url + path
123
+ if query:
124
+ url = f"{url}?{urlencode(sorted(query.items()))}"
125
+
126
+ request = requests.Request(method=method, url=url, data=body_bytes)
127
+ prepared = self.session.prepare_request(request)
128
+ prepared.headers["Content-Type"] = content_type
129
+ prepared.headers["X-Key-ID"] = self.key_id
130
+ prepared.headers["X-Timestamp"] = timestamp
131
+ prepared.headers["X-Signature"] = signature
132
+ prepared.headers["User-Agent"] = self.user_agent
133
+
134
+ response = self.session.send(prepared, timeout=self.timeout, stream=True)
135
+ raw = self._read_response(response)
136
+ if response.status_code < 200 or response.status_code >= 300:
137
+ api_error = self._maybe_api_error(raw)
138
+ if api_error is not None:
139
+ raise api_error
140
+ raise requests.HTTPError(
141
+ f"easyid: http status {response.status_code}",
142
+ response=response,
143
+ )
144
+
145
+ try:
146
+ envelope = json.loads(raw.decode("utf-8"))
147
+ except json.JSONDecodeError as exc:
148
+ raise ValueError(f"easyid: decode response (status={response.status_code}): {exc}") from exc
149
+
150
+ code = int(envelope.get("code", 0))
151
+ if code != 0:
152
+ raise APIError(code, str(envelope.get("message", "")), str(envelope.get("request_id", "")))
153
+ return envelope.get("data")
154
+
155
+ def _read_response(self, response: requests.Response) -> bytes:
156
+ chunks = []
157
+ total = 0
158
+ try:
159
+ for chunk in response.iter_content(chunk_size=8192):
160
+ if not chunk:
161
+ continue
162
+ total += len(chunk)
163
+ if total > MAX_RESPONSE_BYTES:
164
+ raise ValueError("easyid: response exceeds 10 MB limit")
165
+ chunks.append(chunk)
166
+ finally:
167
+ response.close()
168
+ return b"".join(chunks)
169
+
170
+ def _maybe_api_error(self, raw: bytes) -> Optional[APIError]:
171
+ try:
172
+ envelope = json.loads(raw.decode("utf-8"))
173
+ except (UnicodeDecodeError, json.JSONDecodeError):
174
+ return None
175
+ code_value = envelope.get("code", 0)
176
+ if code_value in (None, ""):
177
+ return None
178
+ code = int(code_value)
179
+ if code == 0:
180
+ return None
181
+ return APIError(code, str(envelope.get("message", "")), str(envelope.get("request_id", "")))
182
+
183
+ @staticmethod
184
+ def _normalize_query(query: Optional[Mapping[str, Any]]) -> Optional[Dict[str, str]]:
185
+ if not query:
186
+ return None
187
+ return {str(key): str(value) for key, value in query.items()}
188
+
189
+
190
+ def make_multipart_part(field_name: str, file_obj: FileInput, filename: Optional[str]) -> MultipartPart:
191
+ """Create a :class:`MultipartPart` from bytes or a binary file-like object."""
192
+
193
+ if isinstance(file_obj, (bytes, bytearray)):
194
+ content = bytes(file_obj)
195
+ resolved_name = filename or "upload.bin"
196
+ else:
197
+ content = file_obj.read()
198
+ if not isinstance(content, bytes):
199
+ raise TypeError("easyid: file object must return bytes")
200
+ guessed_name = getattr(file_obj, "name", None)
201
+ resolved_name = filename or (os.path.basename(guessed_name) if guessed_name else "upload.bin")
202
+ return MultipartPart(field_name=field_name, filename=resolved_name, content=content)
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.4
2
+ Name: easyid-python
3
+ Version: 1.0.2
4
+ Summary: Official Python SDK for the EasyID identity verification API.
5
+ Project-URL: Homepage, https://github.com/easyid-com-cn/easyid-python
6
+ Project-URL: Repository, https://github.com/easyid-com-cn/easyid-python
7
+ Project-URL: Documentation, https://www.easyid.com.cn
8
+ Author: EasyID Team
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: easyid,identity,kyc,risk,verification
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Requires-Python: >=3.9
24
+ Requires-Dist: requests<3,>=2.31.0
25
+ Provides-Extra: test
26
+ Requires-Dist: pytest<9,>=8; extra == 'test'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # EasyID Python SDK
30
+
31
+ Official Python SDK for the EasyID identity verification API.
32
+
33
+ EasyID 易验云 focuses on identity verification and security risk control APIs, including real-name verification, liveness detection, face recognition, phone verification, and fraud-risk related capabilities.
34
+
35
+ 中文文档: [README.zh-CN.md](README.zh-CN.md)
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install easyid-python
41
+ ```
42
+
43
+ ## Quick Start
44
+
45
+ ```python
46
+ from easyid import EasyID
47
+
48
+ client = EasyID("ak_xxx", "sk_xxx")
49
+ result = client.idcard.verify2(name="张三", id_number="110101199001011234")
50
+
51
+ print(result.match)
52
+ ```
53
+
54
+ ## Supported APIs
55
+
56
+ - IDCard: `verify2`, `verify3`, `ocr`
57
+ - Phone: `status`, `verify3`
58
+ - Face: `liveness`, `compare`, `verify`
59
+ - Bank: `verify4`
60
+ - Risk: `score`, `store_fingerprint`
61
+ - Billing: `balance`, `records`
62
+
63
+ ## Configuration
64
+
65
+ - `base_url`
66
+ - `timeout`
67
+ - `session`
68
+
69
+ ## Error Handling
70
+
71
+ Service-side business errors raise `APIError`.
72
+
73
+ ```python
74
+ from easyid import APIError
75
+
76
+ try:
77
+ client.phone.status(phone="13800138000")
78
+ except APIError as exc:
79
+ print(exc.code, exc.message, exc.request_id)
80
+ ```
81
+
82
+ ## Security Notice
83
+
84
+ This is a server-side SDK. Never expose `secret` in browsers, mobile apps, or other untrusted clients.
85
+
86
+ ## Official Resources
87
+
88
+ - Official website: `https://www.easyid.com.cn/`
89
+ - GitHub organization: `https://github.com/easyid-com-cn/`
@@ -0,0 +1,16 @@
1
+ easyid/__init__.py,sha256=njg6-q6YluVxXZh-E9DQaQxW5Xt2P4NZ2Udwd_O35mo,1119
2
+ easyid/bank.py,sha256=LAPYl-5buGWQjJICTRVekarxmBzkidnvivGkI3as-xM,1081
3
+ easyid/billing.py,sha256=e-QrqSR1AFmKqDUDwfQJkdXHk5RehfsTs2tJPmcbEm8,1562
4
+ easyid/client.py,sha256=28wH-1rQcW3EJ7Fn_3ck7z5zp7WSg6xj1RmWW34d2ec,1511
5
+ easyid/error.py,sha256=i7lYC9VDU6Upa39Sew_PJJSgQiqFh9V7cT12uG9UZmQ,560
6
+ easyid/face.py,sha256=faX-B7rk9_-wN9_i4Z-Y318nY-xeaF7jOgpLHgFCcU8,2111
7
+ easyid/idcard.py,sha256=eMubWiSZUp_Caj8xc8C5MT2c-nwsn_SMZA3I23OGDJk,2083
8
+ easyid/phone.py,sha256=0urP0PQoEmqMrYW_9nTYh_fpYyttZTqlMeYhU3TbTiY,1031
9
+ easyid/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
10
+ easyid/risk.py,sha256=X9vdoXrGVbYXhppjYrr6yLCF46tASZ_QjYA3PVgGOWk,2281
11
+ easyid/signer.py,sha256=uto372PqeRJdaN_EWmyREe5OpMvJv5krkaxe8Q3PU2E,735
12
+ easyid/transport.py,sha256=7AE_tM5UBNkhtPm289nSf3JM5BfJJe3PmKkaiCZLx4I,6586
13
+ easyid_python-1.0.2.dist-info/METADATA,sha256=IG1O7BoWnC2iKz8RfDQitay86_e7-5zRMGl9JKTL2RY,2512
14
+ easyid_python-1.0.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
15
+ easyid_python-1.0.2.dist-info/licenses/LICENSE,sha256=kCoyPvOpIJB7kNFFsIm0nnd2yEcynll2t0Dg-AvJqVU,1068
16
+ easyid_python-1.0.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 EasyID Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.