crypticorn 2.7.5__py3-none-any.whl → 2.8.0rc1__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.
@@ -8,4 +8,7 @@ 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.router.status_router import router as status_router
14
+ from crypticorn.common.router.admin_router import router as admin_router
@@ -0,0 +1,40 @@
1
+ from enum import StrEnum
2
+ from typing import TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ pass
6
+
7
+
8
+ class AnsiColors(StrEnum):
9
+ # Regular Text Colors
10
+ BLACK = "\033[30m" # black
11
+ RED = "\033[31m" # red
12
+ GREEN = "\033[32m" # green
13
+ YELLOW = "\033[33m" # yellow
14
+ BLUE = "\033[34m" # blue
15
+ MAGENTA = "\033[35m" # magenta
16
+ CYAN = "\033[36m" # cyan
17
+ WHITE = "\033[37m" # white
18
+
19
+ # Bright Text Colors
20
+ BLACK_BRIGHT = "\033[90m" # black_bright
21
+ RED_BRIGHT = "\033[91m" # red_bright
22
+ GREEN_BRIGHT = "\033[92m" # green_bright
23
+ YELLOW_BRIGHT = "\033[93m" # yellow_bright
24
+ BLUE_BRIGHT = "\033[94m" # blue_bright
25
+ MAGENTA_BRIGHT = "\033[95m" # magenta_bright
26
+ CYAN_BRIGHT = "\033[96m" # cyan_bright
27
+ WHITE_BRIGHT = "\033[97m" # white_bright
28
+
29
+ # Bold Text Colors
30
+ BLACK_BOLD = "\033[1;30m" # black_bold
31
+ RED_BOLD = "\033[1;31m" # red_bold
32
+ GREEN_BOLD = "\033[1;32m" # green_bold
33
+ YELLOW_BOLD = "\033[1;33m" # yellow_bold
34
+ BLUE_BOLD = "\033[1;34m" # blue_bold
35
+ MAGENTA_BOLD = "\033[1;35m" # magenta_bold
36
+ CYAN_BOLD = "\033[1;36m" # cyan_bold
37
+ WHITE_BOLD = "\033[1;37m" # white_bold
38
+
39
+ # Reset Color
40
+ RESET = "\033[0m"
crypticorn/common/auth.py CHANGED
@@ -75,7 +75,7 @@ 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: " + ", ".join(api_scopes) + ")",
79
79
  ),
80
80
  )
81
81
 
@@ -9,6 +9,7 @@ import logging
9
9
 
10
10
  logger = logging.getLogger(__name__)
11
11
 
12
+
12
13
  class ExceptionType(StrEnum):
13
14
  HTTP = "http"
14
15
  WEBSOCKET = "websocket"
@@ -91,34 +92,37 @@ class WebSocketException(HTTPException):
91
92
  )
92
93
 
93
94
 
94
- async def general_handler(request: Request, exc: Exception):
95
+ async def general_handler(request: Request, exc: Exception) -> JSONResponse:
95
96
  """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
97
+ body = ExceptionContent(message=str(exc), error=ApiError.UNKNOWN_ERROR)
98
+ res = JSONResponse(
99
+ status_code=body.enrich().status_code, content=HTTPException(content=body).detail
100
100
  )
101
+ logger.error(f"Response validation error: {res}")
102
+ return res
101
103
 
102
104
 
103
- async def request_validation_handler(request: Request, exc: RequestValidationError):
105
+ async def request_validation_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
104
106
  """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
107
+ body = ExceptionContent(message=str(exc), error=ApiError.INVALID_DATA_REQUEST)
108
+ res = JSONResponse(
109
+ status_code=body.enrich().status_code, content=HTTPException(content=body).detail
109
110
  )
111
+ logger.error(f"Response validation error: {res}")
112
+ return res
110
113
 
111
114
 
112
- async def response_validation_handler(request: Request, exc: ResponseValidationError):
115
+ async def response_validation_handler(request: Request, exc: ResponseValidationError) -> JSONResponse:
113
116
  """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
117
+ body = ExceptionContent(message=str(exc), error=ApiError.INVALID_DATA_RESPONSE)
118
+ res = JSONResponse(
119
+ status_code=body.enrich().status_code, content=HTTPException(content=body).detail
118
120
  )
121
+ logger.error(f"Response validation error: {res}")
122
+ return res
119
123
 
120
124
 
121
- async def http_handler(request: Request, exc: HTTPException):
125
+ async def http_handler(request: Request, exc: HTTPException) -> JSONResponse:
122
126
  """This is the exception handler for HTTPExceptions. It unwraps the HTTPException and returns the detail in a flat JSON response."""
123
127
  logger.error(f"HTTP error: {exc.detail}")
124
128
  return JSONResponse(status_code=exc.status_code, content=exc.detail)
@@ -0,0 +1,103 @@
1
+ # shared_logger.py
2
+ import logging
3
+ from logging import _nameToLevel, _levelToName
4
+ import sys
5
+ from contextvars import ContextVar
6
+ import json
7
+ from pydantic import BaseModel
8
+ from enum import StrEnum
9
+ from crypticorn.common.mixins import ValidateEnumMixin
10
+ from crypticorn.common.ansi_colors import AnsiColors as C
11
+ from datetime import datetime
12
+
13
+ class LogLevel(ValidateEnumMixin, StrEnum):
14
+ DEBUG = "DEBUG"
15
+ INFO = "INFO"
16
+ WARNING = "WARNING"
17
+ ERROR = "ERROR"
18
+ CRITICAL = "CRITICAL"
19
+
20
+ @classmethod
21
+ def get_color(cls, level: str) -> str:
22
+ if level == cls.DEBUG:
23
+ return C.GREEN_BRIGHT
24
+ elif level == cls.INFO:
25
+ return C.BLUE_BRIGHT
26
+ elif level == cls.WARNING:
27
+ return C.YELLOW_BRIGHT
28
+ elif level == cls.ERROR:
29
+ return C.RED_BRIGHT
30
+ elif level == cls.CRITICAL:
31
+ return C.RED_BOLD
32
+ else:
33
+ return C.RESET
34
+
35
+ @staticmethod
36
+ def get_level(level: "LogLevel") -> int:
37
+ return _nameToLevel.get(level, logging.INFO)
38
+
39
+ @staticmethod
40
+ def get_name(level: int) -> "LogLevel":
41
+ return LogLevel(_levelToName.get(level, "INFO"))
42
+
43
+
44
+ _LOGFORMAT = (
45
+ f"{C.CYAN_BOLD}%(asctime)s{C.RESET} - "
46
+ f"{C.GREEN_BOLD}%(name)s{C.RESET} - "
47
+ f"%(levelcolor)s%(levelname)s{C.RESET} - "
48
+ f"%(message)s"
49
+ )
50
+ _PLAIN_LOGFORMAT = (
51
+ "%(asctime)s - " "%(name)s - " "%(levelname)s - " "%(message)s"
52
+ )
53
+ _DATEFMT = "%Y-%m-%d %H:%M:%S.%f:"
54
+
55
+
56
+ class CustomFormatter(logging.Formatter):
57
+ def __init__(self, *args, **kwargs):
58
+ super().__init__(*args, **kwargs)
59
+
60
+ def format(self, record):
61
+ color = LogLevel.get_color(record.levelname)
62
+ record.levelcolor = color
63
+ return super().format(record)
64
+
65
+ def formatTime(self, record, datefmt=_DATEFMT):
66
+ dt = datetime.fromtimestamp(record.created)
67
+ s = dt.strftime(datefmt)
68
+ return s[:-3] # Trim last 3 digits to get milliseconds
69
+
70
+ def get_logger(
71
+ name: str, fmt: str = _LOGFORMAT, datefmt: str = _DATEFMT,
72
+ stdout_level: int = logging.INFO,
73
+ file_level: int = logging.INFO,
74
+ log_file: str = None,
75
+ filters: list[logging.Filter] = [],
76
+ ) -> logging.Logger:
77
+ """Returns crypticorn logger instance."""
78
+ logger = logging.getLogger(name)
79
+
80
+ if logger.handlers: # clear existing handlers to avoid duplicates
81
+ logger.handlers.clear()
82
+
83
+ logger.setLevel(min(stdout_level, file_level)) # set to most verbose level
84
+
85
+ # Configure stdout handler
86
+ stdout_handler = logging.StreamHandler(sys.stdout)
87
+ stdout_handler.setLevel(stdout_level)
88
+ stdout_handler.setFormatter(CustomFormatter(fmt=fmt, datefmt=datefmt))
89
+ for filter in filters:
90
+ stdout_handler.addFilter(filter)
91
+ logger.addHandler(stdout_handler)
92
+
93
+ # Configure file handler
94
+ if log_file:
95
+ os.makedirs(os.path.dirname(log_file), exist_ok=True)
96
+ file_handler = logging.RotatingFileHandler(log_file, maxBytes=10*1024*1024, backupCount=5)
97
+ file_handler.setLevel(file_level)
98
+ file_handler.setFormatter(CustomFormatter(fmt=fmt, datefmt=datefmt))
99
+ for filter in filters:
100
+ file_handler.addFilter(filter)
101
+ logger.addHandler(file_handler)
102
+
103
+ return logger
@@ -0,0 +1,96 @@
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.WRITE_LOGS])])
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import pkg_resources
11
+ import threading
12
+ import time
13
+ import json
14
+ import psutil
15
+ from fastapi import APIRouter, HTTPException, Body, Query
16
+ from typing import Literal, Union
17
+ from dotenv import dotenv_values
18
+ from crypticorn.common.logging import LogLevel
19
+ import logging
20
+
21
+ router = APIRouter(tags=["Admin"], prefix="/admin")
22
+
23
+ START_TIME = time.time()
24
+
25
+ @router.get("/log-level", status_code=200, operation_id="getLogLevel")
26
+ async def get_logging_level() -> LogLevel:
27
+ """
28
+ Get the log level of the server logger.
29
+ """
30
+ return LogLevel.get_name(logging.getLogger().level)
31
+
32
+ @router.get("/uptime", operation_id="getUptime", status_code=200)
33
+ def get_uptime(
34
+ type: Literal["seconds", "human"] = "seconds"
35
+ ) -> Union[int, str]:
36
+ """Return the server uptime in seconds or human-readable form."""
37
+ uptime_seconds = int(time.time() - START_TIME)
38
+ if type == "seconds":
39
+ return uptime_seconds
40
+ elif type == "human":
41
+ return time.strftime("%H:%M:%S", time.gmtime(uptime_seconds))
42
+
43
+
44
+ @router.get("/memory", operation_id="getMemoryUsage", status_code=200)
45
+ def get_memory_usage() -> int:
46
+ """
47
+ Resident Set Size (RSS) in MB — the actual memory used by the process in RAM.
48
+ Represents the physical memory footprint. Important for monitoring real usage.
49
+ """
50
+ process = psutil.Process(os.getpid())
51
+ mem_info = process.memory_info()
52
+ return round(mem_info.rss / (1024 * 1024), 2)
53
+
54
+
55
+ @router.get("/threads", operation_id="getThreads", status_code=200)
56
+ def get_threads() -> dict:
57
+ """Return count and names of active threads."""
58
+ threads = threading.enumerate()
59
+ return {
60
+ "count": len(threads),
61
+ "threads": [t.name for t in threads],
62
+ }
63
+
64
+
65
+ @router.get("/limits", operation_id="getContainerLimits", status_code=200)
66
+ def get_container_limits() -> dict:
67
+ """Return container resource limits from cgroup."""
68
+ limits = {}
69
+ try:
70
+ with open("/sys/fs/cgroup/memory/memory.limit_in_bytes") as f:
71
+ limits["memory_limit_MB"] = int(f.read().strip()) / 1024 / 1024
72
+ except Exception:
73
+ limits["memory_limit_MB"] = "N/A"
74
+
75
+ try:
76
+ with open("/sys/fs/cgroup/cpu/cpu.cfs_quota_us") as f1, open("/sys/fs/cgroup/cpu/cpu.cfs_period_us") as f2:
77
+ quota = int(f1.read().strip())
78
+ period = int(f2.read().strip())
79
+ limits["cpu_limit_cores"] = quota / period if quota > 0 else "N/A"
80
+ except Exception:
81
+ limits["cpu_limit_cores"] = "N/A"
82
+
83
+ return limits
84
+
85
+
86
+ @router.get("/dependencies", operation_id="getDependencies", status_code=200)
87
+ def list_installed_packages(
88
+ include: list[str] = Query(
89
+ default=None, 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
+ [{dist.project_name: dist.version} for dist in pkg_resources.working_set if include is None or dist.project_name in include],
95
+ key=lambda x: next(iter(x)),
96
+ )
@@ -22,23 +22,3 @@ async def time(type: Literal["iso", "unix"] = "iso") -> str:
22
22
  return datetime.now().isoformat()
23
23
  else:
24
24
  return str(int(datetime.now().timestamp()))
25
-
26
-
27
- @router.get("/config", operation_id="getConfig")
28
- async def config() -> dict:
29
- """
30
- Returns the version of the crypticorn library and the environment.
31
- """
32
- import importlib.metadata
33
- import os
34
- from dotenv import load_dotenv
35
-
36
- load_dotenv()
37
- try:
38
- crypticorn_version = importlib.metadata.version("crypticorn")
39
- except importlib.metadata.PackageNotFoundError:
40
- crypticorn_version = "not installed"
41
- return {
42
- "crypticorn": f"v{crypticorn_version}",
43
- "environment": os.getenv("API_ENV", "not set"),
44
- }
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crypticorn
3
- Version: 2.7.5
3
+ Version: 2.8.0rc1
4
4
  Summary: Maximise Your Crypto Trading Profits with Machine Learning
5
5
  Author-email: Crypticorn <timon@crypticorn.com>
6
6
  License: MIT
@@ -63,18 +63,21 @@ crypticorn/cli/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
63
63
  crypticorn/cli/templates/auth.py,sha256=Q1TxlA7qzhjvrqp1xz1aV2vGnj3DKFNN-VSl3o0B-dI,983
64
64
  crypticorn/cli/templates/dependabot.yml,sha256=ct5ieB8KAV1KLzoYKUNm6dZ9wKG_P_JQHgRjZUfT54w,861
65
65
  crypticorn/cli/templates/ruff.yml,sha256=gWicFFTzC4nToSmRkIIGipos8CZ447YG0kebBCJhtJE,319
66
- crypticorn/common/__init__.py,sha256=42ajAXlz0LDBb1AFyX8xvwpp1MB_YrvqutFDkLthUQM,464
67
- crypticorn/common/auth.py,sha256=60SRXlW72VJO8rGzCiemWmzGu8tXDqWr0wt9EM6p8aI,8631
66
+ crypticorn/common/__init__.py,sha256=FkOmpnrSPtSyz5CoWWqIEslEF601vb_3H46ZUwwEtAQ,628
67
+ crypticorn/common/ansi_colors.py,sha256=WqyRMlu3tJj5e-c3hEpa0dupNVZhzRmQcxJgRWSyJ_k,1202
68
+ crypticorn/common/auth.py,sha256=6oJ26t0N2Msup-okC2iFhpHz9ZeaGnQZ5zaCODIsN_k,8673
68
69
  crypticorn/common/decorators.py,sha256=pmnGYCIrLv59wZkDbvPyK9NJmgPJWW74LXTdIWSjOkY,1063
69
70
  crypticorn/common/enums.py,sha256=RitDVqlG_HTe6tHT6bWusZNFCeYk1eQvJVH-7x3_Zlg,668
70
71
  crypticorn/common/errors.py,sha256=8jxZ2lLn_NoFKKq6n2JwKPsR0dA2vkGnbXDfEK6ndH0,27851
71
- crypticorn/common/exceptions.py,sha256=FOxScGTnAoiBkpC5lccQ6_b1jIPcWxawZvR_H2KBCNY,5953
72
+ crypticorn/common/exceptions.py,sha256=tOg8WjJXTtDcqZjokwk4QD5cQtXq5-2gAbvbHENJTng,6050
73
+ crypticorn/common/logging.py,sha256=ncy4E9ogL7HPPgvcHS4zMiXBF5kyOrNcxHFHYJ7IyMw,3220
72
74
  crypticorn/common/mixins.py,sha256=LKPcNTR8uREeDGWTlWozNx7rS1mYdQVx1RllLhxIAsE,1640
73
75
  crypticorn/common/pagination.py,sha256=c07jrMNrBaNTmgx4sppdP7ND4RNT7NBqBXWvofazIlE,2251
74
- crypticorn/common/scopes.py,sha256=gbxrzME18ASQS18IHg96TvFZxh5-O8ffD2caGpfs0lc,2333
75
- crypticorn/common/status_router.py,sha256=s7LY3aNQPhtDUgNWHRszfCQMl0Uh13li_jR8jeeolnw,1139
76
+ crypticorn/common/scopes.py,sha256=ofJ5FDf30wab572XvDzAXVKBIUWa3shScAmzNrJsWqQ,2453
76
77
  crypticorn/common/urls.py,sha256=3Gf1NU1XQYcOTjcdztG3bDAE98FVbgTK2QXzUe7tFVQ,878
77
78
  crypticorn/common/utils.py,sha256=Kz2-I96MKIGKM18PHQ77VbKHLMGUvZG_jjj7xpQed8k,2138
79
+ crypticorn/common/router/admin_router.py,sha256=aaAqWAnaZ2CkHU9cJgeZKyUoc6JnC6oEQhUa_P0bWKw,3380
80
+ crypticorn/common/router/status_router.py,sha256=RIgyNVVnD6Y5Qsfz_oJk0BDuPPmCBM_B4ytcRiSXvo8,576
78
81
  crypticorn/hive/__init__.py,sha256=hRfTlEzEql4msytdUC_04vfaHzVKG5CGZle1M-9QFgY,81
79
82
  crypticorn/hive/main.py,sha256=4oQ2RybZMbe0kRxVJrVAABsN5kUTCMExQFJDSnAzBUY,2428
80
83
  crypticorn/hive/utils.py,sha256=dxQ_OszrnTsslO5hDefMmgfj6yRwRPr8sr17fGizWIw,2066
@@ -224,8 +227,8 @@ crypticorn/trade/client/models/strategy_model_input.py,sha256=ala19jARyfA5ysys5D
224
227
  crypticorn/trade/client/models/strategy_model_output.py,sha256=2o2lhbgUSTznowpMLEHF1Ex9TG9oRmzlCIb-gXqo7_s,5643
225
228
  crypticorn/trade/client/models/tpsl.py,sha256=C2KgTIZs-a8W4msdaXgBKJcwtA-o5wR4rBauRP-iQxU,4317
226
229
  crypticorn/trade/client/models/trading_action_type.py,sha256=pGq_TFLMPfYFizYP-xKgEC1ZF4U3lGdJYoGa_ZH2x-Q,769
227
- crypticorn-2.7.5.dist-info/METADATA,sha256=5E_LCIIfd_O0t6Q8HBqaVLQVSvq_xgvVmMiCgPW_AZU,6607
228
- crypticorn-2.7.5.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
229
- crypticorn-2.7.5.dist-info/entry_points.txt,sha256=d_xHsGvUTebPveVUK0SrpDFQ5ZRSjlI7lNCc11sn2PM,59
230
- crypticorn-2.7.5.dist-info/top_level.txt,sha256=EP3NY216qIBYfmvGl0L2Zc9ItP0DjGSkiYqd9xJwGcM,11
231
- crypticorn-2.7.5.dist-info/RECORD,,
230
+ crypticorn-2.8.0rc1.dist-info/METADATA,sha256=HdPQkiN3MMSTb4TS9IJQrhfN758XihFO2IG1OBJtrMg,6610
231
+ crypticorn-2.8.0rc1.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
232
+ crypticorn-2.8.0rc1.dist-info/entry_points.txt,sha256=d_xHsGvUTebPveVUK0SrpDFQ5ZRSjlI7lNCc11sn2PM,59
233
+ crypticorn-2.8.0rc1.dist-info/top_level.txt,sha256=EP3NY216qIBYfmvGl0L2Zc9ItP0DjGSkiYqd9xJwGcM,11
234
+ crypticorn-2.8.0rc1.dist-info/RECORD,,