stable/freqtrade/rpc/api_server/webserver.py

312 lines
11 KiB
Python
Raw Normal View History

import asyncio
import logging
2020-12-27 09:56:19 +00:00
from ipaddress import IPv4Address
2020-12-27 09:59:17 +00:00
from typing import Any, Dict
2020-12-25 19:07:12 +00:00
2022-04-27 11:13:11 +00:00
import orjson
import uvicorn
2020-12-25 14:50:19 +00:00
from fastapi import Depends, FastAPI
from fastapi.middleware.cors import CORSMiddleware
# Look into alternatives
from janus import Queue as ThreadedQueue
2020-12-26 19:05:27 +00:00
from starlette.responses import JSONResponse
2022-09-18 11:20:36 +00:00
from freqtrade.constants import Config
2020-12-31 19:02:27 +00:00
from freqtrade.exceptions import OperationalException
2021-01-01 18:13:32 +00:00
from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer
2022-11-15 03:27:45 +00:00
from freqtrade.rpc.api_server.ws.message_stream import MessageStream
from freqtrade.rpc.api_server.ws_schemas import WSMessageSchemaType
from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler
logger = logging.getLogger(__name__)
2021-02-22 11:11:27 +00:00
class FTJSONResponse(JSONResponse):
media_type = "application/json"
def render(self, content: Any) -> bytes:
"""
Use rapidjson for responses
Handles NaN and Inf / -Inf in a javascript way by default.
"""
2022-04-27 11:13:11 +00:00
return orjson.dumps(content, option=orjson.OPT_SERIALIZE_NUMPY)
2021-02-22 11:11:27 +00:00
class ApiServer(RPCHandler):
2020-12-31 19:02:27 +00:00
__instance = None
__initialized = False
2020-12-31 10:01:50 +00:00
_rpc: RPC
2021-01-02 14:13:32 +00:00
# Backtesting type: Backtesting
_bt = None
2021-07-10 09:19:23 +00:00
_bt_data = None
_bt_timerange = None
2022-09-18 11:20:36 +00:00
_bt_last_config: Config = {}
2021-01-02 12:12:49 +00:00
_has_rpc: bool = False
2021-01-02 13:18:15 +00:00
_bgtask_running: bool = False
2022-09-18 11:20:36 +00:00
_config: Config = {}
2022-01-22 06:11:59 +00:00
# Exchange - only available in webserver mode.
_exchange = None
# websocket message queue stuff
2022-11-15 03:27:45 +00:00
# _ws_channel_manager = None
# _ws_thread = None
# _ws_loop = None
_message_stream = None
2020-12-31 19:02:27 +00:00
def __new__(cls, *args, **kwargs):
"""
This class is a singleton.
We'll only have one instance of it around.
"""
if ApiServer.__instance is None:
ApiServer.__instance = object.__new__(cls)
ApiServer.__initialized = False
return ApiServer.__instance
2022-09-18 11:20:36 +00:00
def __init__(self, config: Config, standalone: bool = False) -> None:
2021-01-02 13:18:15 +00:00
ApiServer._config = config
2020-12-31 19:02:27 +00:00
if self.__initialized and (standalone or self._standalone):
return
self._standalone: bool = standalone
self._server = None
2022-11-15 03:27:45 +00:00
2022-09-09 16:45:49 +00:00
self._ws_queue = None
2022-11-15 03:27:45 +00:00
self._ws_publisher_task = None
2021-01-02 13:18:15 +00:00
ApiServer.__initialized = True
api_config = self._config['api_server']
2022-11-15 03:27:45 +00:00
# ApiServer._ws_channel_manager = ChannelManager()
self.app = FastAPI(title="Freqtrade API",
docs_url='/docs' if api_config.get('enable_openapi', False) else None,
redoc_url=None,
2021-02-22 11:11:27 +00:00
default_response_class=FTJSONResponse,
)
self.configure_app(self.app, self._config)
self.start_api()
2020-12-31 19:02:27 +00:00
def add_rpc_handler(self, rpc: RPC):
"""
Attach rpc handler
"""
2021-01-02 13:18:15 +00:00
if not self._has_rpc:
2020-12-31 19:02:27 +00:00
ApiServer._rpc = rpc
ApiServer._has_rpc = True
else:
# This should not happen assuming we didn't mess up.
raise OperationalException('RPC Handler already attached.')
def cleanup(self) -> None:
""" Cleanup pending module resources """
2021-06-20 10:54:05 +00:00
ApiServer._has_rpc = False
del ApiServer._rpc
if self._server and not self._standalone:
2020-12-27 09:56:19 +00:00
logger.info("Stopping API Server")
self._server.cleanup()
2022-11-15 03:27:45 +00:00
# if self._ws_thread and self._ws_loop:
# logger.info("Stopping API Server background tasks")
2022-11-15 03:27:45 +00:00
# if self._ws_background_task:
# # Cancel the queue task
# self._ws_background_task.cancel()
2022-11-15 03:27:45 +00:00
# self._ws_thread.join()
2022-11-15 03:27:45 +00:00
# self._ws_thread = None
# self._ws_loop = None
# self._ws_background_task = None
2022-09-06 01:29:07 +00:00
2021-01-02 15:12:10 +00:00
@classmethod
def shutdown(cls):
cls.__initialized = False
del cls.__instance
cls.__instance = None
cls._has_rpc = False
cls._rpc = None
2022-10-24 18:21:17 +00:00
def send_msg(self, msg: Dict[str, Any]) -> None:
2022-09-09 16:45:49 +00:00
if self._ws_queue:
sync_q = self._ws_queue.sync_q
sync_q.put(msg)
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):
2022-09-08 17:25:30 +00:00
from freqtrade.rpc.api_server.api_auth import http_basic_or_jwt_token, router_login
from freqtrade.rpc.api_server.api_backtest import router as api_backtest
2021-01-01 18:13:32 +00:00
from freqtrade.rpc.api_server.api_v1 import router as api_v1
from freqtrade.rpc.api_server.api_v1 import router_public as api_v1_public
from freqtrade.rpc.api_server.api_ws import router as ws_router
2021-01-10 09:31:05 +00:00
from freqtrade.rpc.api_server.web_ui import router_ui
app.include_router(api_v1_public, prefix="/api/v1")
2020-12-25 14:50:19 +00:00
app.include_router(api_v1, prefix="/api/v1",
2020-12-26 07:48:15 +00:00
dependencies=[Depends(http_basic_or_jwt_token)],
2020-12-25 14:50:19 +00:00
)
2021-04-01 05:55:29 +00:00
app.include_router(api_backtest, prefix="/api/v1",
dependencies=[Depends(http_basic_or_jwt_token)],
)
2022-09-08 17:25:30 +00:00
app.include_router(ws_router, prefix="/api/v1")
2020-12-26 07:48:15 +00:00
app.include_router(router_login, prefix="/api/v1", tags=["auth"])
2021-01-10 09:31:05 +00:00
# UI Router MUST be last!
app.include_router(router_ui, prefix='')
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)
2022-11-15 03:27:45 +00:00
app.add_event_handler(
event_type="startup",
func=self._api_startup_event
)
app.add_event_handler(
event_type="shutdown",
func=self._api_shutdown_event
)
2022-11-15 03:27:45 +00:00
async def _api_startup_event(self):
if not ApiServer._message_stream:
ApiServer._message_stream = MessageStream()
2022-09-06 01:29:07 +00:00
2022-11-15 03:27:45 +00:00
if not self._ws_queue:
self._ws_queue = ThreadedQueue()
2022-11-15 03:27:45 +00:00
if not self._ws_publisher_task:
self._ws_publisher_task = asyncio.create_task(
self._publish_messages()
)
2022-11-15 03:27:45 +00:00
async def _api_shutdown_event(self):
if ApiServer._message_stream:
ApiServer._message_stream = None
if self._ws_queue:
self._ws_queue = None
2022-11-15 03:27:45 +00:00
if self._ws_publisher_task:
self._ws_publisher_task.cancel()
2022-11-15 03:27:45 +00:00
async def _publish_messages(self):
"""
Background task that reads messages from the queue and adds them
to the message stream
"""
try:
2022-11-15 03:27:45 +00:00
async_queue = self._ws_queue.async_q
message_stream = ApiServer._message_stream
2022-08-31 01:21:34 +00:00
2022-11-15 03:27:45 +00:00
while message_stream:
message: WSMessageSchemaType = await async_queue.get()
message_stream.publish(message)
2022-11-15 03:27:45 +00:00
# Make sure to throttle how fast we
# publish messages as some clients will be
# slower than others
await asyncio.sleep(0.01)
async_queue.task_done()
finally:
self._ws_queue = None
2022-11-15 03:27:45 +00:00
# def start_message_queue(self):
# if self._ws_thread:
# return
# # Create a new loop, as it'll be just for the background thread
# self._ws_loop = asyncio.new_event_loop()
# # Start the thread
# self._ws_thread = Thread(target=self._ws_loop.run_forever)
# self._ws_thread.start()
# # Finally, submit the coro to the thread
# self._ws_background_task = asyncio.run_coroutine_threadsafe(
# self._broadcast_queue_data(), loop=self._ws_loop)
# async def _broadcast_queue_data(self):
# # Instantiate the queue in this coroutine so it's attached to our loop
# self._ws_queue = ThreadedQueue()
# async_queue = self._ws_queue.async_q
# try:
# while True:
# logger.debug("Getting queue messages...")
# # Get data from queue
# message: WSMessageSchemaType = await async_queue.get()
# logger.debug(f"Found message of type: {message.get('type')}")
# async_queue.task_done()
# # Broadcast it
# await self._ws_channel_manager.broadcast(message)
# except asyncio.CancelledError:
# pass
# # For testing, shouldn't happen when stable
# except Exception as e:
# logger.exception(f"Exception happened in background task: {e}")
# finally:
# # Disconnect channels and stop the loop on cancel
# await self._ws_channel_manager.disconnect_all()
# self._ws_loop.stop()
# # Avoid adding more items to the queue if they aren't
# # going to get broadcasted.
# self._ws_queue = None
def start_api(self):
"""
Start API ... should be run in thread.
"""
2020-12-27 09:56:19 +00:00
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!")
2020-12-27 14:24:49 +00:00
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.")
2020-12-27 09:56:19 +00:00
logger.info('Starting Local Rest Server.')
2021-01-03 06:18:41 +00:00
verbosity = self._config['api_server'].get('verbosity', 'error')
uvconfig = uvicorn.Config(self.app,
2020-12-27 09:56:19 +00:00
port=rest_port,
host=rest_ip,
2021-01-03 06:15:45 +00:00
use_colors=False,
log_config=None,
2020-12-29 16:23:28 +00:00
access_log=True if verbosity != 'error' else False,
ws_ping_interval=None # We do this explicitly ourselves
2020-12-29 16:23:28 +00:00
)
2020-12-27 09:56:19 +00:00
try:
self._server = UvicornServer(uvconfig)
if self._standalone:
self._server.run()
else:
2022-11-15 03:27:45 +00:00
# self.start_message_queue()
self._server.run_in_thread()
2020-12-27 09:56:19 +00:00
except Exception:
logger.exception("Api server failed to start.")