├── .env.example ├── .gitignore ├── LICENSE ├── Pipfile ├── README.md ├── fastdash ├── __init__.py ├── api │ ├── __init__.py │ ├── dependencies │ │ ├── __init__.py │ │ └── fastdash.py │ ├── errors │ │ ├── __init__.py │ │ └── http_error.py │ └── routes │ │ ├── __init__.py │ │ ├── api.py │ │ └── fastdash.py ├── core │ ├── __init__.py │ ├── config.py │ ├── events.py │ └── logging.py └── main.py ├── setup.cfg └── setup.py /.env.example: -------------------------------------------------------------------------------- 1 | SECRET_KEY=secret 2 | DEBUG=True -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | 3 | *.pyc 4 | __pycache__/ 5 | 6 | instance/ 7 | 8 | .pytest_cache/ 9 | .coverage 10 | htmlcov/ 11 | 12 | dist/ 13 | build/ 14 | *.egg-info/ 15 | 16 | .vscode/ 17 | 18 | .env -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Chojan Shang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | # url = "https://pypi.tuna.tsinghua.edu.cn/simple" 5 | verify_ssl = true 6 | 7 | [dev-packages] 8 | black = "*" 9 | bump2version = "*" 10 | 11 | [packages] 12 | fastdash = {path = ".",editable = true} 13 | fastapi = "*" 14 | loguru = "*" 15 | uvicorn = "*" 16 | 17 | [requires] 18 | python_version = "3.7" 19 | 20 | [pipenv] 21 | allow_prereleases = true 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FastDash 2 | 3 | > There are countless ways to do one thing, I just choose the one that works for myself. 4 | 5 | _**Note: Just a demo, no production, no guarantee.**_ 6 | 7 | `FastDash` is just `FastAPI` and `DashBoard`. 8 | Obviously another host health dashboard, working with FastAPI. 9 | 10 | ## TODO 11 | 12 | - [ ] API 13 | - [ ] More and more-fine-grained API. 14 | - [ ] Smaller and reasonable root API. 15 | - [ ] UI 16 | - [ ] Basic display. 17 | - [ ] Beautiful charts. 18 | - [ ] Others 19 | - [ ] Reasonable data storage. 20 | - [ ] Simple performance / availability analysis. 21 | 22 | ## Usage 23 | 24 | You need to have Python & Pipenv. 25 | 26 | ```shell 27 | git clone git@github.com:psiace/fastdash.git 28 | pipenv install 29 | # for developer, run `pipenv install --dev` 30 | pipenv shell 31 | uvicorn fastdash.main:app --reload 32 | # or `pipenv run uvicorn fastdash.main:app --reload` 33 | ``` 34 | 35 | Now, you can see all infomation at `http://127.0.0.1:8000/api/fastdash`. 36 | 37 | All routes are available on `/docs` or `/redoc` paths with Swagger or ReDoc. 38 | 39 | ## Contact 40 | 41 | Chojan Shang - [@PsiACE](https://github.com/psiace) - 42 | 43 | Project Link: [https://github.com/psiace/fastdash](https://github.com/psiace/fastdash) 44 | 45 | ## License 46 | 47 | This project is licensed under the terms of the [MIT license](./LICENSE). 48 | 49 | ## Credits 50 | 51 | - [pyDash](https://gitlab.com/k3oni/pydash): A small web-based monitoring dashboard for your linux pc/server writen in Python and Django + Chart.js. 52 | - [FastAPI](https://github.com/tiangolo/fastapi): A modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. 53 | - [FastAPI RealWorld](https://github.com/nsidnev/fastapi-realworld-example-app): Backend logic implementation for [RealWorld](https://github.com/gothinkster/realworld) with awesome FastAPI. I learned a lot, including the code. 54 | -------------------------------------------------------------------------------- /fastdash/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Top-level package for FastDash.""" 4 | 5 | __author__ = """Chojan Shang""" 6 | __email__ = "psiace@outlook.com" 7 | __version__ = "0.1.0" 8 | -------------------------------------------------------------------------------- /fastdash/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psiace-archive/fastdash/7aab3223d06883751f687df7fb477c2e0fe92869/fastdash/api/__init__.py -------------------------------------------------------------------------------- /fastdash/api/dependencies/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psiace-archive/fastdash/7aab3223d06883751f687df7fb477c2e0fe92869/fastdash/api/dependencies/__init__.py -------------------------------------------------------------------------------- /fastdash/api/dependencies/fastdash.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """fastdash module.""" 4 | 5 | # The code cut from pyDash(https://gitlab.com/ahudsonelectric/pydash) 6 | # 7 | # The MIT License (MIT) 8 | # 9 | # Copyright (c) 2014 Florian Neagu - michaelneagu@gmail.com - https://github.com/k3oni/ 10 | # 11 | # Permission is hereby granted, free of charge, to any person obtaining a copy 12 | # of this software and associated documentation files (the "Software"), to deal 13 | # in the Software without restriction, including without limitation the rights 14 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | # copies of the Software, and to permit persons to whom the Software is 16 | # furnished to do so, subject to the following conditions: 17 | # 18 | # The above copyright notice and this permission notice shall be included in all 19 | # copies or substantial portions of the Software. 20 | # 21 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | # SOFTWARE. 28 | 29 | import multiprocessing 30 | import os 31 | import platform 32 | from datetime import timedelta 33 | 34 | 35 | def get_uptime(): 36 | """ 37 | Get uptime 38 | """ 39 | try: 40 | with open("/proc/uptime", "r") as f: 41 | uptime_seconds = float(f.readline().split()[0]) 42 | uptime_time = str(timedelta(seconds=uptime_seconds)) 43 | data = uptime_time.split(".", 1)[0] 44 | 45 | except Exception as err: 46 | data = str(err) 47 | 48 | return data 49 | 50 | 51 | def get_ipaddress(): 52 | """ 53 | Get the IP Address 54 | """ 55 | data = [] 56 | try: 57 | eth = os.popen("ip addr | grep LOWER_UP | awk '{print $2}'") 58 | iface = eth.read().strip().replace(":", "").split("\n") 59 | eth.close() 60 | del iface[0] 61 | 62 | for i in iface: 63 | pipe = os.popen( 64 | "ip addr show " 65 | + i 66 | + "| awk '{if ($2 == \"forever\"){!$2} else {print $2}}'" 67 | ) 68 | data1 = pipe.read().strip().split("\n") 69 | pipe.close() 70 | if len(data1) == 2: 71 | data1.append("unavailable") 72 | if len(data1) == 3: 73 | data1.append("unavailable") 74 | data1[0] = i 75 | data.append(data1) 76 | 77 | ips = {"interface": iface, "itfip": data} 78 | 79 | data = ips 80 | 81 | except Exception as err: 82 | data = str(err) 83 | 84 | return data 85 | 86 | 87 | def get_cpus(): 88 | """ 89 | Get the number of CPUs and model/type 90 | """ 91 | try: 92 | pipe = os.popen("cat /proc/cpuinfo |" + "grep 'model name'") 93 | data = pipe.read().strip().split(":")[-1] 94 | pipe.close() 95 | 96 | if not data: 97 | pipe = os.popen("cat /proc/cpuinfo |" + "grep 'Processor'") 98 | data = pipe.read().strip().split(":")[-1] 99 | pipe.close() 100 | 101 | cpus = multiprocessing.cpu_count() 102 | 103 | data = {"cpus": cpus, "type": data} 104 | 105 | except Exception as err: 106 | data = str(err) 107 | 108 | return data 109 | 110 | 111 | def get_users(): 112 | """ 113 | Get the current logged in users 114 | """ 115 | try: 116 | pipe = os.popen("who |" + "awk '{print $1, $2, $6}'") 117 | data = pipe.read().strip().split("\n") 118 | pipe.close() 119 | 120 | if data == [""]: 121 | data = None 122 | else: 123 | data = [i.split(None, 3) for i in data] 124 | 125 | except Exception as err: 126 | data = str(err) 127 | 128 | return data 129 | 130 | 131 | def get_traffic(request): 132 | """ 133 | Get the traffic for the specified interface 134 | """ 135 | try: 136 | pipe = os.popen( 137 | "cat /proc/net/dev |" + "grep " + request + "| awk '{print $1, $9}'" 138 | ) 139 | data = pipe.read().strip().split(":", 1)[-1] 140 | pipe.close() 141 | 142 | if not data[0].isdigit(): 143 | pipe = os.popen( 144 | "cat /proc/net/dev |" + "grep " + request + "| awk '{print $2, $10}'" 145 | ) 146 | data = pipe.read().strip().split(":", 1)[-1] 147 | pipe.close() 148 | 149 | data = data.split() 150 | 151 | traffic_in = int(data[0]) 152 | traffic_out = int(data[1]) 153 | 154 | all_traffic = {"traffic_in": traffic_in, "traffic_out": traffic_out} 155 | 156 | data = all_traffic 157 | 158 | except Exception as err: 159 | data = str(err) 160 | 161 | return data 162 | 163 | 164 | def get_platform(): 165 | """ 166 | Get the OS name, hostname and kernel 167 | """ 168 | try: 169 | osname = " ".join(platform.linux_distribution()) 170 | uname = platform.uname() 171 | 172 | if osname == " ": 173 | osname = uname[0] 174 | 175 | data = {"osname": osname, "hostname": uname[1], "kernel": uname[2]} 176 | 177 | except Exception as err: 178 | data = str(err) 179 | 180 | return data 181 | 182 | 183 | def get_disk(): 184 | """ 185 | Get disk usage 186 | """ 187 | try: 188 | pipe = os.popen( 189 | "df -Ph | " 190 | + "grep -v Filesystem | " 191 | + "awk '{print $1, $2, $3, $4, $5, $6}'" 192 | ) 193 | data = pipe.read().strip().split("\n") 194 | pipe.close() 195 | 196 | data = [i.split(None, 6) for i in data] 197 | 198 | except Exception as err: 199 | data = str(err) 200 | 201 | return data 202 | 203 | 204 | def get_disk_rw(): 205 | """ 206 | Get the disk reads and writes 207 | """ 208 | try: 209 | pipe = os.popen("cat /proc/partitions | grep -v 'major' | awk '{print $4}'") 210 | data = pipe.read().strip().split("\n") 211 | pipe.close() 212 | 213 | rws = [] 214 | for i in data: 215 | if i.isalpha(): 216 | pipe = os.popen( 217 | "cat /proc/diskstats | grep -w '" + i + "'|awk '{print $4, $8}'" 218 | ) 219 | rw = pipe.read().strip().split() 220 | pipe.close() 221 | 222 | rws.append([i, rw[0], rw[1]]) 223 | 224 | if not rws: 225 | pipe = os.popen( 226 | "cat /proc/diskstats | grep -w '" + data[0] + "'|awk '{print $4, $8}'" 227 | ) 228 | rw = pipe.read().strip().split() 229 | pipe.close() 230 | 231 | rws.append([data[0], rw[0], rw[1]]) 232 | 233 | data = rws 234 | 235 | except Exception as err: 236 | data = str(err) 237 | 238 | return data 239 | 240 | 241 | def get_mem(): 242 | """ 243 | Get memory usage 244 | """ 245 | try: 246 | pipe = os.popen("free -tm | " + "grep 'Mem' | " + "awk '{print $2,$4,$6,$7}'") 247 | data = pipe.read().strip().split() 248 | pipe.close() 249 | 250 | allmem = int(data[0]) 251 | freemem = int(data[1]) 252 | buffers = int(data[2]) 253 | cachedmem = int(data[3]) 254 | 255 | # Memory in buffers + cached is actually available, so we count it 256 | # as free. See http://www.linuxatemyram.com/ for details 257 | freemem += buffers + cachedmem 258 | 259 | percent = 100 - ((freemem * 100) / allmem) 260 | usage = allmem - freemem 261 | 262 | mem_usage = { 263 | "usage": usage, 264 | "buffers": buffers, 265 | "cached": cachedmem, 266 | "free": freemem, 267 | "percent": percent, 268 | } 269 | 270 | data = mem_usage 271 | 272 | except Exception as err: 273 | data = str(err) 274 | 275 | return data 276 | 277 | 278 | def get_cpu_usage(): 279 | """ 280 | Get the CPU usage and running processes 281 | """ 282 | try: 283 | pipe = os.popen("ps aux --sort -%cpu,-rss") 284 | data = pipe.read().strip().split("\n") 285 | pipe.close() 286 | 287 | usage = [i.split(None, 10) for i in data] 288 | del usage[0] 289 | 290 | total_usage = [] 291 | 292 | for element in usage: 293 | usage_cpu = element[2] 294 | total_usage.append(usage_cpu) 295 | 296 | total_usage = sum(float(i) for i in total_usage) 297 | 298 | total_free = (100 * int(get_cpus()["cpus"])) - float(total_usage) 299 | 300 | cpu_used = {"free": total_free, "used": float(total_usage), "all": usage} 301 | 302 | data = cpu_used 303 | 304 | except Exception as err: 305 | data = str(err) 306 | 307 | return data 308 | 309 | 310 | def get_load(): 311 | """ 312 | Get load average 313 | """ 314 | try: 315 | data = os.getloadavg()[0] 316 | except Exception as err: 317 | data = str(err) 318 | 319 | return data 320 | 321 | 322 | def get_netstat(): 323 | """ 324 | Get ports and applications 325 | """ 326 | try: 327 | pipe = os.popen( 328 | "ss -tnp | grep ESTAB | awk '{print $4, $5}'| sed 's/::ffff://g' | awk -F: '{print $1, $2}' " 329 | "| awk 'NF > 0' | sort -n | uniq -c" 330 | ) 331 | data = pipe.read().strip().split("\n") 332 | pipe.close() 333 | 334 | data = [i.split(None, 4) for i in data] 335 | 336 | except Exception as err: 337 | data = str(err) 338 | 339 | return data 340 | -------------------------------------------------------------------------------- /fastdash/api/errors/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psiace-archive/fastdash/7aab3223d06883751f687df7fb477c2e0fe92869/fastdash/api/errors/__init__.py -------------------------------------------------------------------------------- /fastdash/api/errors/http_error.py: -------------------------------------------------------------------------------- 1 | from fastapi import HTTPException 2 | from starlette.requests import Request 3 | from starlette.responses import JSONResponse 4 | 5 | 6 | async def http_error_handler(_: Request, exc: HTTPException) -> JSONResponse: 7 | return JSONResponse({"errors": [exc.detail]}, status_code=exc.status_code) 8 | -------------------------------------------------------------------------------- /fastdash/api/routes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psiace-archive/fastdash/7aab3223d06883751f687df7fb477c2e0fe92869/fastdash/api/routes/__init__.py -------------------------------------------------------------------------------- /fastdash/api/routes/api.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from fastdash.api.routes import fastdash 4 | 5 | router = APIRouter() 6 | router.include_router(fastdash.router, tags=["fastdash"], prefix="/fastdash") 7 | -------------------------------------------------------------------------------- /fastdash/api/routes/fastdash.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from fastdash.api.dependencies.fastdash import * 3 | 4 | router = APIRouter() 5 | 6 | 7 | @router.get( 8 | "", name="fastdash:get-your-system-info", 9 | ) 10 | async def fastdash_info(): 11 | ipaddress = get_ipaddress() 12 | platform = get_platform() 13 | uptime = get_uptime() 14 | cpu = get_cpu_usage() 15 | cores = get_cpus() 16 | memory = get_mem() 17 | disk = get_disk() 18 | diskrw = get_disk_rw() 19 | loadavg = get_load() 20 | # Get traffic 21 | ip = ipaddress["interface"][0] 22 | traffic = get_traffic(ip) 23 | users = get_users() 24 | netstat = get_netstat() 25 | 26 | return { 27 | "ipaddress": ipaddress, 28 | "platform": platform, 29 | "uptime": uptime, 30 | "cpu": cpu, 31 | "cores": cores, 32 | "memory": memory, 33 | "disk": disk, 34 | "diskrw": diskrw, 35 | "loadavg": loadavg, 36 | "traffic": traffic, 37 | "users": users, 38 | "netstat": netstat, 39 | } 40 | -------------------------------------------------------------------------------- /fastdash/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psiace-archive/fastdash/7aab3223d06883751f687df7fb477c2e0fe92869/fastdash/core/__init__.py -------------------------------------------------------------------------------- /fastdash/core/config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | from typing import List 4 | 5 | from loguru import logger 6 | from starlette.config import Config 7 | from starlette.datastructures import CommaSeparatedStrings, Secret 8 | from fastdash.core.logging import InterceptHandler 9 | 10 | API_PREFIX = "/api" 11 | 12 | VERSION = "0.1.0" 13 | 14 | config = Config(".env") 15 | 16 | DEBUG: bool = config("DEBUG", cast=bool, default=False) 17 | 18 | SECRET_KEY: Secret = config("SECRET_KEY", cast=Secret) 19 | 20 | PROJECT_NAME: str = config("PROJECT_NAME", default="FastDash") 21 | 22 | ALLOWED_HOSTS: List[str] = config( 23 | "ALLOWED_HOSTS", cast=CommaSeparatedStrings, default="" 24 | ) 25 | 26 | # logging configuration 27 | 28 | LOGGING_LEVEL = logging.DEBUG if DEBUG else logging.INFO 29 | logging.basicConfig( 30 | handlers=[InterceptHandler(level=LOGGING_LEVEL)], level=LOGGING_LEVEL 31 | ) 32 | logger.configure(handlers=[{"sink": sys.stderr, "level": LOGGING_LEVEL}]) 33 | -------------------------------------------------------------------------------- /fastdash/core/events.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | from fastapi import FastAPI 4 | from loguru import logger 5 | 6 | 7 | def create_start_app_handler(app: FastAPI) -> Callable: # type: ignore 8 | async def start_app() -> None: 9 | logger.info("Do noting.") 10 | 11 | return start_app 12 | 13 | 14 | def create_stop_app_handler(app: FastAPI) -> Callable: # type: ignore 15 | @logger.catch 16 | async def stop_app() -> None: 17 | logger.info("Do noting.") 18 | 19 | return stop_app 20 | -------------------------------------------------------------------------------- /fastdash/core/logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from loguru import logger 4 | 5 | 6 | class InterceptHandler(logging.Handler): 7 | def emit(self, record: logging.LogRecord) -> None: # pragma: no cover 8 | logger_opt = logger.opt(depth=7, exception=record.exc_info) 9 | logger_opt.log(record.levelname, record.getMessage()) 10 | -------------------------------------------------------------------------------- /fastdash/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Main module.""" 4 | # -*- coding: utf-8 -*- 5 | 6 | """Main module.""" 7 | 8 | from fastapi import FastAPI 9 | from starlette.exceptions import HTTPException 10 | from starlette.middleware.cors import CORSMiddleware 11 | 12 | from fastdash.api.errors.http_error import http_error_handler 13 | from fastdash.core.events import create_start_app_handler, create_stop_app_handler 14 | from fastdash.core.config import ALLOWED_HOSTS, API_PREFIX, DEBUG, PROJECT_NAME, VERSION 15 | from fastdash.api.routes.api import router as api_router 16 | 17 | 18 | def create_application() -> FastAPI: 19 | application = FastAPI(title=PROJECT_NAME, debug=DEBUG, version=VERSION) 20 | 21 | application.add_middleware( 22 | CORSMiddleware, 23 | allow_origins=ALLOWED_HOSTS or ["*"], 24 | allow_credentials=True, 25 | allow_methods=["*"], 26 | allow_headers=["*"], 27 | ) 28 | 29 | application.add_event_handler("startup", create_start_app_handler(application)) 30 | application.add_event_handler("shutdown", create_stop_app_handler(application)) 31 | 32 | application.add_exception_handler(HTTPException, http_error_handler) 33 | 34 | application.include_router(api_router, prefix=API_PREFIX) 35 | 36 | return application 37 | 38 | 39 | app = create_application() 40 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.1.0 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | search = version='{current_version}' 8 | replace = version='{new_version}' 9 | 10 | [bumpversion:file:fastdash/__init__.py] 11 | search = __version__ = '{current_version}' 12 | replace = __version__ = '{new_version}' 13 | 14 | [bumpversion:file:fastdash/core/config.py.py] 15 | search = VERSION = '{current_version}' 16 | replace = VERSION = '{new_version}' -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Setup settings.""" 4 | 5 | from setuptools import find_packages, setup 6 | 7 | setup( 8 | name="fastdash", 9 | version="0.1.0", 10 | packages=find_packages(), 11 | include_package_data=True, 12 | zip_safe=False, 13 | install_requires=[], 14 | ) 15 | --------------------------------------------------------------------------------