kntp-lib 0.1.1__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.
- kntp/__init__.py +12 -0
- kntp/core.py +262 -0
- kntp_lib-0.1.1.dist-info/METADATA +34 -0
- kntp_lib-0.1.1.dist-info/RECORD +7 -0
- kntp_lib-0.1.1.dist-info/WHEEL +5 -0
- kntp_lib-0.1.1.dist-info/licenses/LICENSE +1 -0
- kntp_lib-0.1.1.dist-info/top_level.txt +1 -0
kntp/__init__.py
ADDED
kntp/core.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# kntp_lib.py
|
|
2
|
+
# 한국 NTP 비교/추천용 라이브러리 코어
|
|
3
|
+
# Python 3.10+ (3.14 OK)
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import socket
|
|
8
|
+
import struct
|
|
9
|
+
import time
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from statistics import mean, pstdev
|
|
12
|
+
from typing import Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
NTP_PORT = 123
|
|
15
|
+
NTP_DELTA = 2208988800 # seconds between 1900-01-01 and 1970-01-01
|
|
16
|
+
|
|
17
|
+
DEFAULT_BASE = "ntp.kriss.re.kr"
|
|
18
|
+
|
|
19
|
+
DEFAULT_SERVERS: List[str] = [
|
|
20
|
+
# Korea / KR-centric
|
|
21
|
+
"ntp.kriss.re.kr", # KRISS (기준)
|
|
22
|
+
"kr.pool.ntp.org", # Korea NTP Pool
|
|
23
|
+
"asia.pool.ntp.org", # Asia NTP Pool
|
|
24
|
+
"pool.ntp.org", # Global NTP Pool
|
|
25
|
+
"time.bora.net", # 국내에서 흔히 사용
|
|
26
|
+
"time.nuri.net", # 국내에서 흔히 사용
|
|
27
|
+
"clock.iptime.co.kr", # 환경에 따라 응답/차단 가능
|
|
28
|
+
|
|
29
|
+
# Global public (fallback/비교용)
|
|
30
|
+
"time.google.com",
|
|
31
|
+
"time.cloudflare.com",
|
|
32
|
+
"time.windows.com",
|
|
33
|
+
"time.apple.com",
|
|
34
|
+
"time.facebook.com",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class Sample:
|
|
40
|
+
"""단일 측정 결과"""
|
|
41
|
+
offset_ms: float # clock offset (server vs local) in ms
|
|
42
|
+
delay_ms: float # network delay in ms
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class Stats:
|
|
47
|
+
"""서버별 통계"""
|
|
48
|
+
server: str
|
|
49
|
+
ok: int
|
|
50
|
+
fail: int
|
|
51
|
+
avg_offset_ms: float
|
|
52
|
+
std_offset_ms: float
|
|
53
|
+
avg_delay_ms: float
|
|
54
|
+
std_delay_ms: float
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class Ranked:
|
|
59
|
+
"""랭킹/추천용 결과(기준 서버 대비)"""
|
|
60
|
+
server: str
|
|
61
|
+
ok: int
|
|
62
|
+
fail: int
|
|
63
|
+
avg_offset_ms: float
|
|
64
|
+
std_offset_ms: float
|
|
65
|
+
avg_delay_ms: float
|
|
66
|
+
std_delay_ms: float
|
|
67
|
+
vs_base_ms: float
|
|
68
|
+
score: float
|
|
69
|
+
grade: str
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def grade(score: float) -> str:
|
|
73
|
+
"""점수 기반 등급(A가 가장 좋음). 필요하면 사용자 환경에 맞게 조정."""
|
|
74
|
+
if score <= 5:
|
|
75
|
+
return "A"
|
|
76
|
+
if score <= 10:
|
|
77
|
+
return "B"
|
|
78
|
+
if score <= 20:
|
|
79
|
+
return "C"
|
|
80
|
+
return "D"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _system_to_ntp(ts_unix: float) -> float:
|
|
84
|
+
return ts_unix + NTP_DELTA
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _ntp_to_system(ts_ntp: float) -> float:
|
|
88
|
+
return ts_ntp - NTP_DELTA
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def query_ntp(host: str, timeout: float = 2.0) -> Sample:
|
|
92
|
+
"""
|
|
93
|
+
NTP 표준 4타임스탬프 방식(offset/delay) 계산.
|
|
94
|
+
|
|
95
|
+
T1: client send time (local)
|
|
96
|
+
T2: server receive time (from packet)
|
|
97
|
+
T3: server transmit time (from packet)
|
|
98
|
+
T4: client receive time (local)
|
|
99
|
+
|
|
100
|
+
delay = (T4 - T1) - (T3 - T2)
|
|
101
|
+
offset = ((T2 - T1) + (T3 - T4)) / 2
|
|
102
|
+
"""
|
|
103
|
+
addr = (host, NTP_PORT)
|
|
104
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
105
|
+
sock.settimeout(timeout)
|
|
106
|
+
|
|
107
|
+
packet = bytearray(48)
|
|
108
|
+
packet[0] = 0x23 # LI=0, VN=4, Mode=3(client)
|
|
109
|
+
|
|
110
|
+
# client transmit timestamp 채움(서버가 originate 검증에 활용 가능)
|
|
111
|
+
t1 = time.time()
|
|
112
|
+
t1_ntp = _system_to_ntp(t1)
|
|
113
|
+
sec = int(t1_ntp)
|
|
114
|
+
frac = int((t1_ntp - sec) * (2**32))
|
|
115
|
+
struct.pack_into("!II", packet, 40, sec, frac)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
sock.sendto(packet, addr)
|
|
119
|
+
data, _ = sock.recvfrom(512)
|
|
120
|
+
t4 = time.time()
|
|
121
|
+
finally:
|
|
122
|
+
sock.close()
|
|
123
|
+
|
|
124
|
+
if len(data) < 48:
|
|
125
|
+
raise ValueError("NTP response too short")
|
|
126
|
+
|
|
127
|
+
u = struct.unpack("!12I", data[:48])
|
|
128
|
+
|
|
129
|
+
# T2 (recv): words 8,9 / T3 (tx): words 10,11
|
|
130
|
+
t2_ntp = u[8] + (u[9] / 2**32)
|
|
131
|
+
t3_ntp = u[10] + (u[11] / 2**32)
|
|
132
|
+
|
|
133
|
+
t2 = _ntp_to_system(t2_ntp)
|
|
134
|
+
t3 = _ntp_to_system(t3_ntp)
|
|
135
|
+
|
|
136
|
+
delay = (t4 - t1) - (t3 - t2)
|
|
137
|
+
offset = ((t2 - t1) + (t3 - t4)) / 2
|
|
138
|
+
|
|
139
|
+
return Sample(offset_ms=offset * 1000.0, delay_ms=delay * 1000.0)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def collect_stats(
|
|
143
|
+
servers: List[str],
|
|
144
|
+
samples: int = 5,
|
|
145
|
+
timeout: float = 2.0,
|
|
146
|
+
sleep_between: float = 0.5,
|
|
147
|
+
) -> List[Stats]:
|
|
148
|
+
"""
|
|
149
|
+
servers 각 서버를 samples번 측정해서 통계를 반환.
|
|
150
|
+
"""
|
|
151
|
+
if samples < 1:
|
|
152
|
+
raise ValueError("samples must be >= 1")
|
|
153
|
+
|
|
154
|
+
raw: Dict[str, List[Sample]] = {s: [] for s in servers}
|
|
155
|
+
fails: Dict[str, int] = {s: 0 for s in servers}
|
|
156
|
+
|
|
157
|
+
for i in range(samples):
|
|
158
|
+
for s in servers:
|
|
159
|
+
try:
|
|
160
|
+
raw[s].append(query_ntp(s, timeout=timeout))
|
|
161
|
+
except Exception:
|
|
162
|
+
fails[s] += 1
|
|
163
|
+
if i != samples - 1 and sleep_between > 0:
|
|
164
|
+
time.sleep(sleep_between)
|
|
165
|
+
|
|
166
|
+
out: List[Stats] = []
|
|
167
|
+
for s in servers:
|
|
168
|
+
ok = len(raw[s])
|
|
169
|
+
fail = fails[s]
|
|
170
|
+
if ok == 0:
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
offs = [x.offset_ms for x in raw[s]]
|
|
174
|
+
dels = [x.delay_ms for x in raw[s]]
|
|
175
|
+
|
|
176
|
+
out.append(
|
|
177
|
+
Stats(
|
|
178
|
+
server=s,
|
|
179
|
+
ok=ok,
|
|
180
|
+
fail=fail,
|
|
181
|
+
avg_offset_ms=mean(offs),
|
|
182
|
+
std_offset_ms=pstdev(offs) if ok > 1 else 0.0,
|
|
183
|
+
avg_delay_ms=mean(dels),
|
|
184
|
+
std_delay_ms=pstdev(dels) if ok > 1 else 0.0,
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return out
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def rank_servers(
|
|
192
|
+
stats: List[Stats],
|
|
193
|
+
base: str = DEFAULT_BASE,
|
|
194
|
+
*,
|
|
195
|
+
w_delay: float = 0.20,
|
|
196
|
+
w_jitter: float = 0.50,
|
|
197
|
+
max_delay_ms: Optional[float] = 100.0,
|
|
198
|
+
allow_base: bool = True,
|
|
199
|
+
) -> List[Ranked]:
|
|
200
|
+
"""
|
|
201
|
+
기준(base) 대비 점수화해 정렬한 리스트 반환.
|
|
202
|
+
|
|
203
|
+
score(작을수록 좋음) =
|
|
204
|
+
|vs_base_offset| + w_delay*avg_delay + w_jitter*std_offset
|
|
205
|
+
|
|
206
|
+
max_delay_ms:
|
|
207
|
+
- None이면 지연 필터링 없음
|
|
208
|
+
- 숫자면 avg_delay_ms가 그 이상인 서버는 제외(추천 대상에서 제외)
|
|
209
|
+
"""
|
|
210
|
+
base_stat = next((x for x in stats if x.server == base), None)
|
|
211
|
+
if base_stat is None:
|
|
212
|
+
raise RuntimeError(f"Base server '{base}' stats not found (측정 실패/목록 누락).")
|
|
213
|
+
|
|
214
|
+
ranked: List[Ranked] = []
|
|
215
|
+
for st in stats:
|
|
216
|
+
if not allow_base and st.server == base:
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
vs_base = st.avg_offset_ms - base_stat.avg_offset_ms
|
|
220
|
+
score = abs(vs_base) + (w_delay * st.avg_delay_ms) + (w_jitter * st.std_offset_ms)
|
|
221
|
+
|
|
222
|
+
if max_delay_ms is not None and st.avg_delay_ms >= max_delay_ms:
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
ranked.append(
|
|
226
|
+
Ranked(
|
|
227
|
+
server=st.server,
|
|
228
|
+
ok=st.ok,
|
|
229
|
+
fail=st.fail,
|
|
230
|
+
avg_offset_ms=st.avg_offset_ms,
|
|
231
|
+
std_offset_ms=st.std_offset_ms,
|
|
232
|
+
avg_delay_ms=st.avg_delay_ms,
|
|
233
|
+
std_delay_ms=st.std_delay_ms,
|
|
234
|
+
vs_base_ms=vs_base,
|
|
235
|
+
score=score,
|
|
236
|
+
grade=grade(score),
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
ranked.sort(key=lambda x: x.score)
|
|
241
|
+
return ranked
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def recommend(
|
|
245
|
+
ranked: List[Ranked],
|
|
246
|
+
*,
|
|
247
|
+
base: str = DEFAULT_BASE,
|
|
248
|
+
require_ok_rate: float = 0.8,
|
|
249
|
+
) -> Optional[Ranked]:
|
|
250
|
+
"""
|
|
251
|
+
랭킹 결과에서 '추천 1개'를 골라 반환.
|
|
252
|
+
- base 제외
|
|
253
|
+
- 성공률(ok/(ok+fail))이 require_ok_rate 미만이면 제외
|
|
254
|
+
"""
|
|
255
|
+
for r in ranked:
|
|
256
|
+
if r.server == base:
|
|
257
|
+
continue
|
|
258
|
+
total = r.ok + r.fail
|
|
259
|
+
ok_rate = (r.ok / total) if total > 0 else 0.0
|
|
260
|
+
if ok_rate >= require_ok_rate:
|
|
261
|
+
return r
|
|
262
|
+
return None
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kntp-lib
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: KRISS-based NTP server analysis and recommendation library
|
|
5
|
+
Author: 도형
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
# kntp-lib
|
|
13
|
+
|
|
14
|
+
KRISS (Korea Research Institute of Standards and Science) 기준으로
|
|
15
|
+
NTP 서버의 정확도, 지연, 안정성을 분석하고 추천하는 Python 라이브러리입니다.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- NTP 4-timestamp 공식 기반 offset / delay 계산
|
|
20
|
+
- 서버별 통계 분석 (평균, 표준편차)
|
|
21
|
+
- KRISS 기준 상대 오프셋 비교
|
|
22
|
+
- 점수 기반 랭킹 및 등급화 (A–D)
|
|
23
|
+
- 자동 추천 서버 선택
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import kntp_lib as kntp
|
|
29
|
+
|
|
30
|
+
stats = kntp.collect_stats(kntp.DEFAULT_SERVERS, samples=5)
|
|
31
|
+
ranked = kntp.rank_servers(stats, base=kntp.DEFAULT_BASE)
|
|
32
|
+
|
|
33
|
+
best = kntp.recommend(ranked)
|
|
34
|
+
print(best)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
kntp/__init__.py,sha256=6KiQzoi1twsKbhX6FwYjiyV36_f-hi4ay1NPdgfUD3Y,186
|
|
2
|
+
kntp/core.py,sha256=UzvCfndu7YcEoInp_jEodcpZ5-e6rdpSfW1eYlCoQZI,7180
|
|
3
|
+
kntp_lib-0.1.1.dist-info/licenses/LICENSE,sha256=yz-R1U7uMOU-NbK5mQX3Dxae1Un9eJCdPawt78ntjTs,3
|
|
4
|
+
kntp_lib-0.1.1.dist-info/METADATA,sha256=Li_9Vekp0wDS8v2HvCcJMa_morsXLjzMW76JvK-sg3U,930
|
|
5
|
+
kntp_lib-0.1.1.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
6
|
+
kntp_lib-0.1.1.dist-info/top_level.txt,sha256=rRna1dpbOHiJnl7RBhVSrnrd4-85kHr_MhccRXU5FfI,5
|
|
7
|
+
kntp_lib-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
---
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
kntp
|