Rename api_server2 module to apiserver

This commit is contained in:
Matthias
2020-12-31 11:01:50 +01:00
parent eb20f6e7d0
commit b2ab553a31
10 changed files with 25 additions and 27 deletions

View File

@@ -0,0 +1,2 @@
# flake8: noqa: F401
from .webserver import ApiServer

View 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}

View 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),
}

View 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

View 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']

View 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()

View 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.")