crypticorn 2.7.4__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.
- crypticorn/__init__.py +8 -1
- crypticorn/auth/client/models/create_api_key_request.py +3 -1
- crypticorn/auth/client/models/get_api_keys200_response_inner.py +3 -1
- crypticorn/client.py +34 -16
- crypticorn/common/__init__.py +5 -1
- crypticorn/common/ansi_colors.py +37 -0
- crypticorn/common/auth.py +3 -1
- crypticorn/common/exceptions.py +29 -11
- crypticorn/common/logging.py +126 -0
- crypticorn/common/middleware.py +27 -0
- crypticorn/common/mixins.py +2 -1
- crypticorn/common/router/admin_router.py +100 -0
- crypticorn/common/router/status_router.py +24 -0
- crypticorn/common/scopes.py +5 -0
- crypticorn/hive/client/__init__.py +6 -3
- crypticorn/hive/client/api/__init__.py +1 -0
- crypticorn/hive/client/api/admin_api.py +1452 -0
- crypticorn/hive/client/api/models_api.py +7 -7
- crypticorn/hive/client/api/status_api.py +4 -231
- crypticorn/hive/client/models/__init__.py +5 -3
- crypticorn/hive/client/models/api_error_identifier.py +115 -0
- crypticorn/hive/client/models/api_error_level.py +37 -0
- crypticorn/hive/client/models/api_error_type.py +37 -0
- crypticorn/hive/client/models/data_info.py +27 -5
- crypticorn/hive/client/models/data_options.py +92 -0
- crypticorn/hive/client/models/exception_detail.py +6 -3
- crypticorn/hive/client/models/log_level.py +38 -0
- crypticorn/hive/client/models/model.py +3 -3
- crypticorn/hive/main.py +22 -3
- crypticorn/hive/utils.py +1 -2
- crypticorn/metrics/client/__init__.py +11 -0
- crypticorn/metrics/client/api/__init__.py +2 -0
- crypticorn/metrics/client/api/admin_api.py +1452 -0
- crypticorn/metrics/client/api/exchanges_api.py +51 -40
- crypticorn/metrics/client/api/indicators_api.py +49 -32
- crypticorn/metrics/client/api/logs_api.py +7 -7
- crypticorn/metrics/client/api/marketcap_api.py +28 -25
- crypticorn/metrics/client/api/markets_api.py +50 -278
- crypticorn/metrics/client/api/quote_currencies_api.py +289 -0
- crypticorn/metrics/client/api/status_api.py +4 -231
- crypticorn/metrics/client/api/tokens_api.py +241 -37
- crypticorn/metrics/client/models/__init__.py +9 -0
- crypticorn/metrics/client/models/api_error_identifier.py +115 -0
- crypticorn/metrics/client/models/api_error_level.py +37 -0
- crypticorn/metrics/client/models/api_error_type.py +37 -0
- crypticorn/metrics/client/models/exception_detail.py +6 -3
- crypticorn/metrics/client/models/exchange_mapping.py +121 -0
- crypticorn/metrics/client/models/internal_exchange.py +39 -0
- crypticorn/metrics/client/models/log_level.py +38 -0
- crypticorn/metrics/client/models/market_type.py +35 -0
- crypticorn/metrics/client/models/marketcap_ranking.py +87 -0
- crypticorn/metrics/client/models/ohlcv.py +113 -0
- crypticorn/metrics/main.py +14 -2
- crypticorn/pay/client/__init__.py +3 -0
- crypticorn/pay/client/api/__init__.py +1 -0
- crypticorn/pay/client/api/admin_api.py +1453 -0
- crypticorn/pay/client/api/status_api.py +4 -231
- crypticorn/pay/client/models/__init__.py +2 -0
- crypticorn/pay/client/models/log_level.py +38 -0
- crypticorn/{hive/client/models/data_value_value_value_inner.py → pay/client/models/response_getuptime.py} +36 -31
- crypticorn/pay/client/models/scope.py +2 -0
- crypticorn/pay/main.py +2 -0
- {crypticorn-2.7.4.dist-info → crypticorn-2.8.0.dist-info}/METADATA +46 -21
- {crypticorn-2.7.4.dist-info → crypticorn-2.8.0.dist-info}/RECORD +67 -44
- {crypticorn-2.7.4.dist-info → crypticorn-2.8.0.dist-info}/WHEEL +1 -1
- crypticorn/common/status_router.py +0 -44
- {crypticorn-2.7.4.dist-info → crypticorn-2.8.0.dist-info}/entry_points.txt +0 -0
- {crypticorn-2.7.4.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
|
-
|
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.
|
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.
|
43
|
+
self._services: dict[Service, SubClient] = {
|
44
44
|
service: client_class(self._get_default_config(service))
|
45
|
-
for service, client_class in self.
|
45
|
+
for service, client_class in self._service_classes.items()
|
46
46
|
}
|
47
47
|
|
48
48
|
@property
|
49
49
|
def hive(self) -> HiveClient:
|
50
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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.
|
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.
|
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.
|
134
|
+
self._services[service] = type(client)(new_config)
|
117
135
|
|
118
136
|
async def __aenter__(self):
|
119
137
|
return self
|
crypticorn/common/__init__.py
CHANGED
@@ -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.
|
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
|
|
crypticorn/common/exceptions.py
CHANGED
@@ -5,6 +5,10 @@ from fastapi import HTTPException as FastAPIHTTPException, Request, FastAPI
|
|
5
5
|
from fastapi.exceptions import RequestValidationError, ResponseValidationError
|
6
6
|
from fastapi.responses import JSONResponse
|
7
7
|
from crypticorn.common import ApiError, ApiErrorIdentifier, ApiErrorType, ApiErrorLevel
|
8
|
+
import logging
|
9
|
+
import json
|
10
|
+
|
11
|
+
logger = logging.getLogger("crypticorn")
|
8
12
|
|
9
13
|
|
10
14
|
class ExceptionType(StrEnum):
|
@@ -89,37 +93,51 @@ class WebSocketException(HTTPException):
|
|
89
93
|
)
|
90
94
|
|
91
95
|
|
92
|
-
async def general_handler(request: Request, exc: Exception):
|
96
|
+
async def general_handler(request: Request, exc: Exception) -> JSONResponse:
|
93
97
|
"""This is the default exception handler for all exceptions."""
|
94
98
|
body = ExceptionContent(message=str(exc), error=ApiError.UNKNOWN_ERROR)
|
95
|
-
|
96
|
-
status_code=body.enrich().status_code,
|
99
|
+
res = JSONResponse(
|
100
|
+
status_code=body.enrich().status_code,
|
101
|
+
content=HTTPException(content=body).detail,
|
97
102
|
)
|
103
|
+
logger.error(f"Response validation error: {json.loads(res.__dict__.get('body'))}")
|
104
|
+
return res
|
98
105
|
|
99
106
|
|
100
|
-
async def request_validation_handler(
|
107
|
+
async def request_validation_handler(
|
108
|
+
request: Request, exc: RequestValidationError
|
109
|
+
) -> JSONResponse:
|
101
110
|
"""This is the exception handler for all request validation errors."""
|
102
111
|
body = ExceptionContent(message=str(exc), error=ApiError.INVALID_DATA_REQUEST)
|
103
|
-
|
104
|
-
status_code=body.enrich().status_code,
|
112
|
+
res = JSONResponse(
|
113
|
+
status_code=body.enrich().status_code,
|
114
|
+
content=HTTPException(content=body).detail,
|
105
115
|
)
|
116
|
+
logger.error(f"Response validation error: {json.loads(res.__dict__.get('body'))}")
|
117
|
+
return res
|
106
118
|
|
107
119
|
|
108
|
-
async def response_validation_handler(
|
120
|
+
async def response_validation_handler(
|
121
|
+
request: Request, exc: ResponseValidationError
|
122
|
+
) -> JSONResponse:
|
109
123
|
"""This is the exception handler for all response validation errors."""
|
110
124
|
body = ExceptionContent(message=str(exc), error=ApiError.INVALID_DATA_RESPONSE)
|
111
|
-
|
112
|
-
status_code=body.enrich().status_code,
|
125
|
+
res = JSONResponse(
|
126
|
+
status_code=body.enrich().status_code,
|
127
|
+
content=HTTPException(content=body).detail,
|
113
128
|
)
|
129
|
+
logger.error(f"Response validation error: {json.loads(res.__dict__.get('body'))}")
|
130
|
+
return res
|
114
131
|
|
115
132
|
|
116
|
-
async def http_handler(request: Request, exc: HTTPException):
|
133
|
+
async def http_handler(request: Request, exc: HTTPException) -> JSONResponse:
|
117
134
|
"""This is the exception handler for HTTPExceptions. It unwraps the HTTPException and returns the detail in a flat JSON response."""
|
135
|
+
logger.error(f"HTTP error: {exc.detail}")
|
118
136
|
return JSONResponse(status_code=exc.status_code, content=exc.detail)
|
119
137
|
|
120
138
|
|
121
139
|
def register_exception_handlers(app: FastAPI):
|
122
|
-
"""Utility to register serveral exception handlers in one go. Catches Exception, HTTPException and Data Validation errors and responds with a unified json body."""
|
140
|
+
"""Utility to register serveral exception handlers in one go. Catches Exception, HTTPException and Data Validation errors, logs them and responds with a unified json body."""
|
123
141
|
app.add_exception_handler(Exception, general_handler)
|
124
142
|
app.add_exception_handler(FastAPIHTTPException, http_handler)
|
125
143
|
app.add_exception_handler(RequestValidationError, request_validation_handler)
|
@@ -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
|
crypticorn/common/mixins.py
CHANGED
@@ -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()))
|
crypticorn/common/scopes.py
CHANGED
@@ -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.
|
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
|