crypticorn 2.7.5__py3-none-any.whl → 2.8.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 (68) hide show
  1. crypticorn/__init__.py +8 -1
  2. crypticorn/auth/client/models/create_api_key_request.py +3 -1
  3. crypticorn/auth/client/models/get_api_keys200_response_inner.py +3 -1
  4. crypticorn/client.py +34 -16
  5. crypticorn/common/__init__.py +5 -1
  6. crypticorn/common/ansi_colors.py +37 -0
  7. crypticorn/common/auth.py +3 -1
  8. crypticorn/common/exceptions.py +29 -17
  9. crypticorn/common/logging.py +126 -0
  10. crypticorn/common/middleware.py +27 -0
  11. crypticorn/common/mixins.py +2 -1
  12. crypticorn/common/router/admin_router.py +100 -0
  13. crypticorn/common/router/status_router.py +24 -0
  14. crypticorn/common/scopes.py +5 -0
  15. crypticorn/hive/client/__init__.py +6 -3
  16. crypticorn/hive/client/api/__init__.py +1 -0
  17. crypticorn/hive/client/api/admin_api.py +1452 -0
  18. crypticorn/hive/client/api/models_api.py +7 -7
  19. crypticorn/hive/client/api/status_api.py +4 -231
  20. crypticorn/hive/client/models/__init__.py +5 -3
  21. crypticorn/hive/client/models/api_error_identifier.py +115 -0
  22. crypticorn/hive/client/models/api_error_level.py +37 -0
  23. crypticorn/hive/client/models/api_error_type.py +37 -0
  24. crypticorn/hive/client/models/data_info.py +27 -5
  25. crypticorn/hive/client/models/data_options.py +92 -0
  26. crypticorn/hive/client/models/exception_detail.py +6 -3
  27. crypticorn/hive/client/models/log_level.py +38 -0
  28. crypticorn/hive/client/models/model.py +3 -3
  29. crypticorn/hive/main.py +22 -3
  30. crypticorn/hive/utils.py +1 -2
  31. crypticorn/metrics/client/__init__.py +11 -0
  32. crypticorn/metrics/client/api/__init__.py +2 -0
  33. crypticorn/metrics/client/api/admin_api.py +1452 -0
  34. crypticorn/metrics/client/api/exchanges_api.py +51 -40
  35. crypticorn/metrics/client/api/indicators_api.py +49 -32
  36. crypticorn/metrics/client/api/logs_api.py +7 -7
  37. crypticorn/metrics/client/api/marketcap_api.py +28 -25
  38. crypticorn/metrics/client/api/markets_api.py +50 -278
  39. crypticorn/metrics/client/api/quote_currencies_api.py +289 -0
  40. crypticorn/metrics/client/api/status_api.py +4 -231
  41. crypticorn/metrics/client/api/tokens_api.py +241 -37
  42. crypticorn/metrics/client/models/__init__.py +9 -0
  43. crypticorn/metrics/client/models/api_error_identifier.py +115 -0
  44. crypticorn/metrics/client/models/api_error_level.py +37 -0
  45. crypticorn/metrics/client/models/api_error_type.py +37 -0
  46. crypticorn/metrics/client/models/exception_detail.py +6 -3
  47. crypticorn/metrics/client/models/exchange_mapping.py +121 -0
  48. crypticorn/metrics/client/models/internal_exchange.py +39 -0
  49. crypticorn/metrics/client/models/log_level.py +38 -0
  50. crypticorn/metrics/client/models/market_type.py +35 -0
  51. crypticorn/metrics/client/models/marketcap_ranking.py +87 -0
  52. crypticorn/metrics/client/models/ohlcv.py +113 -0
  53. crypticorn/metrics/main.py +14 -2
  54. crypticorn/pay/client/__init__.py +3 -0
  55. crypticorn/pay/client/api/__init__.py +1 -0
  56. crypticorn/pay/client/api/admin_api.py +1453 -0
  57. crypticorn/pay/client/api/status_api.py +4 -231
  58. crypticorn/pay/client/models/__init__.py +2 -0
  59. crypticorn/pay/client/models/log_level.py +38 -0
  60. crypticorn/{hive/client/models/data_value_value_value_inner.py → pay/client/models/response_getuptime.py} +36 -31
  61. crypticorn/pay/client/models/scope.py +2 -0
  62. crypticorn/pay/main.py +2 -0
  63. {crypticorn-2.7.5.dist-info → crypticorn-2.8.0.dist-info}/METADATA +46 -21
  64. {crypticorn-2.7.5.dist-info → crypticorn-2.8.0.dist-info}/RECORD +67 -44
  65. crypticorn/common/status_router.py +0 -44
  66. {crypticorn-2.7.5.dist-info → crypticorn-2.8.0.dist-info}/WHEEL +0 -0
  67. {crypticorn-2.7.5.dist-info → crypticorn-2.8.0.dist-info}/entry_points.txt +0 -0
  68. {crypticorn-2.7.5.dist-info → crypticorn-2.8.0.dist-info}/top_level.txt +0 -0
crypticorn/__init__.py CHANGED
@@ -1,8 +1,15 @@
1
1
  """
2
2
  .. include:: ../README.md
3
- .. include:: ../CHANGELOG.md
3
+
4
+ ## Versioning
5
+ We adhere to [Semantic Versioning](https://semver.org/).
6
+ You can find the full Changelog [below](#changelog).
4
7
  """
5
8
 
9
+ from crypticorn.common.logging import configure_logging
10
+
11
+ configure_logging()
12
+
6
13
  from crypticorn.client import ApiClient
7
14
 
8
15
  __all__ = ["ApiClient"]
@@ -78,10 +78,12 @@ class CreateApiKeyRequest(BaseModel):
78
78
  "read:metrics:tokens",
79
79
  "read:metrics:markets",
80
80
  "read:sentiment",
81
+ "read:admin",
82
+ "write:admin",
81
83
  ]
82
84
  ):
83
85
  raise ValueError(
84
- "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')"
86
+ "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:admin', 'write:admin')"
85
87
  )
86
88
  return value
87
89
 
@@ -86,10 +86,12 @@ class GetApiKeys200ResponseInner(BaseModel):
86
86
  "read:metrics:tokens",
87
87
  "read:metrics:markets",
88
88
  "read:sentiment",
89
+ "read:admin",
90
+ "write:admin",
89
91
  ]
90
92
  ):
91
93
  raise ValueError(
92
- "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')"
94
+ "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:admin', 'write:admin')"
93
95
  )
94
96
  return value
95
97
 
crypticorn/client.py CHANGED
@@ -13,7 +13,7 @@ SubClient = TypeVar("SubClient")
13
13
 
14
14
  class ApiClient:
15
15
  """
16
- The official client for interacting with the Crypticorn API.
16
+ The official Python client for interacting with the Crypticorn API.
17
17
 
18
18
  It is consisting of multiple microservices covering the whole stack of the Crypticorn project.
19
19
  """
@@ -27,11 +27,11 @@ class ApiClient:
27
27
  self.base_url = base_url
28
28
  """The base URL the client will use to connect to the API."""
29
29
  self.api_key = api_key
30
- """The API key to use for authentication."""
30
+ """The API key to use for authentication (recommended)."""
31
31
  self.jwt = jwt
32
- """The JWT to use for authentication."""
32
+ """The JWT to use for authentication (not recommended)."""
33
33
 
34
- self.service_classes: dict[Service, type[SubClient]] = {
34
+ self._service_classes: dict[Service, type[SubClient]] = {
35
35
  Service.HIVE: HiveClient,
36
36
  Service.TRADE: TradeClient,
37
37
  Service.KLINES: KlinesClient,
@@ -40,38 +40,56 @@ class ApiClient:
40
40
  Service.AUTH: AuthClient,
41
41
  }
42
42
 
43
- self.services: dict[Service, SubClient] = {
43
+ self._services: dict[Service, SubClient] = {
44
44
  service: client_class(self._get_default_config(service))
45
- for service, client_class in self.service_classes.items()
45
+ for service, client_class in self._service_classes.items()
46
46
  }
47
47
 
48
48
  @property
49
49
  def hive(self) -> HiveClient:
50
- return self.services[Service.HIVE]
50
+ """
51
+ Entry point for the Hive AI API ([Docs](https://docs.crypticorn.com/api/?api=hive-ai-api)).
52
+ """
53
+ return self._services[Service.HIVE]
51
54
 
52
55
  @property
53
56
  def trade(self) -> TradeClient:
54
- return self.services[Service.TRADE]
57
+ """
58
+ Entry point for the Trading API ([Docs](https://docs.crypticorn.com/api/?api=trading-api)).
59
+ """
60
+ return self._services[Service.TRADE]
55
61
 
56
62
  @property
57
63
  def klines(self) -> KlinesClient:
58
- return self.services[Service.KLINES]
64
+ """
65
+ Entry point for the Klines API ([Docs](https://docs.crypticorn.com/api/?api=klines-api)).
66
+ """
67
+ return self._services[Service.KLINES]
59
68
 
60
69
  @property
61
70
  def metrics(self) -> MetricsClient:
62
- return self.services[Service.METRICS]
71
+ """
72
+ Entry point for the Metrics API ([Docs](https://docs.crypticorn.com/api/?api=metrics-api)).
73
+ """
74
+ return self._services[Service.METRICS]
63
75
 
64
76
  @property
65
77
  def pay(self) -> PayClient:
66
- return self.services[Service.PAY]
78
+ """
79
+ Entry point for the Payment API ([Docs](https://docs.crypticorn.com/api/?api=payment-api)).
80
+ """
81
+ return self._services[Service.PAY]
67
82
 
68
83
  @property
69
84
  def auth(self) -> AuthClient:
70
- return self.services[Service.AUTH]
85
+ """
86
+ Entry point for the Auth API ([Docs](https://docs.crypticorn.com/api/?api=auth-api)).
87
+ """
88
+ return self._services[Service.AUTH]
71
89
 
72
90
  async def close(self):
73
91
  """Close all client sessions."""
74
- for service in self.services.values():
92
+ for service in self._services.values():
75
93
  if hasattr(service.base_client, "close"):
76
94
  await service.base_client.close()
77
95
 
@@ -81,7 +99,7 @@ class ApiClient:
81
99
  """
82
100
  Get the default configuration for a given service.
83
101
  """
84
- config_class = self.service_classes[service].config_class
102
+ config_class = self._service_classes[service].config_class
85
103
  return config_class(
86
104
  host=f"{self.base_url}/{version}/{service}",
87
105
  access_token=self.jwt,
@@ -105,7 +123,7 @@ class ApiClient:
105
123
  >>> client.configure(config=HiveConfig(host="http://localhost:8000"), client=client.hive)
106
124
  """
107
125
  assert Service.validate(service), f"Invalid service: {service}"
108
- client = self.services[service]
126
+ client = self._services[service]
109
127
  new_config = client.config
110
128
 
111
129
  for attr in vars(config):
@@ -113,7 +131,7 @@ class ApiClient:
113
131
  if new_value:
114
132
  setattr(new_config, attr, new_value)
115
133
 
116
- self.services[service] = type(client)(new_config)
134
+ self._services[service] = type(client)(new_config)
117
135
 
118
136
  async def __aenter__(self):
119
137
  return self
@@ -8,4 +8,8 @@ from crypticorn.common.enums import *
8
8
  from crypticorn.common.utils import *
9
9
  from crypticorn.common.exceptions import *
10
10
  from crypticorn.common.pagination import *
11
- from crypticorn.common.status_router import router as status_router
11
+ from crypticorn.common.logging import *
12
+ from crypticorn.common.ansi_colors import *
13
+ from crypticorn.common.middleware import *
14
+ from crypticorn.common.router.status_router import router as status_router
15
+ from crypticorn.common.router.admin_router import router as admin_router
@@ -0,0 +1,37 @@
1
+ from enum import StrEnum
2
+ from typing import TYPE_CHECKING
3
+
4
+
5
+ class AnsiColors(StrEnum):
6
+ # Regular Text Colors
7
+ BLACK = "\033[30m" # black
8
+ RED = "\033[31m" # red
9
+ GREEN = "\033[32m" # green
10
+ YELLOW = "\033[33m" # yellow
11
+ BLUE = "\033[34m" # blue
12
+ MAGENTA = "\033[35m" # magenta
13
+ CYAN = "\033[36m" # cyan
14
+ WHITE = "\033[37m" # white
15
+
16
+ # Bright Text Colors
17
+ BLACK_BRIGHT = "\033[90m" # black_bright
18
+ RED_BRIGHT = "\033[91m" # red_bright
19
+ GREEN_BRIGHT = "\033[92m" # green_bright
20
+ YELLOW_BRIGHT = "\033[93m" # yellow_bright
21
+ BLUE_BRIGHT = "\033[94m" # blue_bright
22
+ MAGENTA_BRIGHT = "\033[95m" # magenta_bright
23
+ CYAN_BRIGHT = "\033[96m" # cyan_bright
24
+ WHITE_BRIGHT = "\033[97m" # white_bright
25
+
26
+ # Bold Text Colors
27
+ BLACK_BOLD = "\033[1;30m" # black_bold
28
+ RED_BOLD = "\033[1;31m" # red_bold
29
+ GREEN_BOLD = "\033[1;32m" # green_bold
30
+ YELLOW_BOLD = "\033[1;33m" # yellow_bold
31
+ BLUE_BOLD = "\033[1;34m" # blue_bold
32
+ MAGENTA_BOLD = "\033[1;35m" # magenta_bold
33
+ CYAN_BOLD = "\033[1;36m" # cyan_bold
34
+ WHITE_BOLD = "\033[1;37m" # white_bold
35
+
36
+ # Reset Color
37
+ RESET = "\033[0m"
crypticorn/common/auth.py CHANGED
@@ -75,7 +75,9 @@ class AuthHandler:
75
75
  raise HTTPException(
76
76
  content=ExceptionContent(
77
77
  error=ApiError.INSUFFICIENT_SCOPES,
78
- message="Insufficient scopes to access this resource",
78
+ message="Insufficient scopes to access this resource (required: "
79
+ + ", ".join(api_scopes)
80
+ + ")",
79
81
  ),
80
82
  )
81
83
 
@@ -6,8 +6,10 @@ from fastapi.exceptions import RequestValidationError, ResponseValidationError
6
6
  from fastapi.responses import JSONResponse
7
7
  from crypticorn.common import ApiError, ApiErrorIdentifier, ApiErrorType, ApiErrorLevel
8
8
  import logging
9
+ import json
10
+
11
+ logger = logging.getLogger("crypticorn")
9
12
 
10
- logger = logging.getLogger(__name__)
11
13
 
12
14
  class ExceptionType(StrEnum):
13
15
  HTTP = "http"
@@ -91,34 +93,44 @@ class WebSocketException(HTTPException):
91
93
  )
92
94
 
93
95
 
94
- async def general_handler(request: Request, exc: Exception):
96
+ async def general_handler(request: Request, exc: Exception) -> JSONResponse:
95
97
  """This is the default exception handler for all exceptions."""
96
- body = ExceptionContent(message=str(exc), error=ApiError.UNKNOWN_ERROR).enrich()
97
- logger.error(f"Unknown error: {body.detail}")
98
- return JSONResponse(
99
- status_code=body.status_code, content=HTTPException(content=body).detail
98
+ body = ExceptionContent(message=str(exc), error=ApiError.UNKNOWN_ERROR)
99
+ res = JSONResponse(
100
+ status_code=body.enrich().status_code,
101
+ content=HTTPException(content=body).detail,
100
102
  )
103
+ logger.error(f"Response validation error: {json.loads(res.__dict__.get('body'))}")
104
+ return res
101
105
 
102
106
 
103
- async def request_validation_handler(request: Request, exc: RequestValidationError):
107
+ async def request_validation_handler(
108
+ request: Request, exc: RequestValidationError
109
+ ) -> JSONResponse:
104
110
  """This is the exception handler for all request validation errors."""
105
- body = ExceptionContent(message=str(exc), error=ApiError.INVALID_DATA_REQUEST).enrich()
106
- logger.error(f"Request validation error: {body.detail}")
107
- return JSONResponse(
108
- status_code=body.status_code, content=HTTPException(content=body).detail
111
+ body = ExceptionContent(message=str(exc), error=ApiError.INVALID_DATA_REQUEST)
112
+ res = JSONResponse(
113
+ status_code=body.enrich().status_code,
114
+ content=HTTPException(content=body).detail,
109
115
  )
116
+ logger.error(f"Response validation error: {json.loads(res.__dict__.get('body'))}")
117
+ return res
110
118
 
111
119
 
112
- async def response_validation_handler(request: Request, exc: ResponseValidationError):
120
+ async def response_validation_handler(
121
+ request: Request, exc: ResponseValidationError
122
+ ) -> JSONResponse:
113
123
  """This is the exception handler for all response validation errors."""
114
- body = ExceptionContent(message=str(exc), error=ApiError.INVALID_DATA_RESPONSE).enrich()
115
- logger.error(f"Response validation error: {body.detail}")
116
- return JSONResponse(
117
- status_code=body.status_code, content=HTTPException(content=body).detail
124
+ body = ExceptionContent(message=str(exc), error=ApiError.INVALID_DATA_RESPONSE)
125
+ res = JSONResponse(
126
+ status_code=body.enrich().status_code,
127
+ content=HTTPException(content=body).detail,
118
128
  )
129
+ logger.error(f"Response validation error: {json.loads(res.__dict__.get('body'))}")
130
+ return res
119
131
 
120
132
 
121
- async def http_handler(request: Request, exc: HTTPException):
133
+ async def http_handler(request: Request, exc: HTTPException) -> JSONResponse:
122
134
  """This is the exception handler for HTTPExceptions. It unwraps the HTTPException and returns the detail in a flat JSON response."""
123
135
  logger.error(f"HTTP error: {exc.detail}")
124
136
  return JSONResponse(status_code=exc.status_code, content=exc.detail)
@@ -0,0 +1,126 @@
1
+ from __future__ import annotations
2
+
3
+ # shared_logger.py
4
+ import logging
5
+ import sys
6
+ from contextvars import ContextVar
7
+ from contextlib import asynccontextmanager
8
+ import json
9
+ from pydantic import BaseModel
10
+ from enum import StrEnum
11
+ from crypticorn.common.mixins import ValidateEnumMixin
12
+ from crypticorn.common.ansi_colors import AnsiColors as C
13
+ from datetime import datetime
14
+ import os
15
+
16
+
17
+ class LogLevel(ValidateEnumMixin, StrEnum):
18
+ DEBUG = "DEBUG"
19
+ INFO = "INFO"
20
+ WARNING = "WARNING"
21
+ ERROR = "ERROR"
22
+ CRITICAL = "CRITICAL"
23
+
24
+ @classmethod
25
+ def get_color(cls, level: str) -> str:
26
+ if level == cls.DEBUG:
27
+ return C.GREEN_BRIGHT
28
+ elif level == cls.INFO:
29
+ return C.BLUE_BRIGHT
30
+ elif level == cls.WARNING:
31
+ return C.YELLOW_BRIGHT
32
+ elif level == cls.ERROR:
33
+ return C.RED_BRIGHT
34
+ elif level == cls.CRITICAL:
35
+ return C.RED_BOLD
36
+ else:
37
+ return C.RESET
38
+
39
+ @staticmethod
40
+ def get_level(level: "LogLevel") -> int:
41
+ return logging._nameToLevel.get(level, logging.INFO)
42
+
43
+ @staticmethod
44
+ def get_name(level: int) -> "LogLevel":
45
+ return LogLevel(logging._levelToName.get(level, "INFO"))
46
+
47
+
48
+ _LOGFORMAT = (
49
+ f"{C.CYAN_BOLD}%(asctime)s{C.RESET} - "
50
+ f"{C.GREEN_BOLD}%(name)s{C.RESET} - "
51
+ f"%(levelcolor)s%(levelname)s{C.RESET} - "
52
+ f"%(message)s"
53
+ )
54
+ _DATEFMT = "%Y-%m-%d %H:%M:%S.%f:"
55
+
56
+
57
+ class CustomFormatter(logging.Formatter):
58
+ def __init__(self, *args, **kwargs):
59
+ super().__init__(*args, **kwargs)
60
+
61
+ def format(self, record):
62
+ color = LogLevel.get_color(record.levelname)
63
+ record.levelcolor = color
64
+ return super().format(record)
65
+
66
+ def formatTime(self, record, datefmt=_DATEFMT):
67
+ dt = datetime.fromtimestamp(record.created)
68
+ s = dt.strftime(datefmt)
69
+ return s[:-3] # Trim last 3 digits to get milliseconds
70
+
71
+
72
+ def configure_logging(
73
+ name: str = None,
74
+ fmt: str = _LOGFORMAT,
75
+ datefmt: str = _DATEFMT,
76
+ stdout_level: int = logging.INFO,
77
+ file_level: int = logging.INFO,
78
+ log_file: str = None,
79
+ filters: list[logging.Filter] = [],
80
+ ) -> None:
81
+ """Configures the logging for the application.
82
+ Run this function as early as possible in the application (for example using the `lifespan` parameter in FastAPI).
83
+ Then use can use the default `logging.getLogger(__name__)` method to get the logger (or <name> if you set the name parameter).
84
+ :param name: The name of the logger. If not provided, the root logger will be used. Use a name if you use multiple loggers in the same application.
85
+ :param fmt: The format of the log message.
86
+ :param datefmt: The date format of the log message.
87
+ :param stdout_level: The level of the log message to be printed to the console.
88
+ :param file_level: The level of the log message to be written to the file. Only used if `log_file` is provided.
89
+ :param log_file: The file to write the log messages to.
90
+ :param filters: A list of filters to apply to the log handlers.
91
+ """
92
+ logger = logging.getLogger(name) if name else logging.getLogger()
93
+
94
+ if logger.handlers: # clear existing handlers to avoid duplicates
95
+ logger.handlers.clear()
96
+
97
+ logger.setLevel(min(stdout_level, file_level)) # set to most verbose level
98
+
99
+ # Configure stdout handler
100
+ stdout_handler = logging.StreamHandler(sys.stdout)
101
+ stdout_handler.setLevel(stdout_level)
102
+ stdout_handler.setFormatter(CustomFormatter(fmt=fmt, datefmt=datefmt))
103
+ for filter in filters:
104
+ stdout_handler.addFilter(filter)
105
+ logger.addHandler(stdout_handler)
106
+
107
+ # Configure file handler
108
+ if log_file:
109
+ os.makedirs(os.path.dirname(log_file), exist_ok=True)
110
+ file_handler = logging.handlers.RotatingFileHandler(
111
+ log_file, maxBytes=10 * 1024 * 1024, backupCount=5
112
+ )
113
+ file_handler.setLevel(file_level)
114
+ file_handler.setFormatter(CustomFormatter(fmt=fmt, datefmt=datefmt))
115
+ for filter in filters:
116
+ file_handler.addFilter(filter)
117
+ logger.addHandler(file_handler)
118
+
119
+ if name:
120
+ logger.propagate = False
121
+
122
+
123
+ def disable_logging():
124
+ """Disable logging for the crypticorn logger."""
125
+ logger = logging.getLogger("crypticorn")
126
+ logger.disabled = True
@@ -0,0 +1,27 @@
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from crypticorn.common.logging import configure_logging
4
+ import logging
5
+
6
+
7
+ def add_cors_middleware(app: "FastAPI"):
8
+ app.add_middleware(
9
+ CORSMiddleware,
10
+ allow_origins=[
11
+ "http://localhost:5173", # vite dev server
12
+ "http://localhost:4173", # vite preview server
13
+ ],
14
+ allow_origin_regex="^https://([a-zA-Z0-9-]+.)*crypticorn.(dev|com)/?$", # matches (multiple or no) subdomains of crypticorn.dev and crypticorn.com
15
+ allow_credentials=True,
16
+ allow_methods=["*"],
17
+ allow_headers=["*"],
18
+ )
19
+
20
+
21
+ async def default_lifespan(app: FastAPI):
22
+ """Default lifespan for the applications.
23
+ This is used to configure the logging for the application.
24
+ To override this, pass a different lifespan to the FastAPI constructor or call this lifespan within a custom lifespan.
25
+ """
26
+ configure_logging()
27
+ yield
@@ -33,13 +33,14 @@ class ValidateEnumMixin:
33
33
  return False
34
34
 
35
35
 
36
+ # This Mixin will be removed in a future version. And has no effect from now on
36
37
  class ExcludeEnumMixin:
37
38
  """Mixin to exclude enum from OpenAPI schema. We use this to avoid duplicating enums when generating client code from the openapi spec."""
38
39
 
39
40
  @classmethod
40
41
  def __get_pydantic_json_schema__(cls, core_schema, handler):
41
42
  schema = handler(core_schema)
42
- schema.pop("enum", None)
43
+ # schema.pop("enum", None)
43
44
  return schema
44
45
 
45
46
 
@@ -0,0 +1,100 @@
1
+ """
2
+ This module contains the admin router for the API.
3
+ It provides endpoints for monitoring the server and getting information about the environment.
4
+ ONLY ALLOW ACCESS TO THIS ROUTER WITH ADMIN SCOPES.
5
+ >>> app.include_router(admin_router, dependencies=[Security(auth_handler.combined_auth, scopes=[Scope.READ_ADMIN, Scope.WRITE_ADMIN])])
6
+ """
7
+
8
+ import os
9
+ import pkg_resources
10
+ import threading
11
+ import time
12
+ import psutil
13
+ from fastapi import APIRouter, Query
14
+ from typing import Literal, Union
15
+ from crypticorn.common.logging import LogLevel
16
+ import logging
17
+
18
+ router = APIRouter(tags=["Admin"], prefix="/admin")
19
+
20
+ START_TIME = time.time()
21
+
22
+
23
+ @router.get("/log-level", status_code=200, operation_id="getLogLevel", deprecated=True)
24
+ async def get_logging_level() -> LogLevel:
25
+ """
26
+ Get the log level of the server logger. Will be removed in a future release.
27
+ """
28
+ return LogLevel.get_name(logging.getLogger().level)
29
+
30
+
31
+ @router.get("/uptime", operation_id="getUptime", status_code=200)
32
+ def get_uptime(type: Literal["seconds", "human"] = "seconds") -> str:
33
+ """Return the server uptime in seconds or human-readable form."""
34
+ uptime_seconds = int(time.time() - START_TIME)
35
+ if type == "seconds":
36
+ return str(uptime_seconds)
37
+ elif type == "human":
38
+ return time.strftime("%H:%M:%S", time.gmtime(uptime_seconds))
39
+
40
+
41
+ @router.get("/memory", operation_id="getMemoryUsage", status_code=200)
42
+ def get_memory_usage() -> float:
43
+ """
44
+ Resident Set Size (RSS) in MB — the actual memory used by the process in RAM.
45
+ Represents the physical memory footprint. Important for monitoring real usage.
46
+ """
47
+ process = psutil.Process(os.getpid())
48
+ mem_info = process.memory_info()
49
+ return round(mem_info.rss / (1024 * 1024), 2)
50
+
51
+
52
+ @router.get("/threads", operation_id="getThreads", status_code=200)
53
+ def get_threads() -> dict:
54
+ """Return count and names of active threads."""
55
+ threads = threading.enumerate()
56
+ return {
57
+ "count": len(threads),
58
+ "threads": [t.name for t in threads],
59
+ }
60
+
61
+
62
+ @router.get("/limits", operation_id="getContainerLimits", status_code=200)
63
+ def get_container_limits() -> dict:
64
+ """Return container resource limits from cgroup."""
65
+ limits = {}
66
+ try:
67
+ with open("/sys/fs/cgroup/memory/memory.limit_in_bytes") as f:
68
+ limits["memory_limit_MB"] = int(f.read().strip()) / 1024 / 1024
69
+ except Exception:
70
+ limits["memory_limit_MB"] = "N/A"
71
+
72
+ try:
73
+ with open("/sys/fs/cgroup/cpu/cpu.cfs_quota_us") as f1, open(
74
+ "/sys/fs/cgroup/cpu/cpu.cfs_period_us"
75
+ ) as f2:
76
+ quota = int(f1.read().strip())
77
+ period = int(f2.read().strip())
78
+ limits["cpu_limit_cores"] = quota / period if quota > 0 else "N/A"
79
+ except Exception:
80
+ limits["cpu_limit_cores"] = "N/A"
81
+
82
+ return limits
83
+
84
+
85
+ @router.get("/dependencies", operation_id="getDependencies", status_code=200)
86
+ def list_installed_packages(
87
+ include: list[str] = Query(
88
+ default=None,
89
+ description="List of dependencies to include in the response. If not provided, all installed packages will be returned.",
90
+ )
91
+ ) -> list:
92
+ """Return a list of installed packages and versions."""
93
+ return sorted(
94
+ [
95
+ {dist.project_name: dist.version}
96
+ for dist in pkg_resources.working_set
97
+ if include is None or dist.project_name in include
98
+ ],
99
+ key=lambda x: next(iter(x)),
100
+ )
@@ -0,0 +1,24 @@
1
+ from datetime import datetime
2
+ from typing import Literal
3
+ from fastapi import APIRouter, Request
4
+
5
+ router = APIRouter(tags=["Status"], prefix="")
6
+
7
+
8
+ @router.get("/", operation_id="ping")
9
+ async def ping(request: Request) -> dict:
10
+ """
11
+ Returns 'OK' if the API is running.
12
+ """
13
+ return {"origin": request.headers.get("x-forwarded-for")}
14
+
15
+
16
+ @router.get("/time", operation_id="getTime")
17
+ async def time(type: Literal["iso", "unix"] = "iso") -> str:
18
+ """
19
+ Returns the current time in either ISO or Unix timestamp (seconds) format.
20
+ """
21
+ if type == "iso":
22
+ return datetime.now().isoformat()
23
+ elif type == "unix":
24
+ return str(int(datetime.now().timestamp()))
@@ -8,6 +8,9 @@ class Scope(StrEnum):
8
8
 
9
9
  # If you update anything here, also update the scopes in the auth-service repository
10
10
 
11
+ WRITE_ADMIN = "write:admin"
12
+ READ_ADMIN = "read:admin"
13
+
11
14
  # Scopes that can be purchased - these actually exist in the jwt token
12
15
  READ_PREDICTIONS = "read:predictions"
13
16
 
@@ -55,6 +58,8 @@ class Scope(StrEnum):
55
58
  return [
56
59
  cls.WRITE_TRADE_STRATEGIES,
57
60
  cls.WRITE_PAY_PRODUCTS,
61
+ cls.WRITE_ADMIN,
62
+ cls.READ_ADMIN,
58
63
  ]
59
64
 
60
65
  @classmethod
@@ -17,6 +17,7 @@ Do not edit the class manually.
17
17
  __version__ = "1.0.0"
18
18
 
19
19
  # import apis into sdk package
20
+ from crypticorn.hive.client.api.admin_api import AdminApi
20
21
  from crypticorn.hive.client.api.data_api import DataApi
21
22
  from crypticorn.hive.client.api.models_api import ModelsApi
22
23
  from crypticorn.hive.client.api.status_api import StatusApi
@@ -33,12 +34,13 @@ from crypticorn.hive.client.exceptions import ApiAttributeError
33
34
  from crypticorn.hive.client.exceptions import ApiException
34
35
 
35
36
  # import models into sdk package
37
+ from crypticorn.hive.client.models.api_error_identifier import ApiErrorIdentifier
38
+ from crypticorn.hive.client.models.api_error_level import ApiErrorLevel
39
+ from crypticorn.hive.client.models.api_error_type import ApiErrorType
36
40
  from crypticorn.hive.client.models.coins import Coins
37
41
  from crypticorn.hive.client.models.data_download_response import DataDownloadResponse
38
42
  from crypticorn.hive.client.models.data_info import DataInfo
39
- from crypticorn.hive.client.models.data_value_value_value_inner import (
40
- DataValueValueValueInner,
41
- )
43
+ from crypticorn.hive.client.models.data_options import DataOptions
42
44
  from crypticorn.hive.client.models.data_version import DataVersion
43
45
  from crypticorn.hive.client.models.data_version_info import DataVersionInfo
44
46
  from crypticorn.hive.client.models.download_links import DownloadLinks
@@ -46,6 +48,7 @@ from crypticorn.hive.client.models.evaluation import Evaluation
46
48
  from crypticorn.hive.client.models.evaluation_response import EvaluationResponse
47
49
  from crypticorn.hive.client.models.exception_detail import ExceptionDetail
48
50
  from crypticorn.hive.client.models.feature_size import FeatureSize
51
+ from crypticorn.hive.client.models.log_level import LogLevel
49
52
  from crypticorn.hive.client.models.model import Model
50
53
  from crypticorn.hive.client.models.model_create import ModelCreate
51
54
  from crypticorn.hive.client.models.model_status import ModelStatus
@@ -1,6 +1,7 @@
1
1
  # flake8: noqa
2
2
 
3
3
  # import apis into api package
4
+ from crypticorn.hive.client.api.admin_api import AdminApi
4
5
  from crypticorn.hive.client.api.data_api import DataApi
5
6
  from crypticorn.hive.client.api.models_api import ModelsApi
6
7
  from crypticorn.hive.client.api.status_api import StatusApi