Rename api_server2 module to apiserver
This commit is contained in:
2
freqtrade/rpc/api_server/__init__.py
Normal file
2
freqtrade/rpc/api_server/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# flake8: noqa: F401
|
||||
from .webserver import ApiServer
|
106
freqtrade/rpc/api_server/api_auth.py
Normal file
106
freqtrade/rpc/api_server/api_auth.py
Normal file
@@ -0,0 +1,106 @@
|
||||
import secrets
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import jwt
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from fastapi.security.http import HTTPBasic, HTTPBasicCredentials
|
||||
|
||||
from freqtrade.rpc.api_server.api_models import AccessAndRefreshToken, AccessToken
|
||||
from freqtrade.rpc.api_server.deps import get_api_config
|
||||
|
||||
|
||||
ALGORITHM = "HS256"
|
||||
|
||||
router_login = APIRouter()
|
||||
|
||||
|
||||
def verify_auth(api_config, username: str, password: str):
|
||||
"""Verify username/password"""
|
||||
return (secrets.compare_digest(username, api_config.get('username')) and
|
||||
secrets.compare_digest(password, api_config.get('password')))
|
||||
|
||||
|
||||
httpbasic = HTTPBasic(auto_error=False)
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False)
|
||||
|
||||
|
||||
def get_user_from_token(token, secret_key: str, token_type: str = "access"):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
try:
|
||||
payload = jwt.decode(token, secret_key, algorithms=[ALGORITHM])
|
||||
username: str = payload.get("identity", {}).get('u')
|
||||
if username is None:
|
||||
raise credentials_exception
|
||||
if payload.get("type") != token_type:
|
||||
raise credentials_exception
|
||||
|
||||
except jwt.PyJWTError:
|
||||
raise credentials_exception
|
||||
return username
|
||||
|
||||
|
||||
def create_token(data: dict, secret_key: str, token_type: str = "access") -> bytes:
|
||||
to_encode = data.copy()
|
||||
if token_type == "access":
|
||||
expire = datetime.utcnow() + timedelta(minutes=15)
|
||||
elif token_type == "refresh":
|
||||
expire = datetime.utcnow() + timedelta(days=30)
|
||||
else:
|
||||
raise ValueError()
|
||||
to_encode.update({
|
||||
"exp": expire,
|
||||
"iat": datetime.utcnow(),
|
||||
"type": token_type,
|
||||
})
|
||||
encoded_jwt = jwt.encode(to_encode, secret_key, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
def http_basic_or_jwt_token(form_data: HTTPBasicCredentials = Depends(httpbasic),
|
||||
token: str = Depends(oauth2_scheme),
|
||||
api_config=Depends(get_api_config)):
|
||||
if token:
|
||||
return get_user_from_token(token, api_config.get('jwt_secret_key', 'super-secret'))
|
||||
elif form_data and verify_auth(api_config, form_data.username, form_data.password):
|
||||
return form_data.username
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Unauthorized",
|
||||
)
|
||||
|
||||
|
||||
@router_login.post('/token/login', response_model=AccessAndRefreshToken)
|
||||
def token_login(form_data: HTTPBasicCredentials = Depends(HTTPBasic()),
|
||||
api_config=Depends(get_api_config)):
|
||||
|
||||
if verify_auth(api_config, form_data.username, form_data.password):
|
||||
token_data = {'identity': {'u': form_data.username}}
|
||||
access_token = create_token(token_data, api_config.get('jwt_secret_key', 'super-secret'))
|
||||
refresh_token = create_token(token_data, api_config.get('jwt_secret_key', 'super-secret'),
|
||||
token_type="refresh")
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
)
|
||||
|
||||
|
||||
@router_login.post('/token/refresh', response_model=AccessToken)
|
||||
def token_refresh(token: str = Depends(oauth2_scheme), api_config=Depends(get_api_config)):
|
||||
# Refresh token
|
||||
u = get_user_from_token(token, api_config.get(
|
||||
'jwt_secret_key', 'super-secret'), 'refresh')
|
||||
token_data = {'identity': {'u': u}}
|
||||
access_token = create_token(token_data, api_config.get('jwt_secret_key', 'super-secret'),
|
||||
token_type="access")
|
||||
return {'access_token': access_token}
|
209
freqtrade/rpc/api_server/api_models.py
Normal file
209
freqtrade/rpc/api_server/api_models.py
Normal file
@@ -0,0 +1,209 @@
|
||||
from datetime import date, datetime
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||
|
||||
|
||||
class Ping(BaseModel):
|
||||
status: str
|
||||
|
||||
|
||||
class AccessToken(BaseModel):
|
||||
access_token: str
|
||||
|
||||
|
||||
class AccessAndRefreshToken(AccessToken):
|
||||
refresh_token: str
|
||||
|
||||
|
||||
class Version(BaseModel):
|
||||
version: str
|
||||
|
||||
|
||||
class StatusMsg(BaseModel):
|
||||
status: str
|
||||
|
||||
|
||||
class ResultMsg(BaseModel):
|
||||
result: str
|
||||
|
||||
|
||||
class Balance(BaseModel):
|
||||
currency: str
|
||||
free: float
|
||||
balance: float
|
||||
used: float
|
||||
est_stake: float
|
||||
stake: str
|
||||
|
||||
|
||||
class Balances(BaseModel):
|
||||
currencies: List[Balance]
|
||||
total: float
|
||||
symbol: str
|
||||
value: float
|
||||
stake: str
|
||||
note: str
|
||||
|
||||
|
||||
class Count(BaseModel):
|
||||
current: int
|
||||
max: int
|
||||
total_stake: float
|
||||
|
||||
|
||||
class PerformanceEntry(BaseModel):
|
||||
pair: str
|
||||
profit: float
|
||||
count: int
|
||||
|
||||
|
||||
class Profit(BaseModel):
|
||||
profit_closed_coin: float
|
||||
profit_closed_percent: float
|
||||
profit_closed_percent_mean: float
|
||||
profit_closed_ratio_mean: float
|
||||
profit_closed_percent_sum: float
|
||||
profit_closed_ratio_sum: float
|
||||
profit_closed_fiat: float
|
||||
profit_all_coin: float
|
||||
profit_all_percent: float
|
||||
profit_all_percent_mean: float
|
||||
profit_all_ratio_mean: float
|
||||
profit_all_percent_sum: float
|
||||
profit_all_ratio_sum: float
|
||||
profit_all_fiat: float
|
||||
trade_count: int
|
||||
closed_trade_count: int
|
||||
first_trade_date: str
|
||||
first_trade_timestamp: int
|
||||
latest_trade_date: str
|
||||
latest_trade_timestamp: int
|
||||
avg_duration: str
|
||||
best_pair: str
|
||||
best_rate: float
|
||||
winning_trades: int
|
||||
losing_trades: int
|
||||
|
||||
|
||||
class SellReason(BaseModel):
|
||||
wins: int
|
||||
losses: int
|
||||
draws: int
|
||||
|
||||
|
||||
class Stats(BaseModel):
|
||||
sell_reasons: Dict[str, SellReason]
|
||||
durations: Dict[str, Union[str, float]]
|
||||
|
||||
|
||||
class DailyRecord(BaseModel):
|
||||
date: date
|
||||
abs_profit: float
|
||||
fiat_value: float
|
||||
trade_count: int
|
||||
|
||||
|
||||
class Daily(BaseModel):
|
||||
data: List[DailyRecord]
|
||||
fiat_display_currency: str
|
||||
stake_currency: str
|
||||
|
||||
|
||||
class LockModel(BaseModel):
|
||||
active: bool
|
||||
lock_end_time: str
|
||||
lock_end_timestamp: int
|
||||
lock_time: str
|
||||
lock_timestamp: int
|
||||
pair: str
|
||||
reason: str
|
||||
|
||||
|
||||
class Locks(BaseModel):
|
||||
lock_count: int
|
||||
locks: List[LockModel]
|
||||
|
||||
|
||||
class Logs(BaseModel):
|
||||
log_count: int
|
||||
logs: List[List]
|
||||
|
||||
|
||||
class ForceBuyPayload(BaseModel):
|
||||
pair: str
|
||||
price: Optional[float]
|
||||
|
||||
|
||||
class ForceSellPayload(BaseModel):
|
||||
tradeid: str
|
||||
|
||||
|
||||
class BlacklistPayload(BaseModel):
|
||||
blacklist: List[str]
|
||||
|
||||
|
||||
class BlacklistResponse(BaseModel):
|
||||
blacklist: List[str]
|
||||
blacklist_expanded: List[str]
|
||||
errors: Dict
|
||||
length: int
|
||||
method: List[str]
|
||||
|
||||
|
||||
class WhitelistResponse(BaseModel):
|
||||
whitelist: List[str]
|
||||
length: int
|
||||
method: List[str]
|
||||
|
||||
|
||||
class DeleteTrade(BaseModel):
|
||||
cancel_order_count: int
|
||||
result: str
|
||||
result_msg: str
|
||||
trade_id: int
|
||||
|
||||
|
||||
class PlotConfig(BaseModel):
|
||||
main_plot: Dict[str, Any]
|
||||
subplots: Optional[Dict[str, Any]]
|
||||
|
||||
|
||||
class StrategyListResponse(BaseModel):
|
||||
strategies: List[str]
|
||||
|
||||
|
||||
class StrategyResponse(BaseModel):
|
||||
strategy: str
|
||||
code: str
|
||||
|
||||
|
||||
class AvailablePairs(BaseModel):
|
||||
length: int
|
||||
pairs: List[str]
|
||||
pair_interval: List[List[str]]
|
||||
|
||||
|
||||
class PairHistory(BaseModel):
|
||||
strategy: str
|
||||
pair: str
|
||||
timeframe: str
|
||||
timeframe_ms: int
|
||||
columns: List[str]
|
||||
data: List[Any]
|
||||
length: int
|
||||
buy_signals: int
|
||||
sell_signals: int
|
||||
last_analyzed: datetime
|
||||
last_analyzed_ts: int
|
||||
data_start_ts: int
|
||||
data_start: str
|
||||
data_stop: str
|
||||
data_stop_ts: int
|
||||
|
||||
class Config:
|
||||
json_encoders = {
|
||||
datetime: lambda v: v.strftime(DATETIME_PRINT_FORMAT),
|
||||
}
|
237
freqtrade/rpc/api_server/api_v1.py
Normal file
237
freqtrade/rpc/api_server/api_v1.py
Normal file
@@ -0,0 +1,237 @@
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.exceptions import HTTPException
|
||||
|
||||
from freqtrade import __version__
|
||||
from freqtrade.constants import USERPATH_STRATEGIES
|
||||
from freqtrade.data.history import get_datahandler
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.rpc import RPC
|
||||
from freqtrade.rpc.rpc import RPCException
|
||||
|
||||
from .api_models import (AvailablePairs, Balances, BlacklistPayload, BlacklistResponse, Count,
|
||||
Daily, DeleteTrade, ForceBuyPayload, ForceSellPayload, Locks, Logs,
|
||||
PairHistory, PerformanceEntry, Ping, PlotConfig, Profit, ResultMsg, Stats,
|
||||
StatusMsg, StrategyListResponse, StrategyResponse, Version,
|
||||
WhitelistResponse)
|
||||
from .deps import get_config, get_rpc
|
||||
|
||||
|
||||
# Public API, requires no auth.
|
||||
router_public = APIRouter()
|
||||
# Private API, protected by authentication
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router_public.get('/ping', response_model=Ping)
|
||||
def ping():
|
||||
"""simple ping"""
|
||||
return {"status": "pong"}
|
||||
|
||||
|
||||
@router.get('/version', response_model=Version, tags=['info'])
|
||||
def version():
|
||||
""" Bot Version info"""
|
||||
return {"version": __version__}
|
||||
|
||||
|
||||
@router.get('/balance', response_model=Balances, tags=['info'])
|
||||
def balance(rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
|
||||
"""Account Balances"""
|
||||
return rpc._rpc_balance(config['stake_currency'], config.get('fiat_display_currency', ''),)
|
||||
|
||||
|
||||
@router.get('/count', response_model=Count, tags=['info'])
|
||||
def count(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_count()
|
||||
|
||||
|
||||
@router.get('/performance', response_model=List[PerformanceEntry], tags=['info'])
|
||||
def performance(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_performance()
|
||||
|
||||
|
||||
@router.get('/profit', response_model=Profit, tags=['info'])
|
||||
def profit(rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
|
||||
return rpc._rpc_trade_statistics(config['stake_currency'],
|
||||
config.get('fiat_display_currency')
|
||||
)
|
||||
|
||||
|
||||
@router.get('/stats', response_model=Stats, tags=['info'])
|
||||
def stats(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_stats()
|
||||
|
||||
|
||||
@router.get('/daily', response_model=Daily, tags=['info'])
|
||||
def daily(timescale: int = 7, rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
|
||||
return rpc._rpc_daily_profit(timescale, config['stake_currency'],
|
||||
config.get('fiat_display_currency', ''))
|
||||
|
||||
|
||||
# TODO: Missing response model
|
||||
@router.get('/status', tags=['info'])
|
||||
def status(rpc: RPC = Depends(get_rpc)):
|
||||
try:
|
||||
return rpc._rpc_trade_status()
|
||||
except RPCException:
|
||||
return []
|
||||
|
||||
|
||||
# TODO: Missing response model
|
||||
@router.get('/trades', tags=['info', 'trading'])
|
||||
def trades(limit: int = 0, rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_trade_history(limit)
|
||||
|
||||
|
||||
@router.delete('/trades/{tradeid}', response_model=DeleteTrade, tags=['info', 'trading'])
|
||||
def trades_delete(tradeid: int, rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_delete(tradeid)
|
||||
|
||||
|
||||
# TODO: Missing response model
|
||||
@router.get('/edge', tags=['info'])
|
||||
def edge(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_edge()
|
||||
|
||||
|
||||
# TODO: Missing response model
|
||||
@router.get('/show_config', tags=['info'])
|
||||
def show_config(rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
|
||||
return RPC._rpc_show_config(config, rpc._freqtrade.state)
|
||||
|
||||
|
||||
# TODO: Missing response model
|
||||
@router.post('/forcebuy', tags=['trading'])
|
||||
def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)):
|
||||
trade = rpc._rpc_forcebuy(payload.pair, payload.price)
|
||||
|
||||
if trade:
|
||||
return trade.to_json()
|
||||
else:
|
||||
return {"status": f"Error buying pair {payload.pair}."}
|
||||
|
||||
|
||||
@router.post('/forcesell', response_model=ResultMsg, tags=['trading'])
|
||||
def forcesell(payload: ForceSellPayload, rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_forcesell(payload.tradeid)
|
||||
|
||||
|
||||
@router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist'])
|
||||
def blacklist(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_blacklist()
|
||||
|
||||
|
||||
@router.post('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist'])
|
||||
def blacklist_post(payload: BlacklistPayload, rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_blacklist(payload.blacklist)
|
||||
|
||||
|
||||
@router.get('/whitelist', response_model=WhitelistResponse, tags=['info', 'pairlist'])
|
||||
def whitelist(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_whitelist()
|
||||
|
||||
|
||||
@router.get('/locks', response_model=Locks, tags=['info'])
|
||||
def locks(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_locks()
|
||||
|
||||
|
||||
@router.get('/logs', response_model=Logs, tags=['info'])
|
||||
def logs(limit: Optional[int] = None, rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_get_logs(limit)
|
||||
|
||||
|
||||
@router.post('/start', response_model=StatusMsg, tags=['botcontrol'])
|
||||
def start(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_start()
|
||||
|
||||
|
||||
@router.post('/stop', response_model=StatusMsg, tags=['botcontrol'])
|
||||
def stop(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_stop()
|
||||
|
||||
|
||||
@router.post('/stopbuy', response_model=StatusMsg, tags=['botcontrol'])
|
||||
def stop_buy(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_stopbuy()
|
||||
|
||||
|
||||
@router.post('/reload_config', response_model=StatusMsg, tags=['botcontrol'])
|
||||
def reload_config(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_reload_config()
|
||||
|
||||
|
||||
@router.get('/pair_candles', response_model=PairHistory, tags=['candle data'])
|
||||
def pair_candles(pair: str, timeframe: str, limit: Optional[int], rpc=Depends(get_rpc)):
|
||||
return rpc._rpc_analysed_dataframe(pair, timeframe, limit)
|
||||
|
||||
|
||||
@router.get('/pair_history', response_model=PairHistory, tags=['candle data'])
|
||||
def pair_history(pair: str, timeframe: str, timerange: str, strategy: str,
|
||||
config=Depends(get_config)):
|
||||
config = deepcopy(config)
|
||||
config.update({
|
||||
'strategy': strategy,
|
||||
})
|
||||
return RPC._rpc_analysed_history_full(config, pair, timeframe, timerange)
|
||||
|
||||
|
||||
@router.get('/plot_config', response_model=Union[PlotConfig, Dict], tags=['candle data'])
|
||||
def plot_config(rpc: RPC = Depends(get_rpc)):
|
||||
return rpc._rpc_plot_config()
|
||||
|
||||
|
||||
@router.get('/strategies', response_model=StrategyListResponse, tags=['strategy'])
|
||||
def list_strategies(config=Depends(get_config)):
|
||||
directory = Path(config.get(
|
||||
'strategy_path', config['user_data_dir'] / USERPATH_STRATEGIES))
|
||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||
strategies = StrategyResolver.search_all_objects(directory, False)
|
||||
strategies = sorted(strategies, key=lambda x: x['name'])
|
||||
|
||||
return {'strategies': [x['name'] for x in strategies]}
|
||||
|
||||
|
||||
@router.get('/strategy/{strategy}', response_model=StrategyResponse, tags=['strategy'])
|
||||
def get_strategy(strategy: str, rpc: RPC = Depends(get_rpc), config=Depends(get_config)):
|
||||
|
||||
config = deepcopy(config)
|
||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||
try:
|
||||
strategy_obj = StrategyResolver._load_strategy(strategy, config,
|
||||
extra_dir=config.get('strategy_path'))
|
||||
except OperationalException:
|
||||
raise HTTPException(status_code=404, detail='Strategy not found')
|
||||
|
||||
return {
|
||||
'strategy': strategy_obj.get_strategy_name(),
|
||||
'code': strategy_obj.__source__,
|
||||
}
|
||||
|
||||
|
||||
@router.get('/available_pairs', response_model=AvailablePairs, tags=['candle data'])
|
||||
def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Optional[str] = None,
|
||||
config=Depends(get_config)):
|
||||
|
||||
dh = get_datahandler(config['datadir'], config.get('dataformat_ohlcv', None))
|
||||
|
||||
pair_interval = dh.ohlcv_get_available_data(config['datadir'])
|
||||
|
||||
if timeframe:
|
||||
pair_interval = [pair for pair in pair_interval if pair[1] == timeframe]
|
||||
if stake_currency:
|
||||
pair_interval = [pair for pair in pair_interval if pair[0].endswith(stake_currency)]
|
||||
pair_interval = sorted(pair_interval, key=lambda x: x[0])
|
||||
|
||||
pairs = list({x[0] for x in pair_interval})
|
||||
|
||||
result = {
|
||||
'length': len(pairs),
|
||||
'pairs': pairs,
|
||||
'pair_interval': pair_interval,
|
||||
}
|
||||
return result
|
13
freqtrade/rpc/api_server/deps.py
Normal file
13
freqtrade/rpc/api_server/deps.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from .webserver import ApiServer
|
||||
|
||||
|
||||
def get_rpc():
|
||||
return ApiServer._rpc
|
||||
|
||||
|
||||
def get_config():
|
||||
return ApiServer._config
|
||||
|
||||
|
||||
def get_api_config():
|
||||
return ApiServer._config['api_server']
|
27
freqtrade/rpc/api_server/uvicorn_threaded.py
Normal file
27
freqtrade/rpc/api_server/uvicorn_threaded.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import contextlib
|
||||
import threading
|
||||
import time
|
||||
|
||||
import uvicorn
|
||||
|
||||
|
||||
class UvicornServer(uvicorn.Server):
|
||||
"""
|
||||
Multithreaded server - as found in https://github.com/encode/uvicorn/issues/742
|
||||
"""
|
||||
def install_signal_handlers(self):
|
||||
"""
|
||||
In the parent implementation, this starts the thread, therefore we must patch it away here.
|
||||
"""
|
||||
pass
|
||||
|
||||
@contextlib.contextmanager
|
||||
def run_in_thread(self):
|
||||
self.thread = threading.Thread(target=self.run)
|
||||
self.thread.start()
|
||||
while not self.started:
|
||||
time.sleep(1e-3)
|
||||
|
||||
def cleanup(self):
|
||||
self.should_exit = True
|
||||
self.thread.join()
|
110
freqtrade/rpc/api_server/webserver.py
Normal file
110
freqtrade/rpc/api_server/webserver.py
Normal file
@@ -0,0 +1,110 @@
|
||||
import logging
|
||||
from ipaddress import IPv4Address
|
||||
from typing import Any, Dict
|
||||
|
||||
import uvicorn
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler
|
||||
|
||||
from .uvicorn_threaded import UvicornServer
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ApiServer(RPCHandler):
|
||||
|
||||
_rpc: RPC
|
||||
_config: Dict[str, Any] = {}
|
||||
|
||||
def __init__(self, rpc: RPC, config: Dict[str, Any]) -> None:
|
||||
super().__init__(rpc, config)
|
||||
self._server = None
|
||||
|
||||
ApiServer._rpc = rpc
|
||||
ApiServer._config = config
|
||||
api_config = self._config['api_server']
|
||||
|
||||
self.app = FastAPI(title="Freqtrade API",
|
||||
openapi_url='openapi.json' if api_config.get(
|
||||
'enable_openapi') else None,
|
||||
redoc_url=None,
|
||||
)
|
||||
self.configure_app(self.app, self._config)
|
||||
|
||||
self.start_api()
|
||||
|
||||
def cleanup(self) -> None:
|
||||
""" Cleanup pending module resources """
|
||||
if self._server:
|
||||
logger.info("Stopping API Server")
|
||||
self._server.cleanup()
|
||||
|
||||
def send_msg(self, msg: Dict[str, str]) -> None:
|
||||
pass
|
||||
|
||||
def handle_rpc_exception(self, request, exc):
|
||||
logger.exception(f"API Error calling: {exc}")
|
||||
return JSONResponse(
|
||||
status_code=502,
|
||||
content={'error': f"Error querying {request.url.path}: {exc.message}"}
|
||||
)
|
||||
|
||||
def configure_app(self, app: FastAPI, config):
|
||||
from .api_auth import http_basic_or_jwt_token, router_login
|
||||
from .api_v1 import router as api_v1
|
||||
from .api_v1 import router_public as api_v1_public
|
||||
app.include_router(api_v1_public, prefix="/api/v1")
|
||||
|
||||
app.include_router(api_v1, prefix="/api/v1",
|
||||
dependencies=[Depends(http_basic_or_jwt_token)],
|
||||
)
|
||||
app.include_router(router_login, prefix="/api/v1", tags=["auth"])
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=config['api_server'].get('CORS_origins', []),
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.add_exception_handler(RPCException, self.handle_rpc_exception)
|
||||
|
||||
def start_api(self):
|
||||
"""
|
||||
Start API ... should be run in thread.
|
||||
"""
|
||||
rest_ip = self._config['api_server']['listen_ip_address']
|
||||
rest_port = self._config['api_server']['listen_port']
|
||||
|
||||
logger.info(f'Starting HTTP Server at {rest_ip}:{rest_port}')
|
||||
if not IPv4Address(rest_ip).is_loopback:
|
||||
logger.warning("SECURITY WARNING - Local Rest Server listening to external connections")
|
||||
logger.warning("SECURITY WARNING - This is insecure please set to your loopback,"
|
||||
"e.g 127.0.0.1 in config.json")
|
||||
|
||||
if not self._config['api_server'].get('password'):
|
||||
logger.warning("SECURITY WARNING - No password for local REST Server defined. "
|
||||
"Please make sure that this is intentional!")
|
||||
|
||||
if (self._config['api_server'].get('jwt_secret_key', 'super-secret')
|
||||
in ('super-secret, somethingrandom')):
|
||||
logger.warning("SECURITY WARNING - `jwt_secret_key` seems to be default."
|
||||
"Others may be able to log into your bot.")
|
||||
|
||||
logger.info('Starting Local Rest Server.')
|
||||
verbosity = self._config['api_server'].get('verbosity', 'info')
|
||||
uvconfig = uvicorn.Config(self.app,
|
||||
port=rest_port,
|
||||
host=rest_ip,
|
||||
access_log=True if verbosity != 'error' else False,
|
||||
)
|
||||
try:
|
||||
self._server = UvicornServer(uvconfig)
|
||||
self._server.run_in_thread()
|
||||
except Exception:
|
||||
logger.exception("Api server failed to start.")
|
Reference in New Issue
Block a user