From 6cf66cf5bf32fa20f20808345733ee64f39a7228 Mon Sep 17 00:00:00 2001 From: Redume Date: Sun, 6 Oct 2024 20:55:06 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9D=D0=B0=D0=BF=D0=B8=D1=81=D0=B0=D0=BB=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BF=D0=B8=D1=82=D0=BE=D0=BD=D0=B5=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B3=D1=80=D0=B0?= =?UTF-8?q?=D1=84=D0=B8=D0=BA=D0=BE=D0=B2.=20=D0=91=D0=B5=D0=B7=20=D0=B8?= =?UTF-8?q?=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=81=D1=82=D0=BE=D1=80=D0=BE=D0=BD=D0=BD=D0=B8?= =?UTF-8?q?=D1=85=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chart/.gitignore | 1 - chart/chart.js | 126 ------------------------- chart/chart.py | 95 +++++++++++++++++++ chart/main.js | 35 ------- chart/package-lock.json | 192 -------------------------------------- chart/package.json | 24 ----- chart/requirements.txt | 6 ++ config_sample.yaml | 1 - server/routes/getChart.js | 38 -------- 9 files changed, 101 insertions(+), 417 deletions(-) delete mode 100644 chart/.gitignore delete mode 100644 chart/chart.js create mode 100644 chart/chart.py delete mode 100644 chart/main.js delete mode 100644 chart/package-lock.json delete mode 100644 chart/package.json create mode 100644 chart/requirements.txt delete mode 100644 server/routes/getChart.js diff --git a/chart/.gitignore b/chart/.gitignore deleted file mode 100644 index 40b878d..0000000 --- a/chart/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules/ \ No newline at end of file diff --git a/chart/chart.js b/chart/chart.js deleted file mode 100644 index 096eef8..0000000 --- a/chart/chart.js +++ /dev/null @@ -1,126 +0,0 @@ -const ChartJSImage = require('chart.js-image'); -const pool = require('../shared/database/src/postgresql.js'); -const fs = require('fs'); -const axios = require('axios'); -const logger = require('../shared/logger/src/main.js'); - -/** - * Graph generation - * @param {String} from_currency - The currency that is being converted - * @param {String} conv_currency - The currency to be converted into - * @param {String} start_date - Start date of the period - * @param {String} end_date - End date of the period - * @returns {Promise} - */ -async function gen_chart(from_currency, conv_currency, start_date, end_date) { - const data = await pool.query( - 'SELECT date, rate FROM currency WHERE ' + - '(date BETWEEN $3 AND $4) AND from_currency = $1 AND conv_currency = $2 ORDER BY date ', - [ - from_currency.toUpperCase(), - conv_currency.toUpperCase(), - start_date, - end_date, - ], - ); - - if (!data['rows'][0]) return 'Missing data'; - - const date = []; - const rate = []; - - for (let i = 0; i < data.rows.length; i++) { - date.push(data.rows[i].date.toLocaleDateString()); - rate.push(data.rows[i].rate); - } - - const chart = ChartJSImage() - .chart({ - type: 'line', - options: { - title: { - display: true, - text: `${from_currency} / ${conv_currency}`, - }, - scales: [ - { - yAxes: [ - { - ticks: { - beginAtZero: false, - }, - }, - ], - }, - ], - }, - data: { - labels: date, - datasets: [ - { - borderColor: - rate[0] < rate[rate.length - 1] - ? 'rgb(24, 218, 39)' - : 'rgb(243, 85, 50)', - backgroundColor: - rate[0] < rate[rate.length - 1] - ? 'rgb(36, 175, 47)' - : 'rgb(218, 56, 24)', - data: rate, - borderWidth: 2, - }, - ], - }, - scales: { - xAxes: [ - { - scaleLabel: { - display: true, - labelString: 'Day', - }, - }, - ], - yAxes: [ - { - stacked: false, - scaleLabel: { - display: true, - labelString: 'Rate', - }, - }, - ], - }, - }) - .width(1000) - .height(1000); - - logger.debug(chart.toURL()); - - return chart.toURL(); -} - -/** - * Saving a graph to a file - * @param {String} url - URL (or Buffer) to the chart - * @param {String} filename - filename - */ -function save_chart(url, filename) { - if (!fs.existsSync('../charts')) fs.mkdirSync('../charts'); - if (!url.startsWith('https://')) - throw new Error('The passed parameter is not a URL'); - - logger.info( - `The schedule has been saved. The path of the graph 'chart/${filename}'`, - ); - axios({ url, responseType: 'stream' }).then( - (response) => - new Promise((resolve, reject) => { - response.data - .pipe(fs.createWriteStream(`../charts/${filename}`)) - .on('finish', () => resolve()) - .on('error', (e) => reject(e)); - }), - ); -} - -module.exports = { gen_chart, save_chart }; diff --git a/chart/chart.py b/chart/chart.py new file mode 100644 index 0000000..d273fb9 --- /dev/null +++ b/chart/chart.py @@ -0,0 +1,95 @@ +import os +import psycopg2 +import uvicorn +import yaml +import matplotlib.pyplot as plt +from fastapi import FastAPI, Response, status +from psycopg2.extras import DictCursor +from starlette.staticfiles import StaticFiles + +app = FastAPI() + +config = yaml.safe_load(open('../config.yaml')) + +con = psycopg2.connect(host=config['database']['host'], + user=config['database']['user'], + password=config['database']['password'], + database=config['database']['name'], + port=config['database']['port']) + +cur = con.cursor(cursor_factory=DictCursor) +con.autocommit = True + +app.mount('/static/charts', StaticFiles(directory='../charts/')) + + +@app.get("/api/getChart/") +async def get_chart(response: Response, + from_currency: str, conv_currency: str, + start_date: str, end_date: str): + """ + :param response: + :param from_currency: The currency to convert from + :type from_currency: str + :param conv_currency: The currency to be converted into + :type conv_currency: str + :param start_date: The start date of the schedule period + :type start_date: str + :param end_date: The last date of the schedule period + :type end_date: str + :return: + """ + cur.execute('SELECT date, rate FROM currency WHERE (date BETWEEN %s AND %s) ' + 'AND from_currency = %s AND conv_currency = %s ORDER BY date', + [ + start_date, end_date, + from_currency.upper(), conv_currency.upper() + ]) + con.commit() + data = cur.fetchall() + + if not data: + response.status_code = status.HTTP_404_NOT_FOUND + return {'message': 'No data found', 'status_code': status.HTTP_404_NOT_FOUND} + + date, rate = [], [] + + for i in range(len(data)): + date.append(str(data[i][0])) + rate.append(data[i][1]) + + if rate[0] < rate[-1]: + plt.plot(date, rate, color='green') + elif rate[0] > rate[-1]: + plt.plot(date, rate, color='red') + else: + plt.plot(date, rate, color='grey') + + plt.xlabel('Date') + plt.ylabel('Rate') + + fig = plt.gcf() + fig.set_size_inches(18.5, 9.5) + + if not os.path.exists('../charts'): + os.mkdir('../charts') + + fig.savefig(f'../charts/{from_currency}-{conv_currency}.png') + fig.clear() + + return {'message': f'./static/charts/{from_currency}-{conv_currency}.png', + 'status_code': status.HTTP_201_CREATED, + } + + +if __name__ == '__main__': + uvicorn.run(app, + host=config['server']['host'], + port=3030, + ssl_keyfile=config['server']['ssl']['private_key'] + if config['server']['ssl']['work'] + else None, + ssl_certfile=config['server']['ssl']['cert'] + if config['server']['ssl']['work'] + else None + ) diff --git a/chart/main.js b/chart/main.js deleted file mode 100644 index 2891f37..0000000 --- a/chart/main.js +++ /dev/null @@ -1,35 +0,0 @@ -const fs = require('fs'); -const config = require('../shared/config/src/main.js')(); -const schedule = require('node-schedule'); - -function main() { - if (config['currency']['chart']['max_size'] === 0) return; - - fs.readdir('../charts/', (err, files) => { - if (err) return; - - let folderSize = 0; - - for (const file of files) { - try { - const fileSize = - fs.statSync(`../charts/${file}`)['size'] / 1024; - folderSize += fileSize; - - if (folderSize > config['currency']['chart']['max_size']) { - for (let i = 0; i < files.length; i++) { - fs.unlinkSync(`../charts/${files[i]}`); - } - } - } catch { - return; - } - } - }); -} - -schedule.scheduleJob('9 20 * * *', async function () { - main(); -}); - -main(); diff --git a/chart/package-lock.json b/chart/package-lock.json deleted file mode 100644 index cf900c9..0000000 --- a/chart/package-lock.json +++ /dev/null @@ -1,192 +0,0 @@ -{ - "name": "chart", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "chart", - "version": "1.0.0", - "license": "GPL-3.0-or-later", - "dependencies": { - "axios": "^1.7.3", - "chart.js-image": "^6.1.3", - "node-schedule": "^2.1.1" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.3.tgz", - "integrity": "sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/axios/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/chart.js-image": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/chart.js-image/-/chart.js-image-6.1.3.tgz", - "integrity": "sha512-K+h0dc/Wf/Dk5CWKrV7xxS7ozONCiQ73XL+QStBxUpgABzSblJwQe/R6X1RWxY2Z/G8OhKsPFB0HC7bwiCmB2w==", - "license": "MIT", - "dependencies": { - "javascript-stringify": "2.0.1", - "node-fetch": "2.6.0" - }, - "engines": { - "node": ">12" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cron-parser": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", - "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", - "license": "MIT", - "dependencies": { - "luxon": "^3.2.1" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/javascript-stringify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.0.1.tgz", - "integrity": "sha512-yV+gqbd5vaOYjqlbk16EG89xB5udgjqQF3C5FAORDg4f/IS1Yc5ERCv5e/57yBcfJYw05V5JyIXabhwb75Xxow==", - "license": "MIT" - }, - "node_modules/long-timeout": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", - "integrity": "sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==", - "license": "MIT" - }, - "node_modules/luxon": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", - "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-fetch": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", - "license": "MIT", - "engines": { - "node": "4.x || >=6.0.0" - } - }, - "node_modules/node-schedule": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.1.1.tgz", - "integrity": "sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==", - "license": "MIT", - "dependencies": { - "cron-parser": "^4.2.0", - "long-timeout": "0.1.1", - "sorted-array-functions": "^1.3.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/sorted-array-functions": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz", - "integrity": "sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==", - "license": "MIT" - } - } -} diff --git a/chart/package.json b/chart/package.json deleted file mode 100644 index f486a21..0000000 --- a/chart/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "chart", - "version": "1.0.0", - "main": "chart.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/redume/kekkai.git" - }, - "author": "Redume", - "license": "GPL-3.0-or-later", - "bugs": { - "url": "https://github.com/redume/kekkai/issues" - }, - "homepage": "https://github.com/redume/kekkai#readme", - "description": "A service for creating graphs", - "dependencies": { - "axios": "^1.7.3", - "chart.js-image": "^6.1.3", - "node-schedule": "^2.1.1" - } -} diff --git a/chart/requirements.txt b/chart/requirements.txt new file mode 100644 index 0000000..588d804 --- /dev/null +++ b/chart/requirements.txt @@ -0,0 +1,6 @@ +matplotlib~=3.9.1 +PyYAML~=6.0.1 +uvicorn~=0.29.0 +fastapi[standard]~=0.111.0 +psycopg2~=2.9.9 +starlette~=0.37.2 \ No newline at end of file diff --git a/config_sample.yaml b/config_sample.yaml index 8a00818..cf3ded2 100644 --- a/config_sample.yaml +++ b/config_sample.yaml @@ -19,7 +19,6 @@ server: currency: chart: save: false # Enable or disable saving graphs to an image (Boolean) - max_size: 1 #kb collecting: fiat: true # Turn off or turn on the collection of the fiat currency rate [Boolean] crypto: false # Turn off or turn on the collection of the cryptocurrency rate [Boolean] diff --git a/server/routes/getChart.js b/server/routes/getChart.js deleted file mode 100644 index 8f86ad5..0000000 --- a/server/routes/getChart.js +++ /dev/null @@ -1,38 +0,0 @@ -const config = require('../../shared/config/src/main.js')(); -const chart = require('../../chart/chart.js'); - -module.exports = async function GetChartRoute(fastify) { - fastify.get('/api/getChart/', async function (req, res) { - const query = req.query; - if (!query['from_currency'] || !query['conv_currency']) { - return res.status(400).send({ - status: 400, - message: 'The from_currency and conv_currency fields are required', - }); - } - if (!query['start_date'] || !query['end_date']) - return res.status(400).send({ - status: 400, - message: 'start_date and end_date is required', - }); - - const charts = await chart.gen_chart( - query['from_currency'], - query['conv_currency'], - query['start_date'], - query['end_date'], - ); - - if (config['currency']['chart']['save']) - chart.save_chart( - charts, - `${query['from_currency']} ${query['conv_currency']} ` + - `(${query['start_date']} - ${query['end_date']}).png`, - ); - - return res.status(200).send({ - status: 200, - message: charts, - }); - }); -} \ No newline at end of file