ksxt 0.0.10__py3-none-any.whl → 1.0.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.
ksxt/base/exchange.py CHANGED
@@ -20,6 +20,11 @@ class Exchange:
20
20
  is_dev = False
21
21
 
22
22
  session = None
23
+ # 세션 유효 시간 (초)
24
+ session_lifetime = 10
25
+ # 세션의 마지막 사용 시간
26
+ session_last_used: time = None
27
+
23
28
  timeout = 10000 # milliseconds = seconds * 1000
24
29
  synchronous = True
25
30
 
@@ -0,0 +1,88 @@
1
+ from abc import ABC, abstractmethod
2
+ import asyncio
3
+ import time
4
+
5
+
6
+ class RateLimiterStrategy(ABC):
7
+ @abstractmethod
8
+ async def async_acquire(self):
9
+ pass
10
+
11
+ @abstractmethod
12
+ def acquire(self):
13
+ pass
14
+
15
+ @abstractmethod
16
+ def release(self):
17
+ pass
18
+
19
+
20
+ class RateLimiterContext:
21
+ def __init__(self, strategy: RateLimiterStrategy):
22
+ self._strategy = strategy
23
+
24
+ async def async_acquire(self):
25
+ await self._strategy.async_acquire()
26
+
27
+ def acquire(self):
28
+ self._strategy.acquire()
29
+
30
+ def release(self):
31
+ self._strategy.release()
32
+
33
+
34
+ class RequestRateLimiter(RateLimiterStrategy):
35
+ def __init__(self, max_requests: int, period: float = 1.0):
36
+ self.max_requests = max_requests
37
+ self.period = period
38
+ self.semaphore = asyncio.BoundedSemaphore(max_requests)
39
+ self.last_reset_time = time.time()
40
+
41
+ async def async_acquire(self):
42
+ current_time = time.time()
43
+ if current_time - self.last_reset_time > self.period:
44
+ self.semaphore = asyncio.BoundedSemaphore(self.max_requests)
45
+ self.last_reset_time = current_time
46
+
47
+ await self.semaphore.acquire()
48
+
49
+ def acquire(self):
50
+ current_time = time.time()
51
+ if current_time - self.last_reset_time > self.period:
52
+ # 동기식 세마포어는 지원되지 않으므로, 대신에 현재 상황을 조정합니다.
53
+ self.last_reset_time = current_time
54
+
55
+ # 세마포어가 동기식으로 동작하지 않으므로, 대신 제한 시간을 체크하여 동기식으로 제한
56
+ while self.semaphore._value <= 0: # 내부 값이 0 이하라면, 대기
57
+ if time.time() - self.last_reset_time > self.period:
58
+ self.semaphore = asyncio.BoundedSemaphore(self.max_requests)
59
+ break
60
+ time.sleep(0.01) # 동기식 대기
61
+
62
+ self.semaphore._value -= 1 # 수동으로 세마포어 값을 감소시킴
63
+
64
+ def release(self):
65
+ self.semaphore.release()
66
+
67
+
68
+ class TimeBasedRateLimiter(RateLimiterStrategy):
69
+ def __init__(self, period: float):
70
+ self.period = period
71
+ self.last_request_time = None
72
+
73
+ async def async_acquire(self):
74
+ current_time = time.time()
75
+ if self.last_request_time and (current_time - self.last_request_time) < self.period:
76
+ raise ValueError("요청이 너무 자주 발생했습니다. 잠시 후에 다시 시도해주세요.")
77
+
78
+ self.last_request_time = current_time
79
+
80
+ def acquire(self):
81
+ current_time = time.time()
82
+ if self.last_request_time and (current_time - self.last_request_time) < self.period:
83
+ raise ValueError("요청이 너무 자주 발생했습니다. 잠시 후에 다시 시도해주세요.")
84
+
85
+ self.last_request_time = current_time
86
+
87
+ def release(self):
88
+ pass # 이 클래스에서는 release가 필요하지 않음
@@ -1,5 +1,6 @@
1
1
  import json
2
2
  import logging
3
+ import tomllib
3
4
  import toml
4
5
  import os
5
6
  from pathlib import Path
@@ -10,6 +11,11 @@ from datetime import datetime, UTC
10
11
  import pytz
11
12
  import time
12
13
 
14
+ from ksxt.base.rate_limiter import (
15
+ RateLimiterContext,
16
+ RequestRateLimiter,
17
+ TimeBasedRateLimiter,
18
+ )
13
19
  from ksxt.base.errors import NotSupportedError
14
20
  from ksxt.base.exchange import Exchange
15
21
  from ksxt.config import CONFIG_DIR
@@ -38,6 +44,9 @@ class RestExchange(Exchange):
38
44
  def __init__(self, config: Dict = None, filename: str = None) -> None:
39
45
  super().__init__()
40
46
 
47
+
48
+ self.rate_limiters: Dict[str, RateLimiterContext] = {}
49
+
41
50
  self.headers = dict() if self.headers is None else self.headers
42
51
 
43
52
  if config is None:
@@ -52,33 +61,50 @@ class RestExchange(Exchange):
52
61
  apis = self._get_api_from_file(filename)
53
62
  Exchange.set_attr(self, apis)
54
63
 
55
- def __del__(self):
56
- if self.session:
57
- try:
58
- self.session.close()
59
- except Exception as e:
60
- pass
61
-
62
- def _get_api_from_file(self, filename: str):
64
+ def _get_api_from_file(self, filename: str) -> Dict[str, Any]:
63
65
  if filename is None:
64
- tr_config_filename = "tr_dev.json" if self.is_dev else "tr_app.json"
65
- else:
66
- tr_config_filename = filename
67
-
68
- config_path = os.path.join(CONFIG_DIR, tr_config_filename)
69
-
70
- if Path(tr_config_filename).suffix == ".json":
71
- with open(
72
- config_path,
73
- encoding="utf-8",
74
- ) as f:
75
- c = json.load(f)
76
- return {"apis": c[self.name]}
77
-
78
- elif Path(tr_config_filename).suffix == ".toml":
79
- with open(config_path, mode="r") as f:
80
- c = toml.load(f)
81
- return c
66
+ raise ValueError("Configuration filename cannot be None.")
67
+
68
+ config_path = os.path.join(CONFIG_DIR, filename)
69
+
70
+ if Path(filename).suffix != ".toml":
71
+ raise ValueError(f"Unsupported file format: {Path(filename).suffix}")
72
+
73
+ with open(config_path, mode="rb") as f:
74
+ config = tomllib.load(f)
75
+
76
+ self._setup_rate_limiters(config)
77
+ return config
78
+
79
+ def _setup_rate_limiters(self, config: Dict[str, Any]):
80
+ """RateLimiter 설정을 구성하는 메서드"""
81
+ default_rate_limit = config.get("rate_limit", 1000) # 최상위 rate_limit 값 가져오기
82
+
83
+ apis = config.get("apis", {}).get("rest", {})
84
+
85
+ def traverse_apis(api_section, section_name="", self=self):
86
+ for key, value in api_section.items():
87
+ if not isinstance(value, dict):
88
+ continue
89
+
90
+ if "url" in value: # "url"이 있는 경우, API 엔드포인트로 간주
91
+ rate_limit = value.get("rate_limit", default_rate_limit)
92
+ rate_limit_period = value.get("rate_limit_period", 1.0)
93
+
94
+ # 전략 패턴을 이용한 RateLimiter 생성
95
+ if rate_limit_period > 1.0:
96
+ strategy = TimeBasedRateLimiter(period=rate_limit_period)
97
+ else:
98
+ strategy = RequestRateLimiter(max_requests=rate_limit, period=1.0)
99
+
100
+ # API 이름을 구성
101
+ api_name = f"{section_name}.{key}" if section_name else key
102
+ self.rate_limiters[api_name] = RateLimiterContext(strategy)
103
+ else:
104
+ # value가 딕셔너리지만 "url"이 없는 경우, 하위 섹션으로 간주하고 재귀 호출
105
+ traverse_apis(value, f"{section_name}.{key}" if section_name else key)
106
+
107
+ traverse_apis(apis)
82
108
 
83
109
  def check_token(func):
84
110
  def wrapper(self, *args, **kwargs):
ksxt/config/__init__.py CHANGED
@@ -1,3 +1,5 @@
1
1
  import os
2
2
 
3
3
  CONFIG_DIR = os.path.dirname(os.path.abspath(__file__))
4
+
5
+ VALID_METHODS = {"get", "post", "put", "delete", "patch", "head", "options"}
ksxt/config/bithumb.toml CHANGED
@@ -6,7 +6,8 @@
6
6
  id = 'bithumb'
7
7
  name = 'Bithumb'
8
8
  countries = ['KR']
9
- rate_limit = 1000
9
+ rate_limit = 30 # 초당 30회로 제한
10
+ rate_limit_period = 600 # 600초(10분)당 1회로 제한
10
11
  enableRateLimit = false
11
12
  www = 'https://www.bithumb.com'
12
13
  doc = 'https://apidocs.bithumb.com/'
ksxt/config/upbit.toml CHANGED
@@ -6,7 +6,8 @@
6
6
  id = 'upbit'
7
7
  name = 'Upbit'
8
8
  countries = ['KR']
9
- rate_limit = 1000
9
+ rate_limit = 30 # 초당 30회로 제한
10
+ rate_limit_period = 600 # 600초(10분)당 1회로 제한
10
11
  enableRateLimit = false
11
12
  www = 'https://www.upbit.com'
12
13
  doc = 'https://docs.upbit.com'
@@ -112,6 +113,7 @@ version = "v1"
112
113
  url = "orders"
113
114
  method = 'POST'
114
115
  api = 'private'
116
+ rate_limit = 8
115
117
  activate = true
116
118
  __comment__ = "주문하기"
117
119
 
@@ -119,6 +121,7 @@ version = "v1"
119
121
  url = "orders"
120
122
  method = 'POST'
121
123
  api = 'private'
124
+ rate_limit = 8
122
125
  activate = true
123
126
  __comment__ = "주문하기"
124
127
 
@@ -126,6 +129,7 @@ version = "v1"
126
129
  url = "orders"
127
130
  method = 'POST'
128
131
  api = 'private'
132
+ rate_limit = 8
129
133
  activate = true
130
134
  __comment__ = "주문하기"
131
135
 
@@ -133,6 +137,7 @@ version = "v1"
133
137
  url = "orders"
134
138
  method = 'POST'
135
139
  api = 'private'
140
+ rate_limit = 8
136
141
  activate = true
137
142
  __comment__ = "주문하기"
138
143
 
ksxt/koreainvest.py CHANGED
@@ -30,7 +30,6 @@ class KoreaInvest(RestExchange, ImplicitAPI):
30
30
 
31
31
  return super().is_activate(path=path, security_type=security_type)
32
32
 
33
- # @RestExchange.check_token
34
33
  def sign(
35
34
  self,
36
35
  path,
@@ -124,23 +123,6 @@ class KoreaInvest(RestExchange, ImplicitAPI):
124
123
  info=response,
125
124
  )
126
125
 
127
- # @RestExchange.check_token
128
- # def fetch_markets(self, market_name: str) -> ksxt.models.KsxtMarketResponse:
129
- # params = {}
130
-
131
- # common_header = self.create_common_header(request_params=params)
132
-
133
- # # TODO from Database
134
- # response = None
135
-
136
- # common_response = self.get_common_response(response=response)
137
- # if common_response.success != "0":
138
- # return ksxt.models.KsxtMarketResponse(header=common_header, response=common_response, info=None)
139
-
140
- # parsed_info = self.parser.parse_markets(response=response, base_market="KRW")
141
-
142
- # return ksxt.models.KsxtMarketResponse(header=common_header, response=common_response, info=parsed_info)
143
-
144
126
  @RestExchange.check_token
145
127
  def fetch_balance(self, acc_num: str, base_market: str = "KRW") -> ksxt.models.KsxtBalanceResponse:
146
128
  if base_market == "KRW":
@@ -282,11 +264,11 @@ class KoreaInvest(RestExchange, ImplicitAPI):
282
264
 
283
265
  if base_market == "KRW":
284
266
  params = {
285
- "FID_COND_MRKT_DIV_CODE": "U",
286
- "FID_INPUT_ISCD": symbol,
287
- "FID_INPUT_DATE_1": start.strftime('%Y%m%d'),
288
- "FID_INPUT_DATE_2": end.strftime('%Y%m%d'),
289
- "FID_PERIOD_DIV_CODE": param_code
267
+ "FID_COND_MRKT_DIV_CODE": "U",
268
+ "FID_INPUT_ISCD": symbol,
269
+ "FID_INPUT_DATE_1": start.strftime("%Y%m%d"),
270
+ "FID_INPUT_DATE_2": end.strftime("%Y%m%d"),
271
+ "FID_PERIOD_DIV_CODE": param_code,
290
272
  }
291
273
  else:
292
274
  assert ValueError(f"{base_market} is not valid value")
@@ -317,7 +299,7 @@ class KoreaInvest(RestExchange, ImplicitAPI):
317
299
  param_code = "Y"
318
300
  else:
319
301
  assert ValueError(f"{time_frame} is not valid value")
320
-
302
+
321
303
  if start is None:
322
304
  start = self.now(base_market) - timedelta(days=100)
323
305
  if end is None:
@@ -466,7 +448,6 @@ class KoreaInvest(RestExchange, ImplicitAPI):
466
448
 
467
449
  return ksxt.models.KsxtCreateOrderResponse(header=common_header, response=common_response, info=parsed_info)
468
450
 
469
- @RestExchange.check_token
470
451
  def get_market_code_in_feeder(self, symbol: str, base_market: str = "KRW"):
471
452
  if base_market == "KRW":
472
453
  return ""
@@ -479,7 +460,6 @@ class KoreaInvest(RestExchange, ImplicitAPI):
479
460
  else:
480
461
  return ""
481
462
 
482
- @RestExchange.check_token
483
463
  def get_market_code_in_broker(self, symbol: str, base_market: str = "KRW"):
484
464
  if base_market == "KRW":
485
465
  return ""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ksxt
3
- Version: 0.0.10
3
+ Version: 1.0.1
4
4
  License: MIT License
5
5
 
6
6
  Copyright © 2023 AMOSA
@@ -1,10 +1,10 @@
1
1
  ksxt/__init__.py,sha256=Q4kbXuxaCcQg5Z1T2z31lrUc5Cr95RIyv-y6hNf3R0E,131
2
2
  ksxt/bithumb.py,sha256=0pRy8XamlHZ2ypSeJkqiLbkqos5_l3vLKGmqL_EplS8,21405
3
- ksxt/koreainvest.py,sha256=UX2BHIU_Yymi2A5EvNOUwcWkv7g677ihzVYTCjBtfcM,20933
3
+ ksxt/koreainvest.py,sha256=vBsAtTSq7n2y_2VxADeb4ByuggoS2fw5f4G80dJMBbw,20111
4
4
  ksxt/upbit.py,sha256=ploO4Q4iX9wUfa6UagY2S5cvZvMHVDXsXF4fbu8723g,21699
5
5
  ksxt/__pycache__/__init__.cpython-312.pyc,sha256=wOfyuexJUbv8R-aktGSuw5cGx_Nz9O0ReqF9npfS50Q,311
6
6
  ksxt/__pycache__/bithumb.cpython-312.pyc,sha256=EtcBPN5niXbkjMK_PUmfOLANsdRpUqI3GW0EIAWpqVA,24716
7
- ksxt/__pycache__/koreainvest.cpython-312.pyc,sha256=xzeKWRPrgPFZ1VnEupted4XpCRePJJ-_LRU2lBe5LLM,22079
7
+ ksxt/__pycache__/koreainvest.cpython-312.pyc,sha256=NItGfTFI1TamcIVjF8v1ce1X6RrFHa2iE0umedQgqUs,21977
8
8
  ksxt/__pycache__/upbit.cpython-312.pyc,sha256=FciYM5KvlGO3-lhbeTP6E-ZWs2KiaWJkvrLTGfBmbcg,25111
9
9
  ksxt/api/__init__.py,sha256=CZ8AedRG8O9vEdSqTaot5sV3nwpxPZoVOYwrFCLUo6M,736
10
10
  ksxt/api/bithumb.py,sha256=6apoSNoiEOIkR6xQu4rtCSz7sdNdx3BHeu8-m8omh6E,2177
@@ -20,35 +20,36 @@ ksxt/api/auto/koreainvest.py,sha256=z4Bf3C7g2W-eQP99oBeM_J0mq2D3PkbQaulrDwxJB04,
20
20
  ksxt/api/auto/upbit.py,sha256=CtSG0jziqJR4FInHvwDisJfMWCUrUIQQcNPuCGGR7Do,1467
21
21
  ksxt/async_/__init__.py,sha256=ztfV65aN2mXamQSfVm3GLYB6cszkvsCSlUyhfe2H_2M,186
22
22
  ksxt/async_/bithumb.py,sha256=8u87o6HBqU1Ga0-MUBnygrkx6IjwzonImlPV3qZ_zug,19285
23
- ksxt/async_/koreainvest.py,sha256=IW0E1zDZZmCM38XRqoX8uwXxRW1Z1oPLlBWDWZ7br5o,32092
23
+ ksxt/async_/koreainvest.py,sha256=Mc6nsYPQkmc-kRs-pSNNWzKgqPbw65RCGAvmOQLHFw0,20278
24
24
  ksxt/async_/upbit.py,sha256=99IC-czWOV6SsKN645BdbM7Rg_oDo8_iwaIzfGxI_i0,21247
25
25
  ksxt/async_/__pycache__/__init__.cpython-312.pyc,sha256=iIsmwj1LPoOUx4ad9K2yCdmVi5vXv23kOTUX7I5NGx4,380
26
26
  ksxt/async_/__pycache__/bithumb.cpython-312.pyc,sha256=r8oTmvbamu7cGJZYc2YieeFo7IhZf1Rcrh6pIvYHlw8,23397
27
- ksxt/async_/__pycache__/koreainvest.cpython-312.pyc,sha256=xSyD87B8mWGLsiPtsXdFEhdMx1sLapIugyl7UnJf5Q8,31962
27
+ ksxt/async_/__pycache__/koreainvest.cpython-312.pyc,sha256=LNkF2wjNfB1tlqm9f3ck5409o4_k8SOQWm9IS7cpk9w,22813
28
28
  ksxt/async_/__pycache__/upbit.cpython-312.pyc,sha256=4f1hx8Na-P01MlYdgsU1CvPD8iT9z4t02OyVOVQkfa8,26273
29
29
  ksxt/async_/base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
- ksxt/async_/base/async_exchange.py,sha256=ARis9XxuWWS8k37vH4BdIGUQpS4p_PUITt0HaQkly6I,9499
31
- ksxt/async_/base/throttler.py,sha256=o6On7tIQdIVCb9reIe5u7FgqJybWZRikdunEj0FoJok,2017
30
+ ksxt/async_/base/async_exchange.py,sha256=wsX-GCcd5PgdVyJVskcKxF-fECw69uFWdtNnj0MGieU,9406
32
31
  ksxt/async_/base/__pycache__/__init__.cpython-312.pyc,sha256=jREIi8ul1C01r_-NcGb44P6KWeLzw0qVRsaHaegOf_0,153
33
- ksxt/async_/base/__pycache__/async_exchange.cpython-312.pyc,sha256=iS7jjyof971KeDV3yxsMbGZiwWuDlKgYYxhtp84G3b4,15215
32
+ ksxt/async_/base/__pycache__/async_exchange.cpython-312.pyc,sha256=jsTiwPq-M2pmbqi57MyXJqzSSa1z-F9TYrc5Fp3IczQ,15428
34
33
  ksxt/async_/base/__pycache__/throttler.cpython-312.pyc,sha256=wNDfiAHgLIXbSPtK4BOYjnnsWcZSO-Cq4NQXhzrJQpM,3192
35
34
  ksxt/base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
35
  ksxt/base/com_exchange.py,sha256=q7C3Bc5q4ENvNGoghnQCN7YLttgwmFoNj9WsH5qxXNQ,287
37
36
  ksxt/base/errors.py,sha256=msGHmm9YGUOXOaK_VhOUEt5LvIDMMm6G-io8nQbNgA0,141
38
- ksxt/base/exchange.py,sha256=oXvg1kC73KZ6zHrgnmRmjsM9zTPsve3Qw_xCi0rNf2Q,16643
39
- ksxt/base/rest_exchange.py,sha256=_LpMDW_aCIW1knYUJoTRWDAyW17b0ZVdnaX4PfgzKj8,14452
37
+ ksxt/base/exchange.py,sha256=z4Xhkkc0VRZoaoO8QMSHDvHLU6cgIi9VfpaWu7w8kp0,16787
38
+ ksxt/base/rate_limiter.py,sha256=9iF6OB-2u0q4nqtng8Tafn-fM3CVQ-DvdNRJ1aSCS3w,3005
39
+ ksxt/base/rest_exchange.py,sha256=4DpSpLYVPZ-WGGV0oBQIDEUDjAAmj7DrsaI1_7GjGUo,15835
40
40
  ksxt/base/types.py,sha256=3bd3obJhhX1njy_o4LW8hSAHX2MThOI-mZm6iifwno4,59
41
41
  ksxt/base/__pycache__/__init__.cpython-312.pyc,sha256=WnqBpy7gGuw6RNmEVSFdxy4sOZJgZD-fZCJRUbhOHmU,146
42
42
  ksxt/base/__pycache__/errors.cpython-312.pyc,sha256=rAZ-JEC6Ga8eAk4FbYv1foQvMiwK2qj8yVSOzPmKAKo,622
43
- ksxt/base/__pycache__/exchange.cpython-312.pyc,sha256=t-jDvi1BSgju3_FolY42GU7-eJt9y60kRN15a1OX7v4,21552
44
- ksxt/base/__pycache__/rest_exchange.cpython-312.pyc,sha256=mobfwenfwekEZN9ywDKK-eS0hTIo-vb-QqAUw6DuN-s,21780
43
+ ksxt/base/__pycache__/exchange.cpython-312.pyc,sha256=fTNuJeA4AGMHSzEFEiAiwyIBtNzmhB53L7_xP2zw4ok,21658
44
+ ksxt/base/__pycache__/rate_limiter.cpython-312.pyc,sha256=fWueaBMHMPK7Z3_YLJV9bPNPQBax_MF_sEIoqorqwx8,5418
45
+ ksxt/base/__pycache__/rest_exchange.cpython-312.pyc,sha256=yL-rNG758IYojfx-y39L-cPWuPrZQ1RRKEFetOI7hBk,22849
45
46
  ksxt/base/__pycache__/types.cpython-312.pyc,sha256=iF742YNlqCX4ZSIJP4FDPd-3765zIr1VWr-XWXisCsA,232
46
- ksxt/config/__init__.py,sha256=W6RIjNN-e2Mh3P7xwHE7pYVENG_drLTLck5d9nRlz14,70
47
- ksxt/config/bithumb.toml,sha256=74nzGNIGl8U6y_TaxEWJDGNMeDZLtnrKP8wjfA9wbq4,11611
47
+ ksxt/config/__init__.py,sha256=hkQx56FVJD8Gq1A_KzfOqO8BoqJRTX5TnUmqIignnlc,148
48
+ ksxt/config/bithumb.toml,sha256=JgKgHQVATsLcbxKYvekSRonJ4PjJ5x2ejp1eZHHusrQ,11693
48
49
  ksxt/config/koreainvest.toml,sha256=XQRSdEgkWlhbfS6AvJRqQJ_8210i__nfFSI9n3GcgqY,11526
49
50
  ksxt/config/token.toml,sha256=d2apbR6t929w6gLUg-tJ1JT8q6RKPTdYkghxdF9ZDeY,864
50
- ksxt/config/upbit.toml,sha256=hzOg9nxGKXB9eko59Y07kwG7MnldytFdbGjRhmgHUL0,13261
51
- ksxt/config/__pycache__/__init__.cpython-312.pyc,sha256=7T4AHX9kW8N67__FU10XfF8pkIzt31Tuhp9Pw40YwMQ,346
51
+ ksxt/config/upbit.toml,sha256=1aGKZ3HAV28rrTVGZQ418I9NJwhoRh70Vx03oKacIfY,13439
52
+ ksxt/config/__pycache__/__init__.cpython-312.pyc,sha256=R1JKzJvTBlmRvs65EiYpcn8PmiWHfpV0pERid4xZ2hI,425
52
53
  ksxt/market/base.py,sha256=SzNPJECU2iWaQEUesI_REW_A0by_Tv4hs4Hv8p_sr0I,6856
53
54
  ksxt/market/db.py,sha256=ZPr2WlQRUOjhJm1fwFBrdwWG7UJkEpy5Pr0oEQ3Nqb0,386
54
55
  ksxt/market/logging.py,sha256=hkyAhzA72-KCGcVavSOHFSz2FU5QJq_gn6WA6kXs_Pw,698
@@ -112,8 +113,8 @@ ksxt/utils/timer.py,sha256=4_rhXdQDFkKshS5utWrgTteIe71tVSTL1zNW-IjOngM,1228
112
113
  ksxt/utils/__pycache__/safer.cpython-312.pyc,sha256=GxRVIQqkB2OhLPrnBuQtFAtytEocdxWqykuutRkJuA8,1893
113
114
  ksxt/utils/__pycache__/sorter.cpython-312.pyc,sha256=7VUIekH9h5SDsz4L5bjCdSQln-fLgQZII46URitryHI,626
114
115
  ksxt/utils/__pycache__/timer.cpython-312.pyc,sha256=8coj4_LWUtYOdFVhaEHyDbDwkt3JKtmQ1pXPtnsROyY,2005
115
- ksxt-0.0.10.dist-info/LICENSE.txt,sha256=vyuXQcPOZ9BriMQz3h1k3jQTrKGsAjohf8WQHHf6xqo,1080
116
- ksxt-0.0.10.dist-info/METADATA,sha256=iX3L6YoHhu8Bkxgaq19BfoYpDYSDxytjYXm4uOOjqqw,1650
117
- ksxt-0.0.10.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
118
- ksxt-0.0.10.dist-info/top_level.txt,sha256=XLUhkZCur5Pe0BPUV3J0syngIPz7jBb2YlQR4epo5kI,5
119
- ksxt-0.0.10.dist-info/RECORD,,
116
+ ksxt-1.0.1.dist-info/LICENSE.txt,sha256=vyuXQcPOZ9BriMQz3h1k3jQTrKGsAjohf8WQHHf6xqo,1080
117
+ ksxt-1.0.1.dist-info/METADATA,sha256=kLTzqD_vS4HMmnf8LA1SFHdCFgOx9n-xfii-MngXo-c,1649
118
+ ksxt-1.0.1.dist-info/WHEEL,sha256=UvcQYKBHoFqaQd6LKyqHw9fxEolWLQnlzP0h_LgJAfI,91
119
+ ksxt-1.0.1.dist-info/top_level.txt,sha256=XLUhkZCur5Pe0BPUV3J0syngIPz7jBb2YlQR4epo5kI,5
120
+ ksxt-1.0.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (72.1.0)
2
+ Generator: setuptools (74.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,63 +0,0 @@
1
- import asyncio
2
- import collections
3
- from time import time
4
-
5
-
6
- class Throttler:
7
- def __init__(self, config, loop=None):
8
- self.loop = loop
9
- self.config = {
10
- "refillRate": 1.0,
11
- "delay": 0.001,
12
- "cost": 1.0,
13
- "tokens": 0,
14
- "maxCapacity": 2000,
15
- "capacity": 1.0,
16
- }
17
-
18
- self.config.update(config)
19
- self.queue = collections.deque()
20
- self.running = False
21
-
22
- async def looper(self):
23
- last_timestamp = time() * 1000
24
- while self.running:
25
- future, cost = self.queue[0]
26
- cost = self.config["cost"] if cost is None else cost
27
- if self.config["tokens"] >= 0:
28
- self.config["tokens"] -= cost
29
-
30
- if not future.done():
31
- future.set_result(None)
32
-
33
- self.queue.popleft()
34
-
35
- # context switch
36
- await asyncio.sleep(0)
37
-
38
- if len(self.queue) == 0:
39
- self.running = False
40
- else:
41
- await asyncio.sleep(self.config["delay"])
42
- now = time() * 1000
43
- elapsed = now - last_timestamp
44
- last_timestamp = now
45
- self.config["tokens"] = min(
46
- self.config["tokens"] + elapsed * self.config["refillRate"], self.config["capacity"]
47
- )
48
-
49
- def __call__(self, cost=None):
50
- future = asyncio.Future()
51
- if len(self.queue) > self.config["maxCapacity"]:
52
- raise RuntimeError(
53
- "throttle queue is over maxCapacity ("
54
- + str(int(self.config["maxCapacity"]))
55
- + "), see https://github.com/ccxt/ccxt/issues/11645#issuecomment-1195695526"
56
- )
57
-
58
- self.queue.append((future, cost))
59
- if not self.running:
60
- self.running = True
61
- asyncio.ensure_future(self.looper(), loop=self.loop)
62
-
63
- return future