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 ADDED
@@ -0,0 +1,12 @@
1
+ from .core import (
2
+ DEFAULT_BASE,
3
+ DEFAULT_SERVERS,
4
+ Sample,
5
+ Stats,
6
+ Ranked,
7
+ query_ntp,
8
+ collect_stats,
9
+ rank_servers,
10
+ recommend,
11
+ grade,
12
+ )
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ ---
@@ -0,0 +1 @@
1
+ kntp