crypticorn 2.7.5__py3-none-any.whl → 2.8.0rc2__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 +4 -0
- crypticorn/auth/client/models/create_api_key_request.py +3 -1
- crypticorn/auth/client/models/get_api_keys200_response_inner.py +3 -1
- crypticorn/common/__init__.py +5 -1
- crypticorn/common/ansi_colors.py +36 -0
- crypticorn/common/auth.py +3 -1
- crypticorn/common/exceptions.py +27 -16
- crypticorn/common/logging.py +116 -0
- crypticorn/common/middleware.py +27 -0
- crypticorn/common/router/admin_router.py +100 -0
- crypticorn/common/router/status_router.py +24 -0
- crypticorn/common/scopes.py +5 -0
- {crypticorn-2.7.5.dist-info → crypticorn-2.8.0rc2.dist-info}/METADATA +2 -1
- {crypticorn-2.7.5.dist-info → crypticorn-2.8.0rc2.dist-info}/RECORD +17 -13
- crypticorn/common/status_router.py +0 -44
- {crypticorn-2.7.5.dist-info → crypticorn-2.8.0rc2.dist-info}/WHEEL +0 -0
- {crypticorn-2.7.5.dist-info → crypticorn-2.8.0rc2.dist-info}/entry_points.txt +0 -0
- {crypticorn-2.7.5.dist-info → crypticorn-2.8.0rc2.dist-info}/top_level.txt +0 -0
crypticorn/__init__.py
CHANGED
@@ -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/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,36 @@
|
|
1
|
+
from enum import StrEnum
|
2
|
+
from typing import TYPE_CHECKING
|
3
|
+
|
4
|
+
class AnsiColors(StrEnum):
|
5
|
+
# Regular Text Colors
|
6
|
+
BLACK = "\033[30m" # black
|
7
|
+
RED = "\033[31m" # red
|
8
|
+
GREEN = "\033[32m" # green
|
9
|
+
YELLOW = "\033[33m" # yellow
|
10
|
+
BLUE = "\033[34m" # blue
|
11
|
+
MAGENTA = "\033[35m" # magenta
|
12
|
+
CYAN = "\033[36m" # cyan
|
13
|
+
WHITE = "\033[37m" # white
|
14
|
+
|
15
|
+
# Bright Text Colors
|
16
|
+
BLACK_BRIGHT = "\033[90m" # black_bright
|
17
|
+
RED_BRIGHT = "\033[91m" # red_bright
|
18
|
+
GREEN_BRIGHT = "\033[92m" # green_bright
|
19
|
+
YELLOW_BRIGHT = "\033[93m" # yellow_bright
|
20
|
+
BLUE_BRIGHT = "\033[94m" # blue_bright
|
21
|
+
MAGENTA_BRIGHT = "\033[95m" # magenta_bright
|
22
|
+
CYAN_BRIGHT = "\033[96m" # cyan_bright
|
23
|
+
WHITE_BRIGHT = "\033[97m" # white_bright
|
24
|
+
|
25
|
+
# Bold Text Colors
|
26
|
+
BLACK_BOLD = "\033[1;30m" # black_bold
|
27
|
+
RED_BOLD = "\033[1;31m" # red_bold
|
28
|
+
GREEN_BOLD = "\033[1;32m" # green_bold
|
29
|
+
YELLOW_BOLD = "\033[1;33m" # yellow_bold
|
30
|
+
BLUE_BOLD = "\033[1;34m" # blue_bold
|
31
|
+
MAGENTA_BOLD = "\033[1;35m" # magenta_bold
|
32
|
+
CYAN_BOLD = "\033[1;36m" # cyan_bold
|
33
|
+
WHITE_BOLD = "\033[1;37m" # white_bold
|
34
|
+
|
35
|
+
# Reset Color
|
36
|
+
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
@@ -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,44 @@ 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)
|
97
|
-
|
98
|
-
|
99
|
-
|
97
|
+
body = ExceptionContent(message=str(exc), error=ApiError.UNKNOWN_ERROR)
|
98
|
+
res = JSONResponse(
|
99
|
+
status_code=body.enrich().status_code,
|
100
|
+
content=HTTPException(content=body).detail,
|
100
101
|
)
|
102
|
+
logger.error(f"Response validation error: {res}")
|
103
|
+
return res
|
101
104
|
|
102
105
|
|
103
|
-
async def request_validation_handler(
|
106
|
+
async def request_validation_handler(
|
107
|
+
request: Request, exc: RequestValidationError
|
108
|
+
) -> JSONResponse:
|
104
109
|
"""This is the exception handler for all request validation errors."""
|
105
|
-
body = ExceptionContent(message=str(exc), error=ApiError.INVALID_DATA_REQUEST)
|
106
|
-
|
107
|
-
|
108
|
-
|
110
|
+
body = ExceptionContent(message=str(exc), error=ApiError.INVALID_DATA_REQUEST)
|
111
|
+
res = JSONResponse(
|
112
|
+
status_code=body.enrich().status_code,
|
113
|
+
content=HTTPException(content=body).detail,
|
109
114
|
)
|
115
|
+
logger.error(f"Response validation error: {res}")
|
116
|
+
return res
|
110
117
|
|
111
118
|
|
112
|
-
async def response_validation_handler(
|
119
|
+
async def response_validation_handler(
|
120
|
+
request: Request, exc: ResponseValidationError
|
121
|
+
) -> JSONResponse:
|
113
122
|
"""This is the exception handler for all response validation errors."""
|
114
|
-
body = ExceptionContent(message=str(exc), error=ApiError.INVALID_DATA_RESPONSE)
|
115
|
-
|
116
|
-
|
117
|
-
|
123
|
+
body = ExceptionContent(message=str(exc), error=ApiError.INVALID_DATA_RESPONSE)
|
124
|
+
res = JSONResponse(
|
125
|
+
status_code=body.enrich().status_code,
|
126
|
+
content=HTTPException(content=body).detail,
|
118
127
|
)
|
128
|
+
logger.error(f"Response validation error: {res}")
|
129
|
+
return res
|
119
130
|
|
120
131
|
|
121
|
-
async def http_handler(request: Request, exc: HTTPException):
|
132
|
+
async def http_handler(request: Request, exc: HTTPException) -> JSONResponse:
|
122
133
|
"""This is the exception handler for HTTPExceptions. It unwraps the HTTPException and returns the detail in a flat JSON response."""
|
123
134
|
logger.error(f"HTTP error: {exc.detail}")
|
124
135
|
return JSONResponse(status_code=exc.status_code, content=exc.detail)
|
@@ -0,0 +1,116 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
# shared_logger.py
|
3
|
+
import logging
|
4
|
+
import sys
|
5
|
+
from contextvars import ContextVar
|
6
|
+
from contextlib import asynccontextmanager
|
7
|
+
import json
|
8
|
+
from pydantic import BaseModel
|
9
|
+
from enum import StrEnum
|
10
|
+
from crypticorn.common.mixins import ValidateEnumMixin
|
11
|
+
from crypticorn.common.ansi_colors import AnsiColors as C
|
12
|
+
from datetime import datetime
|
13
|
+
|
14
|
+
|
15
|
+
class LogLevel(ValidateEnumMixin, StrEnum):
|
16
|
+
DEBUG = "DEBUG"
|
17
|
+
INFO = "INFO"
|
18
|
+
WARNING = "WARNING"
|
19
|
+
ERROR = "ERROR"
|
20
|
+
CRITICAL = "CRITICAL"
|
21
|
+
|
22
|
+
@classmethod
|
23
|
+
def get_color(cls, level: str) -> str:
|
24
|
+
if level == cls.DEBUG:
|
25
|
+
return C.GREEN_BRIGHT
|
26
|
+
elif level == cls.INFO:
|
27
|
+
return C.BLUE_BRIGHT
|
28
|
+
elif level == cls.WARNING:
|
29
|
+
return C.YELLOW_BRIGHT
|
30
|
+
elif level == cls.ERROR:
|
31
|
+
return C.RED_BRIGHT
|
32
|
+
elif level == cls.CRITICAL:
|
33
|
+
return C.RED_BOLD
|
34
|
+
else:
|
35
|
+
return C.RESET
|
36
|
+
|
37
|
+
@staticmethod
|
38
|
+
def get_level(level: "LogLevel") -> int:
|
39
|
+
return logging._nameToLevel.get(level, logging.INFO)
|
40
|
+
|
41
|
+
@staticmethod
|
42
|
+
def get_name(level: int) -> "LogLevel":
|
43
|
+
return LogLevel(logging._levelToName.get(level, "INFO"))
|
44
|
+
|
45
|
+
|
46
|
+
_LOGFORMAT = (
|
47
|
+
f"{C.CYAN_BOLD}%(asctime)s{C.RESET} - "
|
48
|
+
f"{C.GREEN_BOLD}%(name)s{C.RESET} - "
|
49
|
+
f"%(levelcolor)s%(levelname)s{C.RESET} - "
|
50
|
+
f"%(message)s"
|
51
|
+
)
|
52
|
+
_PLAIN_LOGFORMAT = "%(asctime)s - " "%(name)s - " "%(levelname)s - " "%(message)s"
|
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
|
+
|
71
|
+
def configure_logging(
|
72
|
+
name: str,
|
73
|
+
fmt: str = _LOGFORMAT,
|
74
|
+
datefmt: str = _DATEFMT,
|
75
|
+
stdout_level: int = logging.INFO,
|
76
|
+
file_level: int = logging.INFO,
|
77
|
+
log_file: str = None,
|
78
|
+
filters: list[logging.Filter] = [],
|
79
|
+
) -> None:
|
80
|
+
"""Configures the logging for the application.
|
81
|
+
Run this function as early as possible in the application (for example using the `lifespan` parameter in FastAPI).
|
82
|
+
Then use can use the default `logging.getLogger(__name__)` method to get the logger.
|
83
|
+
"""
|
84
|
+
logger = logging.getLogger(name)
|
85
|
+
|
86
|
+
if logger.handlers: # clear existing handlers to avoid duplicates
|
87
|
+
logger.handlers.clear()
|
88
|
+
|
89
|
+
logger.setLevel(min(stdout_level, file_level)) # set to most verbose level
|
90
|
+
|
91
|
+
# Configure stdout handler
|
92
|
+
stdout_handler = logging.StreamHandler(sys.stdout)
|
93
|
+
stdout_handler.setLevel(stdout_level)
|
94
|
+
stdout_handler.setFormatter(CustomFormatter(fmt=fmt, datefmt=datefmt))
|
95
|
+
for filter in filters:
|
96
|
+
stdout_handler.addFilter(filter)
|
97
|
+
logger.addHandler(stdout_handler)
|
98
|
+
|
99
|
+
# Configure file handler
|
100
|
+
if log_file:
|
101
|
+
os.makedirs(os.path.dirname(log_file), exist_ok=True)
|
102
|
+
file_handler = logging.RotatingFileHandler(
|
103
|
+
log_file, maxBytes=10 * 1024 * 1024, backupCount=5
|
104
|
+
)
|
105
|
+
file_handler.setLevel(file_level)
|
106
|
+
file_handler.setFormatter(CustomFormatter(fmt=fmt, datefmt=datefmt))
|
107
|
+
for filter in filters:
|
108
|
+
file_handler.addFilter(filter)
|
109
|
+
logger.addHandler(file_handler)
|
110
|
+
|
111
|
+
|
112
|
+
def disable_logging():
|
113
|
+
"""Disable logging for the crypticorn logger.
|
114
|
+
"""
|
115
|
+
logger = logging.getLogger("crypticorn")
|
116
|
+
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
|
+
async def default_lifespan(app: FastAPI):
|
21
|
+
"""Default lifespan for the applications.
|
22
|
+
This is used to configure the logging for the application.
|
23
|
+
To override this, pass a different lifespan to the FastAPI constructor or call this lifespan within a custom lifespan.
|
24
|
+
"""
|
25
|
+
configure_logging(__name__) # for the consuming app
|
26
|
+
logger = logging.getLogger(__name__)
|
27
|
+
yield
|
@@ -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.WRITE_LOGS])])
|
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")
|
24
|
+
async def get_logging_level() -> LogLevel:
|
25
|
+
"""
|
26
|
+
Get the log level of the server logger.
|
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") -> Union[int, 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 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() -> int:
|
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
|
4
|
+
|
5
|
+
router = APIRouter(tags=["Status"], prefix="")
|
6
|
+
|
7
|
+
|
8
|
+
@router.get("/", operation_id="ping")
|
9
|
+
async def ping() -> str:
|
10
|
+
"""
|
11
|
+
Returns 'OK' if the API is running.
|
12
|
+
"""
|
13
|
+
return "OK"
|
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
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: crypticorn
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.8.0rc2
|
4
4
|
Summary: Maximise Your Crypto Trading Profits with Machine Learning
|
5
5
|
Author-email: Crypticorn <timon@crypticorn.com>
|
6
6
|
License: MIT
|
@@ -16,6 +16,7 @@ Requires-Python: >=3.10
|
|
16
16
|
Description-Content-Type: text/markdown
|
17
17
|
Requires-Dist: fastapi<1.0.0,>=0.115.0
|
18
18
|
Requires-Dist: click<9.0.0,>=8.0.0
|
19
|
+
Requires-Dist: psutil<8.0.0,>=7.0.0
|
19
20
|
Requires-Dist: urllib3<3.0.0,>=1.25.3
|
20
21
|
Requires-Dist: python_dateutil<3.0.0,>=2.8.2
|
21
22
|
Requires-Dist: aiohttp<4.0.0,>=3.8.4
|
@@ -1,4 +1,4 @@
|
|
1
|
-
crypticorn/__init__.py,sha256=
|
1
|
+
crypticorn/__init__.py,sha256=Fi5xI_NnK1cVteLdFY5SJ-mzYkYz3af-WOUauZkfTa0,219
|
2
2
|
crypticorn/client.py,sha256=jMdF4mZuqB8YMZZOwJkTcvpHluGqw6Q-taSF6vQS5WM,3912
|
3
3
|
crypticorn/auth/__init__.py,sha256=JAl1tBLK9pYLr_-YKaj581c-c94PWLoqnatTIVAVvMM,81
|
4
4
|
crypticorn/auth/main.py,sha256=j8eRGN2gUWyeOCnWnUPe3mCAfaidGnOMnZRiSQy-yzM,720
|
@@ -22,9 +22,9 @@ crypticorn/auth/client/models/authorize_user200_response.py,sha256=fstTaffARzCe7
|
|
22
22
|
crypticorn/auth/client/models/authorize_user200_response_auth.py,sha256=OQzmKNICdr7tHrlhEDIxagDZx7XuW8VO2U3OMsWIJOA,3208
|
23
23
|
crypticorn/auth/client/models/authorize_user_request.py,sha256=o_1Ms6npD_1dJQ6aYshEk9JPV7JIkmAmuRawDDPUoTc,3205
|
24
24
|
crypticorn/auth/client/models/create_api_key200_response.py,sha256=UGjZwITv2EdS7bbt55D0w2an6_uO4u6gYr-GihL9m80,2459
|
25
|
-
crypticorn/auth/client/models/create_api_key_request.py,sha256=
|
25
|
+
crypticorn/auth/client/models/create_api_key_request.py,sha256=VaxptajDgoPPIfkkF1qh_wU3a7aTSc5wcjqvDcR5mnQ,5577
|
26
26
|
crypticorn/auth/client/models/create_user_request.py,sha256=nq4t2R4ldqSsJUoVhLw9YocdUwatH4gQ-jdFz3jaqrg,3481
|
27
|
-
crypticorn/auth/client/models/get_api_keys200_response_inner.py,sha256=
|
27
|
+
crypticorn/auth/client/models/get_api_keys200_response_inner.py,sha256=DiNnVej8GasK09PorQCk4jS_LWzwCxtjHEVH42cOA4U,5965
|
28
28
|
crypticorn/auth/client/models/list_wallets200_response.py,sha256=G2LQiThhtynkuDGFvIs-BowR38ut7cRbIeCzK4wMnPw,4732
|
29
29
|
crypticorn/auth/client/models/list_wallets200_response_balances_inner.py,sha256=acTJa8B5U8j3_wS1TUYXnprptqF_T4dlV-wVaQ5ZXig,4023
|
30
30
|
crypticorn/auth/client/models/list_wallets200_response_balances_inner_sale_round.py,sha256=KrCHFXXfc6U61nDFsq26gRtQQnc2_tSjL2fwQOnBfzI,3603
|
@@ -63,18 +63,22 @@ 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=
|
67
|
-
crypticorn/common/
|
66
|
+
crypticorn/common/__init__.py,sha256=29n5tUr9-yjjJ8sjkbMrwq7pDKZ2Z1qz1bR1msGJDkA,671
|
67
|
+
crypticorn/common/ansi_colors.py,sha256=7nRsc5TPRwKI_7yVHD2WrhcOhHUcysktAIEPw_Zl5qk,1173
|
68
|
+
crypticorn/common/auth.py,sha256=GIb9MikQsSxqz-K5rDIOroP5mFoTgQqwByTGO5JqcdM,8713
|
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=
|
72
|
+
crypticorn/common/exceptions.py,sha256=BuRLQIg2_pwsaQAhPKP3lY9q2GNp0SlRdXlvp1O2USo,6088
|
73
|
+
crypticorn/common/logging.py,sha256=aI1RfWMkNDTGov-qiUmvjRk0b9otBwZggWJ5cX5Zmdw,3651
|
74
|
+
crypticorn/common/middleware.py,sha256=mh1lNaN3X_UKH7PflewhlVEc1AKTqhB-tf7r1gLzV3E,1036
|
72
75
|
crypticorn/common/mixins.py,sha256=LKPcNTR8uREeDGWTlWozNx7rS1mYdQVx1RllLhxIAsE,1640
|
73
76
|
crypticorn/common/pagination.py,sha256=c07jrMNrBaNTmgx4sppdP7ND4RNT7NBqBXWvofazIlE,2251
|
74
|
-
crypticorn/common/scopes.py,sha256=
|
75
|
-
crypticorn/common/status_router.py,sha256=s7LY3aNQPhtDUgNWHRszfCQMl0Uh13li_jR8jeeolnw,1139
|
77
|
+
crypticorn/common/scopes.py,sha256=ofJ5FDf30wab572XvDzAXVKBIUWa3shScAmzNrJsWqQ,2453
|
76
78
|
crypticorn/common/urls.py,sha256=3Gf1NU1XQYcOTjcdztG3bDAE98FVbgTK2QXzUe7tFVQ,878
|
77
79
|
crypticorn/common/utils.py,sha256=Kz2-I96MKIGKM18PHQ77VbKHLMGUvZG_jjj7xpQed8k,2138
|
80
|
+
crypticorn/common/router/admin_router.py,sha256=HUhFr5HVOM0JmVgAlLZbQMuSjNfcqULhBqJlNcMJTW4,3376
|
81
|
+
crypticorn/common/router/status_router.py,sha256=922dHfpVM5auOTuNswIhDRmk_1bmU5hH83bxoT70uss,616
|
78
82
|
crypticorn/hive/__init__.py,sha256=hRfTlEzEql4msytdUC_04vfaHzVKG5CGZle1M-9QFgY,81
|
79
83
|
crypticorn/hive/main.py,sha256=4oQ2RybZMbe0kRxVJrVAABsN5kUTCMExQFJDSnAzBUY,2428
|
80
84
|
crypticorn/hive/utils.py,sha256=dxQ_OszrnTsslO5hDefMmgfj6yRwRPr8sr17fGizWIw,2066
|
@@ -224,8 +228,8 @@ crypticorn/trade/client/models/strategy_model_input.py,sha256=ala19jARyfA5ysys5D
|
|
224
228
|
crypticorn/trade/client/models/strategy_model_output.py,sha256=2o2lhbgUSTznowpMLEHF1Ex9TG9oRmzlCIb-gXqo7_s,5643
|
225
229
|
crypticorn/trade/client/models/tpsl.py,sha256=C2KgTIZs-a8W4msdaXgBKJcwtA-o5wR4rBauRP-iQxU,4317
|
226
230
|
crypticorn/trade/client/models/trading_action_type.py,sha256=pGq_TFLMPfYFizYP-xKgEC1ZF4U3lGdJYoGa_ZH2x-Q,769
|
227
|
-
crypticorn-2.
|
228
|
-
crypticorn-2.
|
229
|
-
crypticorn-2.
|
230
|
-
crypticorn-2.
|
231
|
-
crypticorn-2.
|
231
|
+
crypticorn-2.8.0rc2.dist-info/METADATA,sha256=BiEarCgxuOZOuT9onAHw8ciilAQZ03IriGI21WKjYyY,6646
|
232
|
+
crypticorn-2.8.0rc2.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
|
233
|
+
crypticorn-2.8.0rc2.dist-info/entry_points.txt,sha256=d_xHsGvUTebPveVUK0SrpDFQ5ZRSjlI7lNCc11sn2PM,59
|
234
|
+
crypticorn-2.8.0rc2.dist-info/top_level.txt,sha256=EP3NY216qIBYfmvGl0L2Zc9ItP0DjGSkiYqd9xJwGcM,11
|
235
|
+
crypticorn-2.8.0rc2.dist-info/RECORD,,
|
@@ -1,44 +0,0 @@
|
|
1
|
-
from datetime import datetime
|
2
|
-
from typing import Literal
|
3
|
-
from fastapi import APIRouter
|
4
|
-
|
5
|
-
router = APIRouter(tags=["Status"], prefix="")
|
6
|
-
|
7
|
-
|
8
|
-
@router.get("/", operation_id="ping")
|
9
|
-
async def ping() -> str:
|
10
|
-
"""
|
11
|
-
Returns 'OK' if the API is running.
|
12
|
-
"""
|
13
|
-
return "OK"
|
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 the specified format.
|
20
|
-
"""
|
21
|
-
if type == "iso":
|
22
|
-
return datetime.now().isoformat()
|
23
|
-
else:
|
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
|
-
}
|
File without changes
|
File without changes
|
File without changes
|