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.
- echoscan-0.1.0/PKG-INFO +128 -0
- echoscan-0.1.0/README.md +119 -0
- echoscan-0.1.0/echoscan/__init__.py +329 -0
- echoscan-0.1.0/echoscan.egg-info/PKG-INFO +128 -0
- echoscan-0.1.0/echoscan.egg-info/SOURCES.txt +7 -0
- echoscan-0.1.0/echoscan.egg-info/dependency_links.txt +1 -0
- echoscan-0.1.0/echoscan.egg-info/top_level.txt +1 -0
- echoscan-0.1.0/pyproject.toml +18 -0
- echoscan-0.1.0/setup.cfg +4 -0
echoscan-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
```
|
echoscan-0.1.0/README.md
ADDED
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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*"]
|
echoscan-0.1.0/setup.cfg
ADDED