crypticorn 2.15.0__py3-none-any.whl → 2.17.0__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.
Files changed (168) hide show
  1. crypticorn/__init__.py +2 -2
  2. crypticorn/auth/client/api/admin_api.py +397 -13
  3. crypticorn/auth/client/api/auth_api.py +3610 -341
  4. crypticorn/auth/client/api/service_api.py +249 -7
  5. crypticorn/auth/client/api/user_api.py +2295 -179
  6. crypticorn/auth/client/api/wallet_api.py +1468 -81
  7. crypticorn/auth/client/configuration.py +2 -2
  8. crypticorn/auth/client/models/create_api_key_request.py +2 -1
  9. crypticorn/auth/client/models/get_api_keys200_response_inner.py +2 -1
  10. crypticorn/auth/client/rest.py +23 -4
  11. crypticorn/auth/main.py +8 -5
  12. crypticorn/cli/init.py +1 -1
  13. crypticorn/cli/templates/.env.docker.temp +3 -0
  14. crypticorn/cli/templates/.env.example.temp +4 -0
  15. crypticorn/cli/templates/Dockerfile +5 -2
  16. crypticorn/client.py +226 -59
  17. crypticorn/common/__init__.py +1 -0
  18. crypticorn/common/auth.py +45 -14
  19. crypticorn/common/decorators.py +1 -2
  20. crypticorn/common/enums.py +0 -2
  21. crypticorn/common/errors.py +10 -0
  22. crypticorn/common/metrics.py +30 -0
  23. crypticorn/common/middleware.py +94 -1
  24. crypticorn/common/pagination.py +252 -18
  25. crypticorn/common/router/admin_router.py +2 -2
  26. crypticorn/common/router/status_router.py +40 -2
  27. crypticorn/common/scopes.py +2 -0
  28. crypticorn/common/warnings.py +8 -0
  29. crypticorn/dex/__init__.py +6 -0
  30. crypticorn/dex/client/__init__.py +49 -0
  31. crypticorn/dex/client/api/__init__.py +6 -0
  32. crypticorn/dex/client/api/admin_api.py +2986 -0
  33. crypticorn/dex/client/api/signals_api.py +1798 -0
  34. crypticorn/dex/client/api/status_api.py +892 -0
  35. crypticorn/dex/client/api_client.py +758 -0
  36. crypticorn/dex/client/api_response.py +20 -0
  37. crypticorn/dex/client/configuration.py +620 -0
  38. crypticorn/dex/client/exceptions.py +220 -0
  39. crypticorn/dex/client/models/__init__.py +30 -0
  40. crypticorn/dex/client/models/api_error_identifier.py +121 -0
  41. crypticorn/dex/client/models/api_error_level.py +37 -0
  42. crypticorn/dex/client/models/api_error_type.py +37 -0
  43. crypticorn/dex/client/models/exception_detail.py +117 -0
  44. crypticorn/dex/client/models/log_level.py +38 -0
  45. crypticorn/dex/client/models/paginated_response_signal_with_token.py +134 -0
  46. crypticorn/dex/client/models/risk.py +86 -0
  47. crypticorn/dex/client/models/signal_overview_stats.py +158 -0
  48. crypticorn/dex/client/models/signal_volume.py +84 -0
  49. crypticorn/dex/client/models/signal_with_token.py +163 -0
  50. crypticorn/dex/client/models/token_data.py +127 -0
  51. crypticorn/dex/client/models/token_detail.py +116 -0
  52. crypticorn/dex/client/py.typed +0 -0
  53. crypticorn/dex/client/rest.py +217 -0
  54. crypticorn/dex/main.py +1 -0
  55. crypticorn/hive/client/api/admin_api.py +1173 -47
  56. crypticorn/hive/client/api/data_api.py +499 -17
  57. crypticorn/hive/client/api/models_api.py +1595 -87
  58. crypticorn/hive/client/api/status_api.py +397 -16
  59. crypticorn/hive/client/api_client.py +0 -5
  60. crypticorn/hive/client/models/api_error_identifier.py +1 -1
  61. crypticorn/hive/client/models/coin_info.py +1 -1
  62. crypticorn/hive/client/models/exception_detail.py +1 -1
  63. crypticorn/hive/client/models/target_info.py +1 -1
  64. crypticorn/hive/client/rest.py +23 -4
  65. crypticorn/hive/main.py +99 -25
  66. crypticorn/hive/utils.py +2 -2
  67. crypticorn/klines/client/api/admin_api.py +1173 -47
  68. crypticorn/klines/client/api/change_in_timeframe_api.py +269 -11
  69. crypticorn/klines/client/api/funding_rates_api.py +315 -11
  70. crypticorn/klines/client/api/ohlcv_data_api.py +390 -11
  71. crypticorn/klines/client/api/status_api.py +397 -16
  72. crypticorn/klines/client/api/symbols_api.py +216 -11
  73. crypticorn/klines/client/api/udf_api.py +1268 -51
  74. crypticorn/klines/client/api_client.py +0 -5
  75. crypticorn/klines/client/models/api_error_identifier.py +3 -1
  76. crypticorn/klines/client/models/exception_detail.py +1 -1
  77. crypticorn/klines/client/models/ohlcv.py +1 -1
  78. crypticorn/klines/client/models/symbol_group.py +1 -1
  79. crypticorn/klines/client/models/udf_config.py +1 -1
  80. crypticorn/klines/client/rest.py +23 -4
  81. crypticorn/klines/main.py +89 -12
  82. crypticorn/metrics/client/api/admin_api.py +1173 -47
  83. crypticorn/metrics/client/api/exchanges_api.py +1370 -145
  84. crypticorn/metrics/client/api/indicators_api.py +622 -17
  85. crypticorn/metrics/client/api/logs_api.py +296 -11
  86. crypticorn/metrics/client/api/marketcap_api.py +1207 -67
  87. crypticorn/metrics/client/api/markets_api.py +343 -11
  88. crypticorn/metrics/client/api/quote_currencies_api.py +228 -11
  89. crypticorn/metrics/client/api/status_api.py +397 -16
  90. crypticorn/metrics/client/api/tokens_api.py +382 -15
  91. crypticorn/metrics/client/api_client.py +0 -5
  92. crypticorn/metrics/client/configuration.py +4 -2
  93. crypticorn/metrics/client/models/exception_detail.py +1 -1
  94. crypticorn/metrics/client/models/exchange_mapping.py +1 -1
  95. crypticorn/metrics/client/models/marketcap_ranking.py +1 -1
  96. crypticorn/metrics/client/models/marketcap_symbol_ranking.py +1 -1
  97. crypticorn/metrics/client/models/ohlcv.py +1 -1
  98. crypticorn/metrics/client/rest.py +23 -4
  99. crypticorn/metrics/main.py +113 -19
  100. crypticorn/pay/client/api/admin_api.py +1585 -57
  101. crypticorn/pay/client/api/now_payments_api.py +961 -39
  102. crypticorn/pay/client/api/payments_api.py +562 -17
  103. crypticorn/pay/client/api/products_api.py +880 -30
  104. crypticorn/pay/client/api/status_api.py +397 -16
  105. crypticorn/pay/client/api_client.py +0 -5
  106. crypticorn/pay/client/configuration.py +2 -2
  107. crypticorn/pay/client/models/api_error_identifier.py +7 -7
  108. crypticorn/pay/client/models/exception_detail.py +1 -1
  109. crypticorn/pay/client/models/now_create_invoice_req.py +1 -1
  110. crypticorn/pay/client/models/now_create_invoice_res.py +1 -1
  111. crypticorn/pay/client/models/product.py +1 -1
  112. crypticorn/pay/client/models/product_create.py +1 -1
  113. crypticorn/pay/client/models/product_update.py +1 -1
  114. crypticorn/pay/client/models/scope.py +1 -0
  115. crypticorn/pay/client/rest.py +23 -4
  116. crypticorn/pay/main.py +10 -6
  117. crypticorn/trade/client/__init__.py +11 -1
  118. crypticorn/trade/client/api/__init__.py +0 -1
  119. crypticorn/trade/client/api/admin_api.py +1184 -55
  120. crypticorn/trade/client/api/api_keys_api.py +1678 -162
  121. crypticorn/trade/client/api/bots_api.py +7563 -187
  122. crypticorn/trade/client/api/exchanges_api.py +565 -19
  123. crypticorn/trade/client/api/notifications_api.py +1290 -116
  124. crypticorn/trade/client/api/orders_api.py +393 -55
  125. crypticorn/trade/client/api/status_api.py +397 -13
  126. crypticorn/trade/client/api/strategies_api.py +1133 -77
  127. crypticorn/trade/client/api/trading_actions_api.py +786 -65
  128. crypticorn/trade/client/models/__init__.py +11 -0
  129. crypticorn/trade/client/models/actions_count.py +88 -0
  130. crypticorn/trade/client/models/api_error_identifier.py +8 -7
  131. crypticorn/trade/client/models/bot.py +7 -18
  132. crypticorn/trade/client/models/bot_create.py +17 -1
  133. crypticorn/trade/client/models/bot_update.py +17 -1
  134. crypticorn/trade/client/models/exchange.py +6 -1
  135. crypticorn/trade/client/models/exchange_key.py +1 -1
  136. crypticorn/trade/client/models/exchange_key_balance.py +111 -0
  137. crypticorn/trade/client/models/exchange_key_create.py +17 -1
  138. crypticorn/trade/client/models/exchange_key_update.py +17 -1
  139. crypticorn/trade/client/models/execution_ids.py +1 -1
  140. crypticorn/trade/client/models/futures_balance.py +27 -25
  141. crypticorn/trade/client/models/futures_trading_action.py +6 -28
  142. crypticorn/trade/client/models/futures_trading_action_create.py +10 -13
  143. crypticorn/trade/client/models/notification.py +17 -1
  144. crypticorn/trade/client/models/notification_create.py +18 -2
  145. crypticorn/trade/client/models/notification_update.py +17 -1
  146. crypticorn/trade/client/models/order.py +2 -14
  147. crypticorn/trade/client/models/orders_count.py +88 -0
  148. crypticorn/trade/client/models/paginated_response_futures_trading_action.py +134 -0
  149. crypticorn/trade/client/models/paginated_response_order.py +134 -0
  150. crypticorn/trade/client/models/pn_l.py +95 -0
  151. crypticorn/trade/client/models/post_futures_action.py +1 -1
  152. crypticorn/trade/client/models/spot_balance.py +109 -0
  153. crypticorn/trade/client/models/spot_trading_action_create.py +4 -1
  154. crypticorn/trade/client/models/strategy.py +22 -4
  155. crypticorn/trade/client/models/strategy_create.py +23 -5
  156. crypticorn/trade/client/models/strategy_exchange_info.py +16 -4
  157. crypticorn/trade/client/models/strategy_update.py +19 -3
  158. crypticorn/trade/client/models/tpsl.py +4 -27
  159. crypticorn/trade/client/models/tpsl_create.py +6 -19
  160. crypticorn/trade/client/rest.py +23 -4
  161. crypticorn/trade/main.py +15 -12
  162. {crypticorn-2.15.0.dist-info → crypticorn-2.17.0.dist-info}/METADATA +65 -20
  163. {crypticorn-2.15.0.dist-info → crypticorn-2.17.0.dist-info}/RECORD +167 -132
  164. crypticorn/trade/client/api/futures_trading_panel_api.py +0 -1285
  165. {crypticorn-2.15.0.dist-info → crypticorn-2.17.0.dist-info}/WHEEL +0 -0
  166. {crypticorn-2.15.0.dist-info → crypticorn-2.17.0.dist-info}/entry_points.txt +0 -0
  167. {crypticorn-2.15.0.dist-info → crypticorn-2.17.0.dist-info}/licenses/LICENSE +0 -0
  168. {crypticorn-2.15.0.dist-info → crypticorn-2.17.0.dist-info}/top_level.txt +0 -0
@@ -195,7 +195,7 @@ class Configuration:
195
195
  debug: Optional[bool] = None,
196
196
  ) -> None:
197
197
  """Constructor"""
198
- self._base_path = "https://api.crypticorn.dev/v1/auth" if host is None else host
198
+ self._base_path = "http://localhost/v1/auth" if host is None else host
199
199
  """Default Base url
200
200
  """
201
201
  self.server_index = 0 if server_index is None and host is None else server_index
@@ -528,7 +528,7 @@ class Configuration:
528
528
  """
529
529
  return [
530
530
  {
531
- "url": "https://api.crypticorn.dev/v1/auth",
531
+ "url": "http://localhost/v1/auth",
532
532
  "description": "No description provided",
533
533
  }
534
534
  ]
@@ -50,6 +50,7 @@ class CreateApiKeyRequest(BaseModel):
50
50
  if i not in set(
51
51
  [
52
52
  "read:predictions",
53
+ "read:dex:signals",
53
54
  "read:hive:model",
54
55
  "read:hive:data",
55
56
  "write:hive:model",
@@ -84,7 +85,7 @@ class CreateApiKeyRequest(BaseModel):
84
85
  ]
85
86
  ):
86
87
  raise ValueError(
87
- "each list item must be one of ('read:predictions', 'read:hive:model', 'read:hive:data', 'write:hive:model', 'read:trade:bots', 'write:trade:bots', 'read:trade:exchangekeys', 'write:trade:exchangekeys', 'read:trade:orders', 'read:trade:actions', 'write:trade:actions', 'read:trade:exchanges', 'read:trade:futures', 'write:trade:futures', 'read:trade:notifications', 'write:trade:notifications', 'read:trade:strategies', 'write:trade:strategies', 'read:pay:payments', 'read:pay:products', 'write:pay:products', 'read:pay:now', 'write:pay:now', 'read:metrics:marketcap', 'read:metrics:indicators', 'read:metrics:exchanges', 'read:metrics:tokens', 'read:metrics:markets', 'read:sentiment', 'read:klines', 'read:admin', 'write:admin')"
88
+ "each list item must be one of ('read:predictions', 'read:dex:signals', 'read:hive:model', 'read:hive:data', 'write:hive:model', 'read:trade:bots', 'write:trade:bots', 'read:trade:exchangekeys', 'write:trade:exchangekeys', 'read:trade:orders', 'read:trade:actions', 'write:trade:actions', 'read:trade:exchanges', 'read:trade:futures', 'write:trade:futures', 'read:trade:notifications', 'write:trade:notifications', 'read:trade:strategies', 'write:trade:strategies', 'read:pay:payments', 'read:pay:products', 'write:pay:products', 'read:pay:now', 'write:pay:now', 'read:metrics:marketcap', 'read:metrics:indicators', 'read:metrics:exchanges', 'read:metrics:tokens', 'read:metrics:markets', 'read:sentiment', 'read:klines', 'read:admin', 'write:admin')"
88
89
  )
89
90
  return value
90
91
 
@@ -58,6 +58,7 @@ class GetApiKeys200ResponseInner(BaseModel):
58
58
  if i not in set(
59
59
  [
60
60
  "read:predictions",
61
+ "read:dex:signals",
61
62
  "read:hive:model",
62
63
  "read:hive:data",
63
64
  "write:hive:model",
@@ -92,7 +93,7 @@ class GetApiKeys200ResponseInner(BaseModel):
92
93
  ]
93
94
  ):
94
95
  raise ValueError(
95
- "each list item must be one of ('read:predictions', 'read:hive:model', 'read:hive:data', 'write:hive:model', 'read:trade:bots', 'write:trade:bots', 'read:trade:exchangekeys', 'write:trade:exchangekeys', 'read:trade:orders', 'read:trade:actions', 'write:trade:actions', 'read:trade:exchanges', 'read:trade:futures', 'write:trade:futures', 'read:trade:notifications', 'write:trade:notifications', 'read:trade:strategies', 'write:trade:strategies', 'read:pay:payments', 'read:pay:products', 'write:pay:products', 'read:pay:now', 'write:pay:now', 'read:metrics:marketcap', 'read:metrics:indicators', 'read:metrics:exchanges', 'read:metrics:tokens', 'read:metrics:markets', 'read:sentiment', 'read:klines', 'read:admin', 'write:admin')"
96
+ "each list item must be one of ('read:predictions', 'read:dex:signals', 'read:hive:model', 'read:hive:data', 'write:hive:model', 'read:trade:bots', 'write:trade:bots', 'read:trade:exchangekeys', 'write:trade:exchangekeys', 'read:trade:orders', 'read:trade:actions', 'write:trade:actions', 'read:trade:exchanges', 'read:trade:futures', 'write:trade:futures', 'read:trade:notifications', 'write:trade:notifications', 'read:trade:strategies', 'write:trade:strategies', 'read:pay:payments', 'read:pay:products', 'write:pay:products', 'read:pay:now', 'write:pay:now', 'read:metrics:marketcap', 'read:metrics:indicators', 'read:metrics:exchanges', 'read:metrics:tokens', 'read:metrics:markets', 'read:sentiment', 'read:klines', 'read:admin', 'write:admin')"
96
97
  )
97
98
  return value
98
99
 
@@ -77,6 +77,7 @@ class RESTClientObject:
77
77
 
78
78
  self.pool_manager: Optional[aiohttp.ClientSession] = None
79
79
  self.retry_client: Optional[aiohttp_retry.RetryClient] = None
80
+ self.is_sync: bool = False # Track whether this is sync or async mode
80
81
 
81
82
  async def close(self) -> None:
82
83
  if self.pool_manager:
@@ -170,7 +171,9 @@ class RESTClientObject:
170
171
 
171
172
  pool_manager: Union[aiohttp.ClientSession, aiohttp_retry.RetryClient]
172
173
 
173
- # https pool manager
174
+ # For sync operations, always use a fresh session
175
+ should_close_session = False
176
+
174
177
  if self.pool_manager is None:
175
178
  self.pool_manager = aiohttp.ClientSession(
176
179
  connector=aiohttp.TCPConnector(
@@ -178,6 +181,9 @@ class RESTClientObject:
178
181
  ),
179
182
  trust_env=True,
180
183
  )
184
+ # Only close session automatically in sync mode
185
+ should_close_session = self.is_sync
186
+
181
187
  pool_manager = self.pool_manager
182
188
 
183
189
  if self.retries is not None and method in ALLOW_RETRY_METHODS:
@@ -193,6 +199,19 @@ class RESTClientObject:
193
199
  )
194
200
  pool_manager = self.retry_client
195
201
 
196
- r = await pool_manager.request(**args)
197
-
198
- return RESTResponse(r)
202
+ try:
203
+ r = await pool_manager.request(**args)
204
+ # For sessions we're about to close, read the data immediately
205
+ if should_close_session:
206
+ response = RESTResponse(r)
207
+ await response.read() # Read data before closing session
208
+ return response
209
+ else:
210
+ return RESTResponse(r)
211
+ finally:
212
+ if should_close_session:
213
+ if self.retry_client is not None:
214
+ await self.retry_client.close()
215
+ self.retry_client = None
216
+ await self.pool_manager.close()
217
+ self.pool_manager = None
crypticorn/auth/main.py CHANGED
@@ -25,14 +25,17 @@ class AuthClient:
25
25
  self,
26
26
  config: Configuration,
27
27
  http_client: Optional[ClientSession] = None,
28
+ is_sync: bool = False,
28
29
  ):
29
30
  self.config = config
30
31
  self.base_client = ApiClient(configuration=self.config)
31
32
  if http_client is not None:
32
33
  self.base_client.rest_client.pool_manager = http_client
34
+ # Pass sync context to REST client for proper session management
35
+ self.base_client.rest_client.is_sync = is_sync
33
36
  # Instantiate all the endpoint clients
34
- self.admin = AdminApi(self.base_client)
35
- self.service = ServiceApi(self.base_client)
36
- self.user = UserApi(self.base_client)
37
- self.wallet = WalletApi(self.base_client)
38
- self.login = AuthApi(self.base_client)
37
+ self.admin = AdminApi(self.base_client, is_sync=is_sync)
38
+ self.service = ServiceApi(self.base_client, is_sync=is_sync)
39
+ self.user = UserApi(self.base_client, is_sync=is_sync)
40
+ self.wallet = WalletApi(self.base_client, is_sync=is_sync)
41
+ self.login = AuthApi(self.base_client, is_sync=is_sync)
crypticorn/cli/init.py CHANGED
@@ -33,7 +33,7 @@ def copy_template(template_name: str, target_path: Path):
33
33
 
34
34
  def check_file_exists(path: Path, force: bool):
35
35
  if path.exists() and not force:
36
- click.secho(f"File already exists, use --force / -f to overwrite", fg="red")
36
+ click.secho("File already exists, use --force / -f to overwrite", fg="red")
37
37
  return False
38
38
  return True
39
39
 
@@ -0,0 +1,3 @@
1
+ # Specifies which API environment to use in docker builds: "0" if the API_ENV should be used, "1" for the internal host.
2
+ # Any database connections would have to be overridden as well.
3
+ IS_DOCKER=1
@@ -0,0 +1,4 @@
1
+ # Specifies which API environment to use: "local", "dev", "prod", or "docker".
2
+ # When running inside Docker, this defaults to "docker" (internal network host),
3
+ # unless IS_DOCKER is set to 0 in .env.docker.
4
+ API_ENV=local
@@ -19,14 +19,17 @@ WORKDIR /code
19
19
  # and to the workflow:
20
20
  # env:
21
21
  # API_ENV: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }}
22
- ARG API_ENV
22
+ # and in the docker build:
23
+ # build-args: |
24
+ # CACHEBUST=${{ github.run_id }} # invalidates dependency cache on every workflow run
23
25
 
24
- RUN pip install --no-cache-dir -r requirements.txt
26
+ ARG API_ENV
25
27
 
26
28
  RUN if [ "$API_ENV" != "prod" ]; then \
27
29
  pip install --pre crypticorn; \
28
30
  fi
29
31
 
32
+ RUN pip install --no-cache-dir -r requirements.txt
30
33
 
31
34
  COPY ./src /code/src
32
35
 
crypticorn/client.py CHANGED
@@ -1,22 +1,30 @@
1
1
  from typing import TypeVar, Optional
2
+ import warnings
2
3
  from aiohttp import ClientSession, ClientTimeout, TCPConnector
3
4
  from crypticorn.hive import HiveClient
4
5
  from crypticorn.klines import KlinesClient
5
6
  from crypticorn.pay import PayClient
7
+
6
8
  from crypticorn.trade import TradeClient
7
9
  from crypticorn.metrics import MetricsClient
8
10
  from crypticorn.auth import AuthClient
9
- from crypticorn.common import BaseUrl, ApiVersion, Service, apikey_header as aph
11
+ from crypticorn.common import (
12
+ BaseUrl,
13
+ ApiVersion,
14
+ Service,
15
+ apikey_header as aph,
16
+ CrypticornDeprecatedSince217,
17
+ )
10
18
  from importlib.metadata import version
19
+ from typing_extensions import deprecated
11
20
 
12
21
  ConfigT = TypeVar("ConfigT")
13
22
  SubClient = TypeVar("SubClient")
14
23
 
15
24
 
16
- class ApiClient:
25
+ class BaseAsyncClient:
17
26
  """
18
- The official Python client for interacting with the Crypticorn API.
19
- It is consisting of multiple microservices covering the whole stack of the Crypticorn project.
27
+ Base class for Crypticorn API clients containing shared functionality.
20
28
  """
21
29
 
22
30
  def __init__(
@@ -24,20 +32,23 @@ class ApiClient:
24
32
  api_key: Optional[str] = None,
25
33
  jwt: Optional[str] = None,
26
34
  base_url: BaseUrl = BaseUrl.PROD,
27
- *,
35
+ is_sync: bool = False,
28
36
  http_client: Optional[ClientSession] = None,
29
37
  ):
30
- self.base_url = base_url
31
- """The base URL the client will use to connect to the API."""
32
- self.api_key = api_key
33
- """The API key to use for authentication (recommended)."""
34
- self.jwt = jwt
35
- """The JWT to use for authentication (not recommended)."""
36
- self.version = version("crypticorn")
37
- """The version of the client."""
38
-
38
+ """
39
+ :param api_key: The API key to use for authentication (recommended).
40
+ :param jwt: The JWT to use for authentication (not recommended).
41
+ :param base_url: The base URL the client will use to connect to the API.
42
+ :param is_sync: Whether this client should operate in synchronous mode.
43
+ :param http_client: Optional aiohttp ClientSession to use for HTTP requests.
44
+ """
45
+ self._base_url = base_url
46
+ self._api_key = api_key
47
+ self._jwt = jwt
48
+ self._is_sync = is_sync
39
49
  self._http_client = http_client
40
50
  self._owns_http_client = http_client is None # whether we own the http client
51
+
41
52
  self._service_classes: dict[Service, type[SubClient]] = {
42
53
  Service.HIVE: HiveClient,
43
54
  Service.TRADE: TradeClient,
@@ -46,12 +57,69 @@ class ApiClient:
46
57
  Service.METRICS: MetricsClient,
47
58
  Service.AUTH: AuthClient,
48
59
  }
49
- self._services: dict[Service, SubClient] = {
50
- service: client_class(
51
- self._get_default_config(service), http_client=self._http_client
52
- )
53
- for service, client_class in self._service_classes.items()
54
- }
60
+
61
+ self._services: dict[Service, SubClient] = self._create_services()
62
+
63
+ def _create_services(self) -> dict[Service, SubClient]:
64
+ """Create services with the appropriate configuration based on sync/async mode."""
65
+ services = {}
66
+ for service, client_class in self._service_classes.items():
67
+ config = self._get_default_config(service)
68
+ # For sync clients, don't pass the persistent http_client
69
+ # Let each operation manage its own session
70
+ if self._is_sync:
71
+ services[service] = client_class(
72
+ config, http_client=None, is_sync=self._is_sync
73
+ )
74
+ else:
75
+ services[service] = client_class(
76
+ config, http_client=self._http_client, is_sync=self._is_sync
77
+ )
78
+ return services
79
+
80
+ @property
81
+ def base_url(self) -> BaseUrl:
82
+ """
83
+ The base URL the client will use to connect to the API.
84
+ """
85
+ return self._base_url
86
+
87
+ @property
88
+ def api_key(self) -> Optional[str]:
89
+ """
90
+ The API key the client will use to connect to the API.
91
+ This is the preferred way to authenticate.
92
+ """
93
+ return self._api_key
94
+
95
+ @property
96
+ def jwt(self) -> Optional[str]:
97
+ """
98
+ The JWT the client will use to connect to the API.
99
+ This is the not the preferred way to authenticate.
100
+ """
101
+ return self._jwt
102
+
103
+ @property
104
+ def version(self) -> str:
105
+ """
106
+ The version of the client.
107
+ """
108
+ return version("crypticorn")
109
+
110
+ @property
111
+ def is_sync(self) -> bool:
112
+ """
113
+ Whether this client operates in synchronous mode.
114
+ """
115
+ return self._is_sync
116
+
117
+ @property
118
+ def http_client(self) -> Optional[ClientSession]:
119
+ """
120
+ The HTTP client session being used, if any.
121
+ """
122
+ return self._http_client
55
123
 
56
124
  @property
57
125
  def hive(self) -> HiveClient:
@@ -95,17 +163,67 @@ class ApiClient:
95
163
  """
96
164
  return self._services[Service.AUTH]
97
165
 
166
+ def configure(self, config: ConfigT, service: Service) -> None:
167
+ """
168
+ Update a sub-client's configuration by overriding with the values set in the new config.
169
+ Useful for testing a specific service against a local server instead of the default proxy.
170
+
171
+ :param config: The new configuration to use for the sub-client.
172
+ :param service: The service to configure.
173
+
174
+ Example:
175
+ >>> # For async client
176
+ >>> async with AsyncClient() as client:
177
+ ... client.configure(config=HiveConfig(host="http://localhost:8000"), service=Service.HIVE)
178
+ >>>
179
+ >>> # For sync client
180
+ >>> with SyncClient() as client:
181
+ ... client.configure(config=HiveConfig(host="http://localhost:8000"), service=Service.HIVE)
182
+ """
183
+ assert Service.validate(service), f"Invalid service: {service}"
184
+ client = self._services[service]
185
+ new_config = client.config
186
+ for attr in vars(config):
187
+ new_value = getattr(config, attr)
188
+ if new_value:
189
+ setattr(new_config, attr, new_value)
190
+
191
+ # Recreate service with new config and appropriate parameters
192
+ if self._is_sync:
193
+ self._services[service] = type(client)(
194
+ new_config, is_sync=self._is_sync, http_client=self._http_client
195
+ )
196
+ else:
197
+ self._services[service] = type(client)(
198
+ new_config, http_client=self._http_client
199
+ )
200
+
201
+ def _get_default_config(self, service, version=None):
202
+ if version is None:
203
+ version = ApiVersion.V1
204
+ config_class = self._service_classes[service].config_class
205
+ return config_class(
206
+ host=f"{self.base_url}/{version}/{service}",
207
+ access_token=self.jwt,
208
+ api_key={aph.scheme_name: self.api_key} if self.api_key else None,
209
+ )
210
+
98
211
  async def close(self):
99
- # close each in sync
212
+ """Close the client and clean up resources."""
213
+ # close each service
100
214
  for service in self._services.values():
101
- if hasattr(service.base_client, "close") and self._owns_http_client:
215
+ if (
216
+ hasattr(service, "base_client")
217
+ and hasattr(service.base_client, "close")
218
+ and self._owns_http_client
219
+ ):
102
220
  await service.base_client.close()
103
- # close shared in async
221
+ # close shared http client if we own it
104
222
  if self._http_client and self._owns_http_client:
105
223
  await self._http_client.close()
106
224
  self._http_client = None
107
225
 
108
- async def _ensure_session(self) -> None:
226
+ def _ensure_session(self) -> None:
109
227
  """
110
228
  Lazily create the shared HTTP client when first needed and pass it to all subclients.
111
229
  """
@@ -115,48 +233,97 @@ class ApiClient:
115
233
  connector=TCPConnector(limit=100, limit_per_host=20),
116
234
  headers={"User-Agent": f"crypticorn/python/{self.version}"},
117
235
  )
118
- for service in self._services.values():
119
- if hasattr(service, "base_client") and hasattr(
120
- service.base_client, "rest_client"
121
- ):
122
- service.base_client.rest_client.pool_manager = self._http_client
236
+ # Update services to use the new session
237
+ self._services = self._create_services()
123
238
 
124
- def _get_default_config(self, service, version=None):
125
- if version is None:
126
- version = ApiVersion.V1
127
- config_class = self._service_classes[service].config_class
128
- return config_class(
129
- host=f"{self.base_url}/{version}/{service}",
130
- access_token=self.jwt,
131
- api_key={aph.scheme_name: self.api_key} if self.api_key else None,
132
- )
133
-
134
- def configure(self, config: ConfigT, service: Service) -> None:
135
- """
136
- Update a sub-client's configuration by overriding with the values set in the new config.
137
- Useful for testing a specific service against a local server instead of the default proxy.
138
239
 
139
- :param config: The new configuration to use for the sub-client.
140
- :param service: The service to configure.
240
+ class AsyncClient(BaseAsyncClient):
241
+ """
242
+ The official async Python client for interacting with the Crypticorn API.
243
+ It is consisting of multiple microservices covering the whole stack of the Crypticorn project.
244
+ """
141
245
 
142
- Example:
143
- >>> async with ApiClient() as client:
144
- ... client.configure(config=HiveConfig(host="http://localhost:8000"), service=Service.HIVE)
246
+ def __init__(
247
+ self,
248
+ api_key: Optional[str] = None,
249
+ jwt: Optional[str] = None,
250
+ base_url: BaseUrl = BaseUrl.PROD,
251
+ *,
252
+ http_client: Optional[ClientSession] = None,
253
+ ):
145
254
  """
146
- assert Service.validate(service), f"Invalid service: {service}"
147
- client = self._services[service]
148
- new_config = client.config
149
- for attr in vars(config):
150
- new_value = getattr(config, attr)
151
- if new_value:
152
- setattr(new_config, attr, new_value)
153
- self._services[service] = type(client)(
154
- new_config, http_client=self._http_client
155
- )
255
+ :param api_key: The API key to use for authentication (recommended).
256
+ :param jwt: The JWT to use for authentication (not recommended).
257
+ :param base_url: The base URL the client will use to connect to the API.
258
+ :param http_client: The HTTP client to use for the client.
259
+ """
260
+ # Initialize as async client
261
+ super().__init__(api_key, jwt, base_url, is_sync=False, http_client=http_client)
262
+
263
+ async def close(self):
264
+ await super().close()
156
265
 
157
266
  async def __aenter__(self):
158
- await self._ensure_session()
267
+ self._ensure_session()
159
268
  return self
160
269
 
161
270
  async def __aexit__(self, exc_type, exc_val, exc_tb):
162
271
  await self.close()
272
+
273
+
274
+ @deprecated("Use AsyncClient instead", category=None)
275
+ class ApiClient(AsyncClient):
276
+ def __init__(self, *args, **kwargs):
277
+ warnings.warn(
278
+ "ApiClient is deprecated. Use AsyncClient instead.",
279
+ CrypticornDeprecatedSince217,
280
+ )
281
+ super().__init__(*args, **kwargs)
282
+
283
+
284
+ class SyncClient(BaseAsyncClient):
285
+ """
286
+ The official synchronous Python client for interacting with the Crypticorn API.
287
+ """
288
+
289
+ def __init__(
290
+ self,
291
+ api_key: Optional[str] = None,
292
+ jwt: Optional[str] = None,
293
+ base_url: BaseUrl = BaseUrl.PROD,
294
+ *,
295
+ http_client: Optional[ClientSession] = None,
296
+ ):
297
+ """
298
+ :param http_client: Optional aiohttp ClientSession to use for HTTP requests.
299
+ Note: For sync client, session management is handled automatically.
300
+ """
301
+ super().__init__(api_key, jwt, base_url, is_sync=True, http_client=http_client)
302
+
303
+ def close(self):
304
+ """Close the client and clean up resources."""
305
+ # For sync client, don't maintain persistent sessions
306
+ # Each operation creates its own session within async_to_sync
307
+ self._http_client = None
308
+
309
+ def _ensure_session(self) -> None:
310
+ """
311
+ For sync client, don't create persistent sessions.
312
+ Let each async_to_sync call handle its own session.
313
+ """
314
+ # Don't create persistent sessions in sync mode
315
+ # Each API call will handle session creation/cleanup within async_to_sync
316
+ pass
317
+
318
+ def __enter__(self):
319
+ return self
320
+
321
+ def __exit__(self, exc_type, exc_val, exc_tb):
322
+ self.close()
323
+
324
+ def __del__(self):
325
+ """Automatic cleanup when the object is garbage collected."""
326
+ try:
327
+ self.close()
328
+ except Exception:
329
+ pass
@@ -13,5 +13,6 @@ from crypticorn.common.ansi_colors import *
13
13
  from crypticorn.common.middleware import *
14
14
  from crypticorn.common.warnings import *
15
15
  from crypticorn.common.openapi import *
16
+ from crypticorn.common.metrics import *
16
17
  from crypticorn.common.router.status_router import router as status_router
17
18
  from crypticorn.common.router.admin_router import router as admin_router
crypticorn/common/auth.py CHANGED
@@ -7,18 +7,19 @@ from crypticorn.common.exceptions import (
7
7
  ApiError,
8
8
  HTTPException,
9
9
  ExceptionContent,
10
- WebSocketException,
11
10
  )
12
11
  from crypticorn.common.urls import BaseUrl, Service, ApiVersion
13
- from fastapi import Depends, Query
12
+ from fastapi import Depends, Query, Request
14
13
  from fastapi.security import (
15
14
  HTTPAuthorizationCredentials,
16
15
  SecurityScopes,
17
16
  HTTPBearer,
18
17
  APIKeyHeader,
18
+ HTTPBasic,
19
19
  )
20
20
  from typing_extensions import Annotated
21
21
  from typing import Union
22
+ from fastapi.security import HTTPBasicCredentials
22
23
 
23
24
  # Auth Schemes
24
25
  http_bearer = HTTPBearer(
@@ -33,6 +34,12 @@ apikey_header = APIKeyHeader(
33
34
  description="The API key to use for authentication.",
34
35
  )
35
36
 
37
+ basic_auth = HTTPBasic(
38
+ scheme_name="Basic",
39
+ auto_error=False,
40
+ description="The username and password to use for authentication. Only used in /admin/metrics",
41
+ )
42
+
36
43
 
37
44
  # Auth Handler
38
45
  class AuthHandler:
@@ -112,6 +119,7 @@ class AuthHandler:
112
119
  elif message == "jwt expired":
113
120
  error = ApiError.EXPIRED_BEARER
114
121
  else:
122
+ message = "Invalid bearer token"
115
123
  error = (
116
124
  ApiError.INVALID_BEARER
117
125
  ) # jwt malformed, jwt not active (https://www.npmjs.com/package/jsonwebtoken#errors--codes)
@@ -150,6 +158,7 @@ class AuthHandler:
150
158
  error=ApiError.NO_API_KEY,
151
159
  message="No credentials provided. API key is required",
152
160
  ),
161
+ headers={"WWW-Authenticate": "X-API-Key"},
153
162
  )
154
163
  raise e
155
164
 
@@ -175,6 +184,7 @@ class AuthHandler:
175
184
  error=ApiError.NO_BEARER,
176
185
  message="No credentials provided. Bearer token is required",
177
186
  ),
187
+ headers={"WWW-Authenticate": "Bearer"},
178
188
  )
179
189
  raise e
180
190
 
@@ -222,6 +232,7 @@ class AuthHandler:
222
232
  error=ApiError.NO_CREDENTIALS,
223
233
  message="No credentials provided. Either API key or bearer token is required.",
224
234
  ),
235
+ headers={"WWW-Authenticate": "Bearer, X-API-Key"},
225
236
  )
226
237
 
227
238
  async def ws_api_key_auth(
@@ -234,10 +245,7 @@ class AuthHandler:
234
245
  Use this function if you only want to allow access via the API key.
235
246
  This function is used for WebSocket connections.
236
247
  """
237
- try:
238
- return await self.api_key_auth(api_key=api_key, sec=sec)
239
- except HTTPException as e:
240
- raise WebSocketException.from_http_exception(e)
248
+ return await self.api_key_auth(api_key=api_key, sec=sec)
241
249
 
242
250
  async def ws_bearer_auth(
243
251
  self,
@@ -249,10 +257,8 @@ class AuthHandler:
249
257
  Use this function if you only want to allow access via the bearer token.
250
258
  This function is used for WebSocket connections.
251
259
  """
252
- try:
253
- return await self.bearer_auth(bearer=bearer, sec=sec)
254
- except HTTPException as e:
255
- raise WebSocketException.from_http_exception(e)
260
+ credentials = HTTPAuthorizationCredentials(scheme="Bearer", credentials=bearer)
261
+ return await self.bearer_auth(bearer=credentials, sec=sec)
256
262
 
257
263
  async def ws_combined_auth(
258
264
  self,
@@ -270,9 +276,34 @@ class AuthHandler:
270
276
  if bearer
271
277
  else None
272
278
  )
279
+ return await self.combined_auth(bearer=credentials, api_key=api_key, sec=sec)
280
+
281
+ async def basic_auth(
282
+ self,
283
+ request: Request,
284
+ credentials: Annotated[Union[HTTPBasicCredentials, None], Depends(basic_auth)],
285
+ ):
286
+ """
287
+ Verifies the basic authentication credentials. This authentication method should just be used for special cases like /admin/metrics, where JWT and API key authentication are not desired or not possible.
288
+ """
289
+ if not credentials:
290
+ raise HTTPException(
291
+ content=ExceptionContent(
292
+ error=ApiError.NO_CREDENTIALS,
293
+ message="No credentials provided. Basic authentication credentials are required.",
294
+ ),
295
+ )
296
+
273
297
  try:
274
- return await self.combined_auth(
275
- bearer=credentials, api_key=api_key, sec=sec
298
+ await self.client.login.verify_basic_auth_without_preload_content(
299
+ credentials.username, credentials.password
276
300
  )
277
- except HTTPException as e:
278
- raise WebSocketException.from_http_exception(e)
301
+ except ApiException as e:
302
+ raise HTTPException(
303
+ content=ExceptionContent(
304
+ error=ApiError.INVALID_BASIC_AUTH,
305
+ message="Invalid basic authentication credentials",
306
+ ),
307
+ headers={"WWW-Authenticate": "Basic"},
308
+ )
309
+ return credentials.username