echoscan 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,128 @@
1
+ Metadata-Version: 2.4
2
+ Name: echoscan
3
+ Version: 0.1.0
4
+ Summary: EchoScan package for Python
5
+ Author: EchoScan Team
6
+ License: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+
10
+ # `echoscan` (Python)
11
+
12
+ 中文 | [English](#english)
13
+
14
+ ## 中文
15
+
16
+ ### 可用调用方式
17
+
18
+ - `EchoScanLiteClient()`
19
+ - `EchoScanProClient(api_key=...)`
20
+
21
+ 方法:
22
+
23
+ - Lite: `get_report(imprint)`
24
+ - Pro: `get_report(imprint)`, `get_history(imprint, days=..., from_=..., to=..., recent=...)`
25
+
26
+ History 查询:
27
+
28
+ 仅 Pro 客户端可用(需要 Pro API Key)。
29
+
30
+ ### 返回值与错误
31
+
32
+ `get_report()` 顶层结构:
33
+
34
+ ```json
35
+ {
36
+ "analysis": {},
37
+ "lyingCount": 0,
38
+ "projection": {}
39
+ }
40
+ ```
41
+
42
+ `get_history()` 顶层结构:
43
+
44
+ ```json
45
+ {
46
+ "imprint": "fp_session_...",
47
+ "range": {},
48
+ "recent": [],
49
+ "summary": {},
50
+ "timeline": []
51
+ }
52
+ ```
53
+
54
+ 完整字段说明链接:[echoscan](https://echoscan.org)
55
+
56
+
57
+ ### 示例
58
+
59
+ ```python
60
+ import os
61
+ from echoscan import EchoScanProClient
62
+
63
+ pro = EchoScanProClient(api_key=os.environ["ECHOSCAN_PRO_KEY"])
64
+ report = pro.get_report("fp_session_123")
65
+ history = pro.get_history("fp_session_123", from_="2026-03-01", to="2026-03-18", recent=20)
66
+
67
+ print(report)
68
+ print(history)
69
+ ```
70
+
71
+ ---
72
+
73
+ ## English
74
+
75
+ ### Usage
76
+
77
+ - `EchoScanLiteClient()`
78
+ - `EchoScanProClient(api_key=...)`
79
+
80
+ Methods:
81
+
82
+ - Lite: `get_report(imprint)`
83
+ - Pro: `get_report(imprint)`, `get_history(imprint, days=..., from_=..., to=..., recent=...)`
84
+
85
+ History query:
86
+
87
+ Pro-only (requires a Pro API key).
88
+
89
+ ### Response and errors
90
+
91
+ `get_report()` top-level shape:
92
+
93
+ ```json
94
+ {
95
+ "analysis": {},
96
+ "lyingCount": 0,
97
+ "projection": {}
98
+ }
99
+ ```
100
+
101
+ `get_history()` top-level shape:
102
+
103
+ ```json
104
+ {
105
+ "imprint": "fp_session_...",
106
+ "range": {},
107
+ "recent": [],
108
+ "summary": {},
109
+ "timeline": []
110
+ }
111
+ ```
112
+
113
+ Full field reference: [echoscan](https://echoscan.org)
114
+
115
+
116
+ ### Example
117
+
118
+ ```python
119
+ import os
120
+ from echoscan import EchoScanProClient
121
+
122
+ pro = EchoScanProClient(api_key=os.environ["ECHOSCAN_PRO_KEY"])
123
+ report = pro.get_report("fp_session_123")
124
+ history = pro.get_history("fp_session_123", from_="2026-03-01", to="2026-03-18", recent=20)
125
+
126
+ print(report)
127
+ print(history)
128
+ ```
@@ -0,0 +1,119 @@
1
+ # `echoscan` (Python)
2
+
3
+ 中文 | [English](#english)
4
+
5
+ ## 中文
6
+
7
+ ### 可用调用方式
8
+
9
+ - `EchoScanLiteClient()`
10
+ - `EchoScanProClient(api_key=...)`
11
+
12
+ 方法:
13
+
14
+ - Lite: `get_report(imprint)`
15
+ - Pro: `get_report(imprint)`, `get_history(imprint, days=..., from_=..., to=..., recent=...)`
16
+
17
+ History 查询:
18
+
19
+ 仅 Pro 客户端可用(需要 Pro API Key)。
20
+
21
+ ### 返回值与错误
22
+
23
+ `get_report()` 顶层结构:
24
+
25
+ ```json
26
+ {
27
+ "analysis": {},
28
+ "lyingCount": 0,
29
+ "projection": {}
30
+ }
31
+ ```
32
+
33
+ `get_history()` 顶层结构:
34
+
35
+ ```json
36
+ {
37
+ "imprint": "fp_session_...",
38
+ "range": {},
39
+ "recent": [],
40
+ "summary": {},
41
+ "timeline": []
42
+ }
43
+ ```
44
+
45
+ 完整字段说明链接:[echoscan](https://echoscan.org)
46
+
47
+
48
+ ### 示例
49
+
50
+ ```python
51
+ import os
52
+ from echoscan import EchoScanProClient
53
+
54
+ pro = EchoScanProClient(api_key=os.environ["ECHOSCAN_PRO_KEY"])
55
+ report = pro.get_report("fp_session_123")
56
+ history = pro.get_history("fp_session_123", from_="2026-03-01", to="2026-03-18", recent=20)
57
+
58
+ print(report)
59
+ print(history)
60
+ ```
61
+
62
+ ---
63
+
64
+ ## English
65
+
66
+ ### Usage
67
+
68
+ - `EchoScanLiteClient()`
69
+ - `EchoScanProClient(api_key=...)`
70
+
71
+ Methods:
72
+
73
+ - Lite: `get_report(imprint)`
74
+ - Pro: `get_report(imprint)`, `get_history(imprint, days=..., from_=..., to=..., recent=...)`
75
+
76
+ History query:
77
+
78
+ Pro-only (requires a Pro API key).
79
+
80
+ ### Response and errors
81
+
82
+ `get_report()` top-level shape:
83
+
84
+ ```json
85
+ {
86
+ "analysis": {},
87
+ "lyingCount": 0,
88
+ "projection": {}
89
+ }
90
+ ```
91
+
92
+ `get_history()` top-level shape:
93
+
94
+ ```json
95
+ {
96
+ "imprint": "fp_session_...",
97
+ "range": {},
98
+ "recent": [],
99
+ "summary": {},
100
+ "timeline": []
101
+ }
102
+ ```
103
+
104
+ Full field reference: [echoscan](https://echoscan.org)
105
+
106
+
107
+ ### Example
108
+
109
+ ```python
110
+ import os
111
+ from echoscan import EchoScanProClient
112
+
113
+ pro = EchoScanProClient(api_key=os.environ["ECHOSCAN_PRO_KEY"])
114
+ report = pro.get_report("fp_session_123")
115
+ history = pro.get_history("fp_session_123", from_="2026-03-01", to="2026-03-18", recent=20)
116
+
117
+ print(report)
118
+ print(history)
119
+ ```
@@ -0,0 +1,329 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import random
6
+ import time
7
+ import urllib.error
8
+ import urllib.parse
9
+ import urllib.request
10
+ from dataclasses import dataclass
11
+ from typing import Any, Dict, Optional
12
+
13
+ DEFAULT_BASE_URL = "https://api.echoscan.org"
14
+ DEFAULT_TIMEOUT_MS = 5000
15
+ DEFAULT_RETRIES = 2
16
+ SDK_USER_AGENT = "echoscan-python/0.1.0"
17
+
18
+
19
+ @dataclass
20
+ class ApiError(Exception):
21
+ code: str
22
+ http_status: int
23
+ message: str
24
+ request_id: Optional[str]
25
+ retryable: bool
26
+
27
+ def __str__(self) -> str:
28
+ return f"{self.message} ({self.code})"
29
+
30
+
31
+ class _BaseClient:
32
+ def __init__(self, api_key: Optional[str]):
33
+ base_url = (os.getenv("ECHOSCAN_SERVER_BASE_URL") or DEFAULT_BASE_URL).strip()
34
+ self._base_url = base_url.rstrip("/")
35
+ self._timeout_ms = _parse_positive_int("ECHOSCAN_SERVER_TIMEOUT_MS", DEFAULT_TIMEOUT_MS)
36
+ self._retries = _parse_non_negative_int("ECHOSCAN_SERVER_RETRIES", DEFAULT_RETRIES)
37
+ self._api_key = api_key
38
+
39
+ def _get_json(self, path: str) -> Dict[str, Any]:
40
+ url = f"{self._base_url}{path}"
41
+ max_attempts = self._retries + 1
42
+ last_error: Optional[ApiError] = None
43
+
44
+ for attempt in range(1, max_attempts + 1):
45
+ request_id = _generate_request_id()
46
+ headers = {
47
+ "Accept": "application/json",
48
+ "User-Agent": SDK_USER_AGENT,
49
+ "X-Request-Id": request_id,
50
+ }
51
+ if self._api_key:
52
+ headers["X-API-Key"] = self._api_key
53
+
54
+ request = urllib.request.Request(url=url, method="GET", headers=headers)
55
+ try:
56
+ with urllib.request.urlopen(request, timeout=self._timeout_ms / 1000) as response:
57
+ body = response.read()
58
+ return _json_or_empty(body)
59
+ except urllib.error.HTTPError as e:
60
+ code = _map_status_to_code(e.code)
61
+ retryable = attempt < max_attempts and (e.code >= 500 or e.code == 429)
62
+ response_request_id = e.headers.get("X-Request-Id") if e.headers else None
63
+ mapped = ApiError(
64
+ code=code,
65
+ http_status=e.code,
66
+ message=_default_message(code),
67
+ request_id=response_request_id or request_id,
68
+ retryable=retryable,
69
+ )
70
+ if retryable:
71
+ last_error = mapped
72
+ continue
73
+ raise mapped
74
+ except urllib.error.URLError as e:
75
+ timeout = "timed out" in str(e).lower()
76
+ code = "timeout" if timeout else "network_error"
77
+ status = 408 if timeout else 0
78
+ retryable = attempt < max_attempts
79
+ mapped = ApiError(
80
+ code=code,
81
+ http_status=status,
82
+ message=_default_message(code),
83
+ request_id=request_id,
84
+ retryable=retryable,
85
+ )
86
+ if retryable:
87
+ last_error = mapped
88
+ continue
89
+ raise mapped
90
+
91
+ if last_error:
92
+ raise last_error
93
+ raise ApiError(
94
+ code="unknown_error",
95
+ http_status=0,
96
+ message="Unexpected error",
97
+ request_id=None,
98
+ retryable=False,
99
+ )
100
+
101
+
102
+ class EchoScanLiteClient(_BaseClient):
103
+ def __init__(self):
104
+ super().__init__(api_key=None)
105
+
106
+ def get_report(self, imprint: str) -> Dict[str, Any]:
107
+ normalized = _assert_imprint(imprint)
108
+ return self._get_json(f"/report-lite/{urllib.parse.quote(normalized, safe='')}")
109
+
110
+
111
+ class EchoScanProClient(_BaseClient):
112
+ def __init__(self, api_key: str):
113
+ normalized_key = _assert_api_key(api_key, "pro")
114
+ super().__init__(api_key=normalized_key)
115
+
116
+ def get_report(self, imprint: str) -> Dict[str, Any]:
117
+ normalized = _assert_imprint(imprint)
118
+ return self._get_json(f"/api/v1/fingerprint/report/{urllib.parse.quote(normalized, safe='')}")
119
+
120
+ def get_history(
121
+ self,
122
+ imprint: str,
123
+ *,
124
+ days: Optional[int] = None,
125
+ from_: Optional[str] = None,
126
+ to: Optional[str] = None,
127
+ recent: Optional[int] = None,
128
+ ) -> Dict[str, Any]:
129
+ normalized = _assert_imprint(imprint)
130
+ query = _build_history_query(days=days, from_=from_, to=to, recent=recent)
131
+ path = f"/api/v1/fingerprint/imprint/{urllib.parse.quote(normalized, safe='')}/history{query}"
132
+ return self._get_json(path)
133
+
134
+
135
+ class EchoScanDebugClient(_BaseClient):
136
+ def __init__(self, api_key: str):
137
+ normalized_key = _assert_api_key(api_key, "debug")
138
+ super().__init__(api_key=normalized_key)
139
+
140
+ def get_report(self, imprint: str) -> Dict[str, Any]:
141
+ normalized = _assert_imprint(imprint)
142
+ return self._get_json(
143
+ f"/api/v1/internal/fingerprint/report/{urllib.parse.quote(normalized, safe='')}"
144
+ )
145
+
146
+ def get_details(self, imprint: str) -> Dict[str, Any]:
147
+ normalized = _assert_imprint(imprint)
148
+ return self._get_json(
149
+ f"/api/v1/internal/fingerprint/details/{urllib.parse.quote(normalized, safe='')}"
150
+ )
151
+
152
+
153
+ def _assert_imprint(imprint: str) -> str:
154
+ normalized = (imprint or "").strip()
155
+ if not normalized:
156
+ raise ApiError(
157
+ code="invalid_request",
158
+ http_status=400,
159
+ message="imprint must be a non-empty string",
160
+ request_id=None,
161
+ retryable=False,
162
+ )
163
+ return normalized
164
+
165
+
166
+ def _assert_api_key(api_key: str, role: str) -> str:
167
+ normalized = (api_key or "").strip()
168
+ if not normalized:
169
+ raise ApiError(
170
+ code="invalid_request",
171
+ http_status=400,
172
+ message=f"apiKey is required for {role} client",
173
+ request_id=None,
174
+ retryable=False,
175
+ )
176
+ return normalized
177
+
178
+
179
+ def _build_history_query(
180
+ *, days: Optional[int], from_: Optional[str], to: Optional[str], recent: Optional[int]
181
+ ) -> str:
182
+ has_days = days is not None
183
+ has_from = from_ is not None
184
+ has_to = to is not None
185
+
186
+ if has_days and (has_from or has_to):
187
+ raise ApiError(
188
+ code="invalid_request",
189
+ http_status=400,
190
+ message="history query: days and from/to are mutually exclusive",
191
+ request_id=None,
192
+ retryable=False,
193
+ )
194
+ if has_from != has_to:
195
+ raise ApiError(
196
+ code="invalid_request",
197
+ http_status=400,
198
+ message="history query: from and to must be provided together",
199
+ request_id=None,
200
+ retryable=False,
201
+ )
202
+
203
+ params = {}
204
+ if has_days:
205
+ if not isinstance(days, int) or days <= 0:
206
+ raise ApiError(
207
+ code="invalid_request",
208
+ http_status=400,
209
+ message="history query: days must be a positive number",
210
+ request_id=None,
211
+ retryable=False,
212
+ )
213
+ params["days"] = str(days)
214
+ if has_from and has_to:
215
+ if not _is_yyyy_mm_dd(str(from_)):
216
+ raise ApiError(
217
+ code="invalid_request",
218
+ http_status=400,
219
+ message="from must use YYYY-MM-DD format",
220
+ request_id=None,
221
+ retryable=False,
222
+ )
223
+ if not _is_yyyy_mm_dd(str(to)):
224
+ raise ApiError(
225
+ code="invalid_request",
226
+ http_status=400,
227
+ message="to must use YYYY-MM-DD format",
228
+ request_id=None,
229
+ retryable=False,
230
+ )
231
+ if str(from_) > str(to):
232
+ raise ApiError(
233
+ code="invalid_request",
234
+ http_status=400,
235
+ message="history query: from must be <= to",
236
+ request_id=None,
237
+ retryable=False,
238
+ )
239
+ params["from"] = str(from_)
240
+ params["to"] = str(to)
241
+ if recent is not None:
242
+ if not isinstance(recent, int) or recent <= 0:
243
+ raise ApiError(
244
+ code="invalid_request",
245
+ http_status=400,
246
+ message="history query: recent must be a positive number",
247
+ request_id=None,
248
+ retryable=False,
249
+ )
250
+ params["recent"] = str(recent)
251
+
252
+ if not params:
253
+ return ""
254
+ return "?" + urllib.parse.urlencode(params)
255
+
256
+
257
+ def _is_yyyy_mm_dd(value: str) -> bool:
258
+ if len(value) != 10 or value[4] != "-" or value[7] != "-":
259
+ return False
260
+ y, m, d = value[:4], value[5:7], value[8:10]
261
+ return y.isdigit() and m.isdigit() and d.isdigit()
262
+
263
+
264
+ def _parse_positive_int(name: str, fallback: int) -> int:
265
+ raw = (os.getenv(name) or "").strip()
266
+ if not raw:
267
+ return fallback
268
+ try:
269
+ value = int(raw)
270
+ return value if value > 0 else fallback
271
+ except ValueError:
272
+ return fallback
273
+
274
+
275
+ def _parse_non_negative_int(name: str, fallback: int) -> int:
276
+ raw = (os.getenv(name) or "").strip()
277
+ if not raw:
278
+ return fallback
279
+ try:
280
+ value = int(raw)
281
+ return value if value >= 0 else fallback
282
+ except ValueError:
283
+ return fallback
284
+
285
+
286
+ def _map_status_to_code(status: int) -> str:
287
+ if status == 400:
288
+ return "invalid_request"
289
+ if status == 401:
290
+ return "auth_failed"
291
+ if status == 403:
292
+ return "forbidden"
293
+ if status == 404:
294
+ return "not_found"
295
+ if status == 408:
296
+ return "timeout"
297
+ if status == 429:
298
+ return "quota_exceeded"
299
+ if status in (500, 502, 503, 504):
300
+ return "upstream_unavailable"
301
+ return "unknown_error"
302
+
303
+
304
+ def _default_message(code: str) -> str:
305
+ return {
306
+ "invalid_request": "Invalid request",
307
+ "auth_failed": "Authentication failed",
308
+ "forbidden": "Forbidden",
309
+ "not_found": "Not found",
310
+ "quota_exceeded": "Quota exceeded",
311
+ "timeout": "Request timed out",
312
+ "upstream_unavailable": "Upstream service unavailable",
313
+ "network_error": "Network error",
314
+ "unknown_error": "Unexpected error",
315
+ }.get(code, "Unexpected error")
316
+
317
+
318
+ def _generate_request_id() -> str:
319
+ return f"req_{time.time_ns()}_{random.randint(100000, 999999)}"
320
+
321
+
322
+ def _json_or_empty(raw: bytes) -> Dict[str, Any]:
323
+ if not raw:
324
+ return {}
325
+ try:
326
+ decoded = json.loads(raw.decode("utf-8"))
327
+ except Exception:
328
+ return {}
329
+ return decoded if isinstance(decoded, dict) else {}
@@ -0,0 +1,128 @@
1
+ Metadata-Version: 2.4
2
+ Name: echoscan
3
+ Version: 0.1.0
4
+ Summary: EchoScan package for Python
5
+ Author: EchoScan Team
6
+ License: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+
10
+ # `echoscan` (Python)
11
+
12
+ 中文 | [English](#english)
13
+
14
+ ## 中文
15
+
16
+ ### 可用调用方式
17
+
18
+ - `EchoScanLiteClient()`
19
+ - `EchoScanProClient(api_key=...)`
20
+
21
+ 方法:
22
+
23
+ - Lite: `get_report(imprint)`
24
+ - Pro: `get_report(imprint)`, `get_history(imprint, days=..., from_=..., to=..., recent=...)`
25
+
26
+ History 查询:
27
+
28
+ 仅 Pro 客户端可用(需要 Pro API Key)。
29
+
30
+ ### 返回值与错误
31
+
32
+ `get_report()` 顶层结构:
33
+
34
+ ```json
35
+ {
36
+ "analysis": {},
37
+ "lyingCount": 0,
38
+ "projection": {}
39
+ }
40
+ ```
41
+
42
+ `get_history()` 顶层结构:
43
+
44
+ ```json
45
+ {
46
+ "imprint": "fp_session_...",
47
+ "range": {},
48
+ "recent": [],
49
+ "summary": {},
50
+ "timeline": []
51
+ }
52
+ ```
53
+
54
+ 完整字段说明链接:[echoscan](https://echoscan.org)
55
+
56
+
57
+ ### 示例
58
+
59
+ ```python
60
+ import os
61
+ from echoscan import EchoScanProClient
62
+
63
+ pro = EchoScanProClient(api_key=os.environ["ECHOSCAN_PRO_KEY"])
64
+ report = pro.get_report("fp_session_123")
65
+ history = pro.get_history("fp_session_123", from_="2026-03-01", to="2026-03-18", recent=20)
66
+
67
+ print(report)
68
+ print(history)
69
+ ```
70
+
71
+ ---
72
+
73
+ ## English
74
+
75
+ ### Usage
76
+
77
+ - `EchoScanLiteClient()`
78
+ - `EchoScanProClient(api_key=...)`
79
+
80
+ Methods:
81
+
82
+ - Lite: `get_report(imprint)`
83
+ - Pro: `get_report(imprint)`, `get_history(imprint, days=..., from_=..., to=..., recent=...)`
84
+
85
+ History query:
86
+
87
+ Pro-only (requires a Pro API key).
88
+
89
+ ### Response and errors
90
+
91
+ `get_report()` top-level shape:
92
+
93
+ ```json
94
+ {
95
+ "analysis": {},
96
+ "lyingCount": 0,
97
+ "projection": {}
98
+ }
99
+ ```
100
+
101
+ `get_history()` top-level shape:
102
+
103
+ ```json
104
+ {
105
+ "imprint": "fp_session_...",
106
+ "range": {},
107
+ "recent": [],
108
+ "summary": {},
109
+ "timeline": []
110
+ }
111
+ ```
112
+
113
+ Full field reference: [echoscan](https://echoscan.org)
114
+
115
+
116
+ ### Example
117
+
118
+ ```python
119
+ import os
120
+ from echoscan import EchoScanProClient
121
+
122
+ pro = EchoScanProClient(api_key=os.environ["ECHOSCAN_PRO_KEY"])
123
+ report = pro.get_report("fp_session_123")
124
+ history = pro.get_history("fp_session_123", from_="2026-03-01", to="2026-03-18", recent=20)
125
+
126
+ print(report)
127
+ print(history)
128
+ ```
@@ -0,0 +1,7 @@
1
+ README.md
2
+ pyproject.toml
3
+ echoscan/__init__.py
4
+ echoscan.egg-info/PKG-INFO
5
+ echoscan.egg-info/SOURCES.txt
6
+ echoscan.egg-info/dependency_links.txt
7
+ echoscan.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ echoscan
@@ -0,0 +1,18 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "echoscan"
7
+ version = "0.1.0"
8
+ description = "EchoScan package for Python"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ authors = [
12
+ { name = "EchoScan Team" }
13
+ ]
14
+ license = { text = "MIT" }
15
+
16
+ [tool.setuptools.packages.find]
17
+ where = ["."]
18
+ include = ["echoscan*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+