crypticorn 2.4.1__py3-none-any.whl → 2.4.2__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.
@@ -49,6 +49,7 @@ class CreateApiKeyRequest(BaseModel):
49
49
  for i in value:
50
50
  if i not in set(
51
51
  [
52
+ "read:predictions",
52
53
  "read:hive:model",
53
54
  "read:hive:data",
54
55
  "write:hive:model",
@@ -71,11 +72,15 @@ class CreateApiKeyRequest(BaseModel):
71
72
  "write:pay:products",
72
73
  "read:pay:now",
73
74
  "write:pay:now",
74
- "read:predictions",
75
+ "read:metrics:marketcap",
76
+ "read:metrics:indicators",
77
+ "read:metrics:exchanges",
78
+ "read:metrics:tokens",
79
+ "read:metrics:markets",
75
80
  ]
76
81
  ):
77
82
  raise ValueError(
78
- "each list item must be one of ('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:predictions')"
83
+ "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')"
79
84
  )
80
85
  return value
81
86
 
@@ -57,6 +57,7 @@ class GetApiKeys200ResponseInner(BaseModel):
57
57
  for i in value:
58
58
  if i not in set(
59
59
  [
60
+ "read:predictions",
60
61
  "read:hive:model",
61
62
  "read:hive:data",
62
63
  "write:hive:model",
@@ -79,11 +80,15 @@ class GetApiKeys200ResponseInner(BaseModel):
79
80
  "write:pay:products",
80
81
  "read:pay:now",
81
82
  "write:pay:now",
82
- "read:predictions",
83
+ "read:metrics:marketcap",
84
+ "read:metrics:indicators",
85
+ "read:metrics:exchanges",
86
+ "read:metrics:tokens",
87
+ "read:metrics:markets",
83
88
  ]
84
89
  ):
85
90
  raise ValueError(
86
- "each list item must be one of ('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:predictions')"
91
+ "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')"
87
92
  )
88
93
  return value
89
94
 
@@ -1,3 +1,3 @@
1
1
  from crypticorn.cli.init import init_group
2
2
 
3
- __all__ = ["init_group"]
3
+ __all__ = ["init_group"]
@@ -3,12 +3,14 @@
3
3
  import click
4
4
  from crypticorn.cli import init_group
5
5
 
6
+
6
7
  @click.group()
7
8
  def cli():
8
9
  """🧙 Crypticorn CLI — magic for our microservices."""
9
10
  pass
10
11
 
12
+
11
13
  cli.add_command(init_group, name="init")
12
14
 
13
15
  if __name__ == "__main__":
14
- cli()
16
+ cli()
crypticorn/cli/init.py CHANGED
@@ -4,16 +4,24 @@ import subprocess
4
4
  import importlib.resources as pkg_resources
5
5
  import crypticorn.cli.templates as templates
6
6
 
7
+
7
8
  def get_git_root() -> Path:
8
- '''Get the root directory of the git repository.'''
9
+ """Get the root directory of the git repository."""
9
10
  try:
10
- return Path(subprocess.check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip())
11
+ return Path(
12
+ subprocess.check_output(
13
+ ["git", "rev-parse", "--show-toplevel"], text=True
14
+ ).strip()
15
+ )
11
16
  except Exception:
12
17
  return Path.cwd()
13
18
 
19
+
14
20
  def copy_template(template_name: str, target_path: Path):
15
- '''Copy a template file to the target path.'''
16
- with pkg_resources.files(templates).joinpath(template_name).open("r") as template_file:
21
+ """Copy a template file to the target path."""
22
+ with pkg_resources.files(templates).joinpath(template_name).open(
23
+ "r"
24
+ ) as template_file:
17
25
  content = template_file.read()
18
26
 
19
27
  target_path.parent.mkdir(parents=True, exist_ok=True)
@@ -22,13 +30,15 @@ def copy_template(template_name: str, target_path: Path):
22
30
 
23
31
  click.secho(f"✅ Created: {target_path}", fg="green")
24
32
 
33
+
25
34
  @click.group()
26
35
  def init_group():
27
36
  """Initialize files like CI configs, linters, etc."""
28
37
  pass
29
38
 
39
+
30
40
  @init_group.command("ruff")
31
- @click.option('-f', '--force', is_flag=True, help='Force overwrite the ruff.yml')
41
+ @click.option("-f", "--force", is_flag=True, help="Force overwrite the ruff.yml")
32
42
  def init_ruff(force):
33
43
  """Add .github/workflows/ruff.yml"""
34
44
  root = get_git_root()
@@ -38,9 +48,12 @@ def init_ruff(force):
38
48
  return
39
49
  copy_template("ruff.yml", target)
40
50
 
51
+
41
52
  @init_group.command("docker")
42
- @click.option('-o', '--output', type=click.Path(), help='Custom output path for the Dockerfile')
43
- @click.option('-f', '--force', is_flag=True, help='Force overwrite the Dockerfile')
53
+ @click.option(
54
+ "-o", "--output", type=click.Path(), help="Custom output path for the Dockerfile"
55
+ )
56
+ @click.option("-f", "--force", is_flag=True, help="Force overwrite the Dockerfile")
44
57
  def init_docker(output, force):
45
58
  """Add Dockerfile"""
46
59
  root = get_git_root()
@@ -51,9 +64,12 @@ def init_docker(output, force):
51
64
  copy_template("Dockerfile", target)
52
65
  click.secho("Make sure to update the Dockerfile", fg="yellow")
53
66
 
67
+
54
68
  @init_group.command("auth")
55
- @click.option('-o', '--output', type=click.Path(), help='Custom output path for the auth handler')
56
- @click.option('-f', '--force', is_flag=True, help='Force overwrite the auth handler')
69
+ @click.option(
70
+ "-o", "--output", type=click.Path(), help="Custom output path for the auth handler"
71
+ )
72
+ @click.option("-f", "--force", is_flag=True, help="Force overwrite the auth handler")
57
73
  def init_auth(output, force):
58
74
  """Add auth.py with auth handler. Everything you need to start using the auth service."""
59
75
  root = get_git_root()
@@ -65,17 +81,21 @@ def init_auth(output, force):
65
81
  click.secho("File already exists, use --force / -f to overwrite", fg="red")
66
82
  return
67
83
  copy_template("auth.py", target)
68
- click.secho('''
84
+ click.secho(
85
+ """
69
86
  Make sure to update the .env file with:
70
87
  IS_DOCKER=0
71
88
  API_ENV=local
72
89
  and the docker-compose.yml file with:
73
90
  environment:
74
91
  - IS_DOCKER=1
75
- ''', fg="yellow")
92
+ """,
93
+ fg="yellow",
94
+ )
95
+
76
96
 
77
97
  @init_group.command("dependabot")
78
- @click.option('-f', '--force', is_flag=True, help='Force overwrite the dependabot.yml')
98
+ @click.option("-f", "--force", is_flag=True, help="Force overwrite the dependabot.yml")
79
99
  def init_dependabot(force):
80
100
  """Add dependabot.yml"""
81
101
  root = get_git_root()
@@ -18,10 +18,14 @@ DOCKER_ENV = os.getenv("IS_DOCKER")
18
18
  API_ENV = os.getenv("API_ENV")
19
19
 
20
20
  if not DOCKER_ENV:
21
- raise ValueError("IS_DOCKER is not set. Please set it to '0' in .env and '1' in the docker-compose.yml file.")
21
+ raise ValueError(
22
+ "IS_DOCKER is not set. Please set it to '0' in .env and '1' in the docker-compose.yml file."
23
+ )
22
24
 
23
25
  if not API_ENV:
24
- raise ValueError("API_ENV is not set. Please set it to 'prod', 'dev' or 'local' in .env (of type ApiEnv).")
26
+ raise ValueError(
27
+ "API_ENV is not set. Please set it to 'prod', 'dev' or 'local' in .env (of type ApiEnv)."
28
+ )
25
29
 
26
30
  if DOCKER_ENV == "0":
27
31
  logger.info(f"Using {API_ENV} environment")
@@ -31,4 +35,4 @@ else:
31
35
  logger.info("Using docker environment")
32
36
 
33
37
  auth_handler = AuthHandler(base_url=base_url)
34
- logger.info(f"Auth URL: {auth_handler.client.config.host}")
38
+ logger.info(f"Auth URL: {auth_handler.client.config.host}")
@@ -1,5 +1,6 @@
1
1
  from enum import StrEnum
2
2
 
3
+
3
4
  class ValidateEnumMixin:
4
5
  """
5
6
  Mixin for validating enum values manually.
@@ -18,6 +19,7 @@ class ValidateEnumMixin:
18
19
 
19
20
  Order of inheritance matters — the mixin must come first.
20
21
  """
22
+
21
23
  @classmethod
22
24
  def validate(cls, value) -> bool:
23
25
  try:
@@ -26,13 +28,17 @@ class ValidateEnumMixin:
26
28
  except ValueError:
27
29
  return False
28
30
 
31
+
29
32
  class Exchange(ValidateEnumMixin, StrEnum):
30
33
  """Supported exchanges for trading"""
34
+
31
35
  KUCOIN = "kucoin"
32
36
  BINGX = "bingx"
33
37
 
38
+
34
39
  class InternalExchange(ValidateEnumMixin, StrEnum):
35
40
  """All exchanges we are using, including public (Exchange)"""
41
+
36
42
  KUCOIN = "kucoin"
37
43
  BINGX = "bingx"
38
44
  BINANCE = "binance"
@@ -40,9 +46,11 @@ class InternalExchange(ValidateEnumMixin, StrEnum):
40
46
  HYPERLIQUID = "hyperliquid"
41
47
  BITGET = "bitget"
42
48
 
49
+
43
50
  class MarketType(ValidateEnumMixin, StrEnum):
44
51
  """
45
52
  Market types
46
53
  """
54
+
47
55
  SPOT = "spot"
48
56
  FUTURES = "futures"
@@ -1,5 +1,6 @@
1
1
  from enum import Enum, EnumMeta
2
2
  import logging
3
+ from fastapi import status
3
4
 
4
5
  logger = logging.getLogger(__name__)
5
6
 
@@ -58,7 +59,7 @@ class ApiErrorIdentifier(str, Enum):
58
59
  INSUFFICIENT_SCOPES = "insufficient_scopes"
59
60
  INVALID_API_KEY = "invalid_api_key"
60
61
  INVALID_BEARER = "invalid_bearer"
61
- INVALID_EXCHANGE_API_KEY = "invalid_exchange_api_key"
62
+ INVALID_EXCHANGE_KEY = "invalid_exchange_key"
62
63
  INVALID_MARGIN_MODE = "invalid_margin_mode"
63
64
  INVALID_PARAMETER = "invalid_parameter_provided"
64
65
  JWT_EXPIRED = "jwt_expired"
@@ -67,6 +68,7 @@ class ApiErrorIdentifier(str, Enum):
67
68
  NO_CREDENTIALS = "no_credentials"
68
69
  NOW_API_DOWN = "now_api_down"
69
70
  OBJECT_NOT_FOUND = "object_not_found"
71
+ OBJECT_ALREADY_EXISTS = "object_already_exists"
70
72
  ORDER_ALREADY_FILLED = "order_is_already_filled"
71
73
  ORDER_IN_PROCESS = "order_is_being_processed"
72
74
  ORDER_LIMIT_EXCEEDED = "order_quantity_limit_exceeded"
@@ -105,7 +107,7 @@ class ApiErrorLevel(str, Enum):
105
107
 
106
108
 
107
109
  class ApiError(Enum, metaclass=Fallback):
108
- """API error codes"""
110
+ """API error codes. Fallback to UNKNOWN_ERROR for error codes not yet published to PyPI."""
109
111
 
110
112
  ALLOCATION_BELOW_EXPOSURE = (
111
113
  ApiErrorIdentifier.ALLOCATION_BELOW_EXPOSURE,
@@ -252,8 +254,8 @@ class ApiError(Enum, metaclass=Fallback):
252
254
  ApiErrorType.USER_ERROR,
253
255
  ApiErrorLevel.ERROR,
254
256
  )
255
- INVALID_EXCHANGE_API_KEY = (
256
- ApiErrorIdentifier.INVALID_EXCHANGE_API_KEY,
257
+ INVALID_EXCHANGE_KEY = (
258
+ ApiErrorIdentifier.INVALID_EXCHANGE_KEY,
257
259
  ApiErrorType.SERVER_ERROR,
258
260
  ApiErrorLevel.ERROR,
259
261
  )
@@ -297,6 +299,11 @@ class ApiError(Enum, metaclass=Fallback):
297
299
  ApiErrorType.SERVER_ERROR,
298
300
  ApiErrorLevel.ERROR,
299
301
  )
302
+ OBJECT_ALREADY_EXISTS = (
303
+ ApiErrorIdentifier.OBJECT_ALREADY_EXISTS,
304
+ ApiErrorType.SERVER_ERROR,
305
+ ApiErrorLevel.ERROR,
306
+ )
300
307
  ORDER_ALREADY_FILLED = (
301
308
  ApiErrorIdentifier.ORDER_ALREADY_FILLED,
302
309
  ApiErrorType.SERVER_ERROR,
@@ -426,17 +433,115 @@ class ApiError(Enum, metaclass=Fallback):
426
433
 
427
434
  @property
428
435
  def identifier(self) -> str:
436
+ """Identifier of the error."""
429
437
  return self.value[0]
430
438
 
431
439
  @property
432
440
  def type(self) -> ApiErrorType:
441
+ """Type of the error."""
433
442
  return self.value[1]
434
443
 
435
444
  @property
436
445
  def level(self) -> ApiErrorLevel:
446
+ """Level of the error."""
437
447
  return self.value[2]
448
+
449
+ @property
450
+ def status_code(self) -> int:
451
+ """HTTP status code for the error."""
452
+ return HttpStatusMapper.get_status_code(self)
453
+
454
+
455
+ class HttpStatusMapper:
456
+ """Map API errors to HTTP status codes."""
457
+ # TODO: decide if we need all of these mappings, since most errors are not exposed to the client via HTTP
458
+ # in case we remove some, update the pytest length check
459
+ _mapping = {
460
+ # Authentication/Authorization
461
+ ApiError.JWT_EXPIRED: status.HTTP_401_UNAUTHORIZED,
462
+ ApiError.INVALID_BEARER: status.HTTP_401_UNAUTHORIZED,
463
+ ApiError.INVALID_API_KEY: status.HTTP_401_UNAUTHORIZED,
464
+ ApiError.NO_CREDENTIALS: status.HTTP_401_UNAUTHORIZED,
465
+ ApiError.INSUFFICIENT_SCOPES: status.HTTP_403_FORBIDDEN,
466
+ ApiError.EXCHANGE_PERMISSION_DENIED: status.HTTP_403_FORBIDDEN,
467
+ ApiError.EXCHANGE_USER_FROZEN: status.HTTP_403_FORBIDDEN,
468
+ ApiError.TRADING_LOCKED: status.HTTP_403_FORBIDDEN,
469
+
470
+ # Not Found
471
+ ApiError.URL_NOT_FOUND: status.HTTP_404_NOT_FOUND,
472
+ ApiError.OBJECT_NOT_FOUND: status.HTTP_404_NOT_FOUND,
473
+ ApiError.ORDER_NOT_FOUND: status.HTTP_404_NOT_FOUND,
474
+ ApiError.POSITION_NOT_FOUND: status.HTTP_404_NOT_FOUND,
475
+ ApiError.SYMBOL_NOT_FOUND: status.HTTP_404_NOT_FOUND,
476
+
477
+ # Conflicts/Duplicates
478
+ ApiError.CLIENT_ORDER_ID_REPEATED: status.HTTP_409_CONFLICT,
479
+ ApiError.OBJECT_ALREADY_EXISTS: status.HTTP_409_CONFLICT,
480
+ ApiError.EXCHANGE_KEY_ALREADY_EXISTS: status.HTTP_409_CONFLICT,
481
+ ApiError.BOT_ALREADY_DELETED: status.HTTP_409_CONFLICT,
482
+
483
+ # Invalid Content
484
+ ApiError.CONTENT_TYPE_ERROR: status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
485
+
486
+ # Rate Limits
487
+ ApiError.EXCHANGE_RATE_LIMIT: status.HTTP_429_TOO_MANY_REQUESTS,
488
+ ApiError.REQUEST_SCOPE_EXCEEDED: status.HTTP_429_TOO_MANY_REQUESTS,
489
+
490
+ # Server Errors
491
+ ApiError.UNKNOWN_ERROR: status.HTTP_500_INTERNAL_SERVER_ERROR,
492
+ ApiError.EXCHANGE_SYSTEM_ERROR: status.HTTP_500_INTERNAL_SERVER_ERROR,
493
+ ApiError.NOW_API_DOWN: status.HTTP_500_INTERNAL_SERVER_ERROR,
494
+ ApiError.RPC_TIMEOUT: status.HTTP_500_INTERNAL_SERVER_ERROR,
495
+
496
+ # Service Unavailable
497
+ ApiError.EXCHANGE_SERVICE_UNAVAILABLE: status.HTTP_503_SERVICE_UNAVAILABLE,
498
+ ApiError.EXCHANGE_MAINTENANCE: status.HTTP_503_SERVICE_UNAVAILABLE,
499
+ ApiError.EXCHANGE_SYSTEM_BUSY: status.HTTP_503_SERVICE_UNAVAILABLE,
500
+ ApiError.SETTLEMENT_IN_PROGRESS: status.HTTP_503_SERVICE_UNAVAILABLE,
501
+ ApiError.POSITION_SUSPENDED: status.HTTP_503_SERVICE_UNAVAILABLE,
502
+ ApiError.TRADING_SUSPENDED: status.HTTP_503_SERVICE_UNAVAILABLE,
503
+
504
+ # Bad Requests (400) - Invalid parameters or states
505
+ ApiError.ALLOCATION_BELOW_EXPOSURE: status.HTTP_400_BAD_REQUEST,
506
+ ApiError.ALLOCATION_BELOW_MINIMUM: status.HTTP_400_BAD_REQUEST,
507
+ ApiError.BLACK_SWAN: status.HTTP_400_BAD_REQUEST,
508
+ ApiError.BOT_DISABLED: status.HTTP_400_BAD_REQUEST,
509
+ ApiError.DELETE_BOT_ERROR: status.HTTP_400_BAD_REQUEST,
510
+ ApiError.EXCHANGE_INVALID_SIGNATURE: status.HTTP_400_BAD_REQUEST,
511
+ ApiError.EXCHANGE_INVALID_TIMESTAMP: status.HTTP_400_BAD_REQUEST,
512
+ ApiError.EXCHANGE_IP_RESTRICTED: status.HTTP_400_BAD_REQUEST,
513
+ ApiError.EXCHANGE_KEY_IN_USE: status.HTTP_400_BAD_REQUEST,
514
+ ApiError.EXCHANGE_SYSTEM_CONFIG_ERROR: status.HTTP_400_BAD_REQUEST,
515
+ ApiError.HEDGE_MODE_NOT_ACTIVE: status.HTTP_400_BAD_REQUEST,
516
+ ApiError.HTTP_ERROR: status.HTTP_400_BAD_REQUEST,
517
+ ApiError.INSUFFICIENT_BALANCE: status.HTTP_400_BAD_REQUEST,
518
+ ApiError.INSUFFICIENT_MARGIN: status.HTTP_400_BAD_REQUEST,
519
+ ApiError.INVALID_EXCHANGE_KEY: status.HTTP_400_BAD_REQUEST,
520
+ ApiError.INVALID_MARGIN_MODE: status.HTTP_400_BAD_REQUEST,
521
+ ApiError.INVALID_PARAMETER: status.HTTP_400_BAD_REQUEST,
522
+ ApiError.LEVERAGE_EXCEEDED: status.HTTP_400_BAD_REQUEST,
523
+ ApiError.LIQUIDATION_PRICE_VIOLATION: status.HTTP_400_BAD_REQUEST,
524
+ ApiError.ORDER_ALREADY_FILLED: status.HTTP_400_BAD_REQUEST,
525
+ ApiError.ORDER_IN_PROCESS: status.HTTP_400_BAD_REQUEST,
526
+ ApiError.ORDER_LIMIT_EXCEEDED: status.HTTP_400_BAD_REQUEST,
527
+ ApiError.ORDER_PRICE_INVALID: status.HTTP_400_BAD_REQUEST,
528
+ ApiError.ORDER_SIZE_TOO_LARGE: status.HTTP_400_BAD_REQUEST,
529
+ ApiError.ORDER_SIZE_TOO_SMALL: status.HTTP_400_BAD_REQUEST,
530
+ ApiError.POSITION_LIMIT_EXCEEDED: status.HTTP_400_BAD_REQUEST,
531
+ ApiError.POST_ONLY_REJECTED: status.HTTP_400_BAD_REQUEST,
532
+ ApiError.RISK_LIMIT_EXCEEDED: status.HTTP_400_BAD_REQUEST,
533
+ ApiError.STRATEGY_DISABLED: status.HTTP_400_BAD_REQUEST,
534
+ ApiError.STRATEGY_LEVERAGE_MISMATCH: status.HTTP_400_BAD_REQUEST,
535
+ ApiError.STRATEGY_NOT_SUPPORTING_EXCHANGE: status.HTTP_400_BAD_REQUEST,
536
+ ApiError.TRADING_ACTION_EXPIRED: status.HTTP_400_BAD_REQUEST,
537
+ ApiError.TRADING_ACTION_SKIPPED: status.HTTP_400_BAD_REQUEST,
438
538
 
539
+ # Success cases
540
+ ApiError.SUCCESS: status.HTTP_200_OK,
541
+ ApiError.BOT_STOPPING_COMPLETED: status.HTTP_200_OK,
542
+ }
439
543
 
440
- assert len(list(ApiErrorIdentifier)) == len(
441
- list(ApiError)
442
- ), f"{len(list(ApiErrorIdentifier))} != {len(list(ApiError))}"
544
+ @classmethod
545
+ def get_status_code(cls, error: ApiError) -> int:
546
+ """Get the HTTP status code for the error. If the error is not in the mapping, return 500."""
547
+ return cls._mapping.get(error, status.HTTP_500_INTERNAL_SERVER_ERROR)
@@ -1,16 +1,19 @@
1
1
  from enum import StrEnum, EnumMeta
2
2
  import logging
3
3
 
4
- logger = logging.getLogger('uvicorn')
4
+ logger = logging.getLogger("uvicorn")
5
+
5
6
 
6
7
  class Fallback(EnumMeta):
7
8
  """Fallback to no scope for unknown scopes."""
9
+
8
10
  def __getattr__(cls, name):
9
11
  logger.warning(
10
12
  f"Unknown scope '{name}' - falling back to no scope - update crypticorn package or check for typos"
11
13
  )
12
14
  return None
13
15
 
16
+
14
17
  class Scope(StrEnum, metaclass=Fallback):
15
18
  """
16
19
  The permission scopes for the API.
@@ -55,7 +58,7 @@ class Scope(StrEnum, metaclass=Fallback):
55
58
 
56
59
  # Metrics scopes
57
60
  READ_METRICS_MARKETCAP = "read:metrics:marketcap"
58
- READ_METRICS_INDICATORS = "read:metrics:indicators"
61
+ READ_METRICS_INDICATORS = "read:metrics:indicators"
59
62
  READ_METRICS_EXCHANGES = "read:metrics:exchanges"
60
63
  READ_METRICS_TOKENS = "read:metrics:tokens"
61
64
  READ_METRICS_MARKETS = "read:metrics:markets"
@@ -0,0 +1,50 @@
1
+ from typing import Any
2
+ from decimal import Decimal
3
+ import string
4
+ import random
5
+
6
+ from fastapi import HTTPException
7
+ from fastapi import status
8
+
9
+ from crypticorn.common import ApiError
10
+
11
+
12
+ def throw_if_none(value: Any, message: ApiError) -> None:
13
+ """Throws an FastAPI HTTPException if the value is None."""
14
+ if value is None:
15
+ raise HTTPException(
16
+ status_code=status.HTTP_404_NOT_FOUND, detail=message.identifier
17
+ )
18
+
19
+ def throw_if_falsy(value: Any, message: ApiError) -> None:
20
+ """Throws an FastAPI HTTPException if the value is False."""
21
+ if not value:
22
+ raise HTTPException(
23
+ status_code=status.HTTP_400_BAD_REQUEST, detail=message.identifier
24
+ )
25
+
26
+ def gen_random_id(length: int = 20) -> str:
27
+ """Generate a random base62 string (a-zA-Z0-9) of specified length.
28
+ Kucoin max 40, bingx max 40"""
29
+ charset = string.ascii_letters + string.digits
30
+ return "".join(random.choice(charset) for _ in range(length))
31
+
32
+
33
+ def is_equal(
34
+ a: float | Decimal,
35
+ b: float | Decimal,
36
+ rel_tol: float = 1e-9,
37
+ abs_tol: float = 0.0,
38
+ ) -> bool:
39
+ """
40
+ Compare two Decimal numbers for approximate equality.
41
+ """
42
+ if not isinstance(a, Decimal):
43
+ a = Decimal(str(a))
44
+ if not isinstance(b, Decimal):
45
+ b = Decimal(str(b))
46
+
47
+ # Convert tolerances to Decimal
48
+ return Decimal(abs(a - b)) <= max(
49
+ Decimal(str(rel_tol)) * max(abs(a), abs(b)), Decimal(str(abs_tol))
50
+ )
@@ -59,7 +59,7 @@ class ApiErrorIdentifier(str, Enum):
59
59
  INSUFFICIENT_SCOPES = "insufficient_scopes"
60
60
  INVALID_API_KEY = "invalid_api_key"
61
61
  INVALID_BEARER = "invalid_bearer"
62
- INVALID_EXCHANGE_API_KEY = "invalid_exchange_api_key"
62
+ INVALID_EXCHANGE_KEY = "invalid_exchange_api_key"
63
63
  INVALID_MARGIN_MODE = "invalid_margin_mode"
64
64
  INVALID_PARAMETER_PROVIDED = "invalid_parameter_provided"
65
65
  JWT_EXPIRED = "jwt_expired"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crypticorn
3
- Version: 2.4.1
3
+ Version: 2.4.2
4
4
  Summary: Maximise Your Crypto Trading Profits with AI Predictions
5
5
  Author-email: Crypticorn <timon@crypticorn.com>
6
6
  Project-URL: Homepage, https://crypticorn.com
@@ -56,11 +56,6 @@ You can install the latest stable version from PyPi:
56
56
  pip install crypticorn
57
57
  ```
58
58
 
59
- If you want a specific version, run:
60
- ```bash
61
- pip install crypticorn==2.0.0
62
- ```
63
-
64
59
  If you want the latest version, which could be a pre release, run:
65
60
  ```bash
66
61
  pip install --pre crypticorn
@@ -22,9 +22,9 @@ crypticorn/auth/client/models/authorize_user200_response.py,sha256=tkhq7TaQK4li0
22
22
  crypticorn/auth/client/models/authorize_user200_response_auth.py,sha256=h1PFbqzF96ssj7t7dMMKiNtRjRu44kPNUydevJm8vKo,3228
23
23
  crypticorn/auth/client/models/authorize_user_request.py,sha256=NZRa3ghNEsdt5IpqICCIXm7cn0-iptk_g4ljJ2FavmY,3225
24
24
  crypticorn/auth/client/models/create_api_key200_response.py,sha256=dj6G4vqSTDoUY3jT3_RbUxsLdg_2W-BvfYfwP9O9RJE,2479
25
- crypticorn/auth/client/models/create_api_key_request.py,sha256=5cv8KY_6mFBD6rURBr5V5UTxLzMP56MNCygoM-hdeb8,5091
25
+ crypticorn/auth/client/models/create_api_key_request.py,sha256=FICEtxFThGiEksGaIrN0fhwtRdtRllRHh_vj4NlFC7Y,5443
26
26
  crypticorn/auth/client/models/create_user_request.py,sha256=kqVBJatlPtoYC1-nZnP2Mx2qZP2ATXffod7hTdWtoCY,3501
27
- crypticorn/auth/client/models/get_api_keys200_response_inner.py,sha256=IdTjwSgv5wpQWXb3POk3p4ogT5YP4O8-W6lSLZQYMDY,5479
27
+ crypticorn/auth/client/models/get_api_keys200_response_inner.py,sha256=xVysGpOd0znz55Y2xULhcA0PBNoKwoJ5i_Zc2k_y-vc,5831
28
28
  crypticorn/auth/client/models/list_wallets200_response.py,sha256=OB8nKlBflpj8dXhjTeTewxjSRok3LnllZZ5ZKISDRvE,4752
29
29
  crypticorn/auth/client/models/list_wallets200_response_balances_inner.py,sha256=sLebeWVenEeiHpiCgxQR8iCMlAgjtC6K8CKq6v9g-Ok,4043
30
30
  crypticorn/auth/client/models/list_wallets200_response_balances_inner_sale_round.py,sha256=rqXN7WVEhmeJxDKCSK5sxwWXvmn-Gnf810MGWwIUXII,3623
@@ -54,19 +54,20 @@ crypticorn/auth/client/models/verify_email_request.py,sha256=8MBfxPTLn5X6Z3vE2bl
54
54
  crypticorn/auth/client/models/verify_wallet_request.py,sha256=b0DAocvhKzPXPjM62DZqezlHxq3cNL7UVKl0d2judHQ,2691
55
55
  crypticorn/auth/client/models/wallet_verified200_response.py,sha256=QILnTLsCKdI-WdV_fsLBy1UH4ZZU-U-wWJ9ot8v08tI,2465
56
56
  crypticorn/auth/client/models/whoami200_response.py,sha256=uehdq5epgeOphhrIR3tbrseflxcLAzGyKF-VW-o5cY8,2974
57
- crypticorn/cli/__init__.py,sha256=P5wOAU00ylb1uROkv6BmQwnKtSMGMB4HA0utVYxEhu8,68
58
- crypticorn/cli/__main__.py,sha256=XujzQmkfLN6zDJ-AiUAUTn8Kq-G3HhQ0y3TaUvs9J1E,250
59
- crypticorn/cli/init.py,sha256=n9ldSI5yNYlYTt4jSlOXVSiHBmb2QlE__MYcNkz6Cl0,3321
57
+ crypticorn/cli/__init__.py,sha256=bgMmlpRThjYcxXJ1U3UmLE8ODVT5olmFY1u69VOjthQ,69
58
+ crypticorn/cli/__main__.py,sha256=x9T4xS3U-qokGEzad7rTujmq4yjV5xcYSXgNsDFkvyo,253
59
+ crypticorn/cli/init.py,sha256=hwiu3kOuWVNYNwy9bIhYGCgf6w0kPtimRXUXoNqM-IE,3429
60
60
  crypticorn/cli/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
61
- crypticorn/cli/templates/auth.py,sha256=pr4O9ejxEz10QphEjXAtl0qfwLNKHYzLwvNKHs--WUQ,954
61
+ crypticorn/cli/templates/auth.py,sha256=Q1TxlA7qzhjvrqp1xz1aV2vGnj3DKFNN-VSl3o0B-dI,983
62
62
  crypticorn/common/__init__.py,sha256=r--xgeJUW4FGRabb1AyqlEFsySiSNfxO_Qi3uOpZ-xI,231
63
63
  crypticorn/common/auth.py,sha256=5aVNsl39ngsoFaR5xGuWERRccq4HibYbSp61Z4QOVew,7483
64
- crypticorn/common/enums.py,sha256=OQQT6uaounQzCGoYEM91yjhu8pU2eD5VaYiY8K89k3o,1235
65
- crypticorn/common/errors.py,sha256=HZvd4J2HAN7TvqlZxWtp29XV0wbkacWNZzgdVO-YWpw,14197
64
+ crypticorn/common/enums.py,sha256=6cCwQZVdXUoN33WA8kSf4LeSZyExZcWO2ahSsgGddCs,1243
65
+ crypticorn/common/errors.py,sha256=bhwSrKoCWCKm1iVkZSqxlMVPdccrx3B8kEhjGmOc098,19844
66
66
  crypticorn/common/pydantic.py,sha256=pmnGYCIrLv59wZkDbvPyK9NJmgPJWW74LXTdIWSjOkY,1063
67
- crypticorn/common/scopes.py,sha256=kzCaGRusoYGt_IIPpVQGmvapBGlDmEgdnTQot98x3uE,2133
67
+ crypticorn/common/scopes.py,sha256=LaT0qqhe1ePL0B5KbooXvPs0aIwXzSc69KyDvrZKSdU,2135
68
68
  crypticorn/common/sorter.py,sha256=Lx7hZMzsqrx2nqWOO0sFrSXSK1t2CqQJux70xU49Bz0,1282
69
69
  crypticorn/common/urls.py,sha256=X557WaODUqW2dECi-mOjTbmhkSpnp40fPXDdvlnBXfo,805
70
+ crypticorn/common/utils.py,sha256=PrnLqCACOZQRoKs1mdii-lVULEuMiWZZKTebXnHHgCQ,1461
70
71
  crypticorn/hive/__init__.py,sha256=hRfTlEzEql4msytdUC_04vfaHzVKG5CGZle1M-9QFgY,81
71
72
  crypticorn/hive/main.py,sha256=U4wurkxnKai2i7iiq049ah9nVzBxmexRxBdFIWfd9qE,631
72
73
  crypticorn/hive/client/__init__.py,sha256=DIj3v16Yq5l6yMPoywrL2sOvQ6ZqpAsSJuhssIp8JFQ,2396
@@ -244,7 +245,7 @@ crypticorn/trade/client/api/strategies_api.py,sha256=asNtn8mZDTRu36PGGGHLRtevQ3N
244
245
  crypticorn/trade/client/api/trading_actions_api.py,sha256=BfW61jUsOZxhzcYgLLF19hGeSHElarxUeK1Re0q_wpY,32402
245
246
  crypticorn/trade/client/models/__init__.py,sha256=pYftF6baXUaReQDn54rRln4KljNnN4nbbJqedqW1XaU,2244
246
247
  crypticorn/trade/client/models/action_model.py,sha256=W-6IJl9KgeiBkZc95HH9kdvmmb-vzYtJ-HfKN0YzC_U,10131
247
- crypticorn/trade/client/models/api_error_identifier.py,sha256=kwCKzYwf61WSiscwNHjNFtsX8fzJDJoOT9vZYO2dLxk,4448
248
+ crypticorn/trade/client/models/api_error_identifier.py,sha256=qSdkBAeoQkRhcg-10Tr7DAYqV_pef448ufYOEpbGG9s,4444
248
249
  crypticorn/trade/client/models/api_error_level.py,sha256=78zYTqbnUGvbjptf04e6-bpF8nN-YWqGxjGrdwNe4_4,799
249
250
  crypticorn/trade/client/models/api_error_type.py,sha256=ANXQ3lPxQ9Jyh_-Q4ljHFHt5uH6ljBHPzK7SDy7etek,840
250
251
  crypticorn/trade/client/models/api_key_model.py,sha256=CM6BeEc3ctmfLMnRht-_k_WDCjGWreOI7670h4KhhAM,5281
@@ -269,8 +270,8 @@ crypticorn/trade/client/models/tpsl.py,sha256=QGPhcgadjxAgyzpRSwlZJg_CDLnKxdZgse
269
270
  crypticorn/trade/client/models/trading_action_type.py,sha256=jW0OsNz_ZNXlITxAfh979BH5U12oTXSr6qUVcKcGHhw,847
270
271
  crypticorn/trade/client/models/validation_error.py,sha256=uTkvsKrOAt-21UC0YPqCdRl_OMsuu7uhPtWuwRSYvv0,3228
271
272
  crypticorn/trade/client/models/validation_error_loc_inner.py,sha256=22ql-H829xTBgfxNQZsqd8fS3zQt9tLW1pj0iobo0jY,5131
272
- crypticorn-2.4.1.dist-info/METADATA,sha256=5IBnH4H9R_BsBIiHgeXyFpyK47_X3i2inHZsZ08Z4eE,5927
273
- crypticorn-2.4.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
274
- crypticorn-2.4.1.dist-info/entry_points.txt,sha256=d_xHsGvUTebPveVUK0SrpDFQ5ZRSjlI7lNCc11sn2PM,59
275
- crypticorn-2.4.1.dist-info/top_level.txt,sha256=EP3NY216qIBYfmvGl0L2Zc9ItP0DjGSkiYqd9xJwGcM,11
276
- crypticorn-2.4.1.dist-info/RECORD,,
273
+ crypticorn-2.4.2.dist-info/METADATA,sha256=fcTSKnxW2krhVSbs1P4p5XNNiGKvb4kThvgK9p2yiJ8,5847
274
+ crypticorn-2.4.2.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
275
+ crypticorn-2.4.2.dist-info/entry_points.txt,sha256=d_xHsGvUTebPveVUK0SrpDFQ5ZRSjlI7lNCc11sn2PM,59
276
+ crypticorn-2.4.2.dist-info/top_level.txt,sha256=EP3NY216qIBYfmvGl0L2Zc9ItP0DjGSkiYqd9xJwGcM,11
277
+ crypticorn-2.4.2.dist-info/RECORD,,