From 91c9c2ba7c9a93ea783fe82ff3072a130d32f548 Mon Sep 17 00:00:00 2001 From: MarkusJoe Date: Thu, 2 Jun 2022 18:50:09 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=A0=E9=99=A4=E5=8E=86=E5=8F=B2=E7=89=88?= =?UTF-8?q?=E6=9C=AC,=20=E5=88=9D=E5=A7=8B=E5=8C=96=E4=BB=93=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 16 +++ LICENSE | 201 ++++++++++++++++++++++++++++ Procfile | 1 + README.md | 67 ++++++++++ app/__init__.py | 54 ++++++++ app/api/__init__.py | 19 +++ app/api/auth.py | 26 ++++ app/api/errors.py | 44 ++++++ app/api/views.py | 172 ++++++++++++++++++++++++ app/config.py | 71 ++++++++++ app/db/__init__.py | 28 ++++ app/db/db.py | 151 +++++++++++++++++++++ app/main/__init__.py | 19 +++ app/main/errors.py | 31 +++++ app/main/views.py | 50 +++++++ app/static/favicon.ico | Bin 0 -> 67646 bytes app/static/index.css | 15 +++ app/static/request.js | 10 ++ app/templates/index.html | 73 ++++++++++ app/templates/view.html | 11 ++ app/tests/__init__.py | 7 + app/tests/test_api.py | 38 ++++++ app/tests/test_auth.py | 38 ++++++ app/tests/test_count.py | 23 ++++ app/tests/test_resources.py | 46 +++++++ app/tests/test_view.py | 30 +++++ app/utils/__init__.py | 11 ++ app/utils/password.py | 18 +++ app/utils/response.py | 79 +++++++++++ app/utils/update.py | 79 +++++++++++ docs/.nojekyll | 0 docs/README.md | 259 ++++++++++++++++++++++++++++++++++++ docs/_coverpage.md | 13 ++ docs/icon.svg | 1 + docs/index.html | 23 ++++ manage.py | 39 ++++++ requirements.txt | 10 ++ 37 files changed, 1773 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Procfile create mode 100644 README.md create mode 100644 app/__init__.py create mode 100644 app/api/__init__.py create mode 100644 app/api/auth.py create mode 100644 app/api/errors.py create mode 100644 app/api/views.py create mode 100644 app/config.py create mode 100644 app/db/__init__.py create mode 100644 app/db/db.py create mode 100644 app/main/__init__.py create mode 100644 app/main/errors.py create mode 100644 app/main/views.py create mode 100644 app/static/favicon.ico create mode 100644 app/static/index.css create mode 100644 app/static/request.js create mode 100644 app/templates/index.html create mode 100644 app/templates/view.html create mode 100644 app/tests/__init__.py create mode 100644 app/tests/test_api.py create mode 100644 app/tests/test_auth.py create mode 100644 app/tests/test_count.py create mode 100644 app/tests/test_resources.py create mode 100644 app/tests/test_view.py create mode 100644 app/utils/__init__.py create mode 100644 app/utils/password.py create mode 100644 app/utils/response.py create mode 100644 app/utils/update.py create mode 100644 docs/.nojekyll create mode 100644 docs/README.md create mode 100644 docs/_coverpage.md create mode 100644 docs/icon.svg create mode 100644 docs/index.html create mode 100644 manage.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..77d84ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Dirs +.idea/ +build/ +dist/ + + +# Files +*.pyc +*.zip +*.tar +*.gz +*.sqlite +*.log +*.log.* +*.sql +*.gif \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ae6456f --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [2022] [MarkusJoe] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..f213311 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: gunicorn manage:app \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6e8abc9 --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +

+ + PythonVersion + LICENSE + FlaskVersion + +

+ +
+ Counter +
+ +* 上方的计数器为不更新版本只保留了最基本的功能 + +## Python 版访问次数计数器 使用了flask 作为服务端接收请求 + +* [文档](https://request-counter-docs.vercel.app/#/) + +### 开源 + +- 本项目以[Apache-2.0](./LICENSE)许可开源, 即: + - 你可以直接使用该项目提供的功能, 无需任何授权 + - 你可以在**注明来源版权信息**的情况下对源代码进行任意分发和修改以及衍生 + +## 部署 + +> 在部署之前你需要先安装`python3.10.x`以上的版本 + +### 已部署好的地址 + +* [Click to enter](https://requestcounter.herokuapp.com/count/MarkusJoe) +* 默认主题: `lewd` + +### 部署到Heroku + +1. `fork` 本项目到你的仓库 +2. 在[Heroku](https://www.heroku.com/) 注册账号 +3. 在[Dashboard](https://dashboard.heroku.com/apps) 新建App +4. 流程: 进入网址 -> 点击右上角`New` -> 点击 `Create new app` -> 输入App名称 -> `Create app` -> 选择`Github` (登陆完成后) -> 点击`Search` -> + 选择你fork的项目并点击`Connect` -> 滑到末尾点击`Deploy branch`(如果你想在仓库更新时自动部署的话可以把`Enable Automatic Deploy`勾选) -> 等待完成 +5. App的地址就是 `App名称` + `.herokuapp.com` + +### 部署到本地服务器 + +```shell +$ git clone https://github.com/MarkusJoe/RequestCounter.git +$ cd RequestCounter +$ pip3 install -r requirements.txt +``` + +### 启动服务 + +```shell +$ python3 manage.py or gunicorn manage:app or waitress-serve --port=5000 manage:app +``` + +## 调用须知 + +- API支持`GET` 和 `POST` 方法请求 +- 最大可以计数`10`位数, 超过则重置 +- 可以自定义显示位数默认`7`位数最大`10`位 +- 可以自己选择更多的主题只需要加上请求参数: `theme` 再加上想要的主题即可 + +# 一些信息 + +- 使用了Python3.6 的标准库`Sqlite3`进行数据库操作 +- 你可以在`api`获取原始数据, 文档地址: [文档](https://markusjoe.github.io/RequestCounter/#/?id=api) diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..c3e22d3 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# -- coding:utf-8 -- +# @Author: markushammered@gmail.com +# @Development Tool: PyCharm +# @Create Time: 2022/3/25 +# @File Name: __init__.py + + +import os +from flask import Flask +from flask_sslify import SSLify +from .main import main as main_blueprint +from .api import api as api_blueprint +from .config import config +from .utils.password import generate_pwd + + +def create_app(config_name): + """ + 创建 app + :param config_name: + :return: + """ + app = Flask(__name__) + app.config.from_object(config[config_name]) + config[config_name].init_app(app) + if app.config['SSL_REDIRECT']: + SSLify(app) + + app.register_blueprint(main_blueprint) + app.register_blueprint(api_blueprint, url_prefix='/api/v1/') + + access_key = os.getenv('ACCESS_KEY') + secret = app.config['SECRET_KEY'] + try: + if not access_key and not secret: + access_key = generate_pwd(16) + elif not access_key and secret: + access_key = secret + finally: + os.environ['ACCESS_KEY'] = access_key + + app.logger.info('访问密钥: {}'.format(access_key)) + + return app + + +__all__ = [ + 'db', + 'main', + 'tests', + 'utils', + 'create_app' +] diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..6999019 --- /dev/null +++ b/app/api/__init__.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# -- coding:utf-8 -- +# @Author: markushammered@gmail.com +# @Development Tool: PyCharm +# @Create Time: 2022/4/9 +# @File Name: __init__.py.py + + +from flask import Blueprint + +api = Blueprint('api', __name__) + +from . import views, errors + +__all__ = [ + 'api', + 'views', + 'errors' +] diff --git a/app/api/auth.py b/app/api/auth.py new file mode 100644 index 0000000..68df554 --- /dev/null +++ b/app/api/auth.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# -- coding:utf-8 -- +# @Author: markushammered@gmail.com +# @Development Tool: Pycharm +# @Create Time: 2022/5/2 +# @File Name: auth.py + + +import os +from flask import request +from flask_httpauth import HTTPTokenAuth + +auth = HTTPTokenAuth() + + +@auth.verify_token +def verify_token(token): + secret_key = os.getenv('ACCESS_KEY') + key = request.args.get('key') + if token or key == secret_key: + return True + elif (token == 'unittest' or + key == 'unittest') and \ + 'system' in request.path.split('/'): # 限定在system这个接口使用unittest密钥获取数据 + return True + return False diff --git a/app/api/errors.py b/app/api/errors.py new file mode 100644 index 0000000..be2e0bc --- /dev/null +++ b/app/api/errors.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -- coding:utf-8 -- +# @Author: markushammered@gmail.com +# @Development Tool: PyCharm +# @Create Time: 2022/4/9 +# @File Name: errors.py + +from . import api +from .auth import auth +from flask import jsonify + + +@api.errorhandler(403) +@auth.error_handler +def forbidden(e): + return jsonify( + { + 'code': 403, + 'msg': 'API: Permission Denied.', + 'data': None + } + ), 403 + + +@api.errorhandler(404) +def page_not_found(e): + return jsonify( + { + 'code': 404, + 'msg': 'API: Page Not Found.', + 'data': None + } + ), 404 + + +@api.errorhandler(500) +def internal_server_error(e): + return jsonify( + { + 'code': 500, + 'msg': 'API: Internal Server Error.', + 'data': None + } + ), 500 diff --git a/app/api/views.py b/app/api/views.py new file mode 100644 index 0000000..9375361 --- /dev/null +++ b/app/api/views.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +# -- coding:utf-8 -- +# @Author: markushammered@gmail.com +# @Development Tool: PyCharm +# @Create Time: 2022/4/9 +# @File Name: views.py + + +import sys +import psutil +import platform +from flask import jsonify +from flask import request +from flask import send_file +from ..db.db import SQLite as db +from . import api +from .auth import auth + + +@api.route('/test/', methods=['GET', 'POST']) +def test_api(): + """ + 测试专用接口 + :return: + """ + data = db().fetch('test-example') + return str(data[1]) + + +@api.route('/overall/', methods=['GET', 'POST']) +def overall(): + """ + 查询ReqCount表内所有的源数据 + :return: + """ + limit = request.args.get('limit', type=int) + try: + data = db().exec('select * from reqcount;') + format_data = [] + for i in data: + format_data.append({'name': i[0], 'times': i[1]}) + return jsonify( + { + 'code': 200, + 'msg': 'Success. Total: %s row(s)' % len(data), + 'data': format_data[:limit] + } + ), 200 + except Exception as e: + return jsonify( + { + 'code': -200, + 'msg': 'Error', + 'data': e + } + ), 500 + + +@api.route('/query/', methods=['GET', 'POST']) +def query(): + """ + 查询指定名称的计数数据 + :return: + """ + name = request.args.get('name', type=str) + nochange = request.args.get('nochange', type=bool) + if nochange: + nochange = True + if not db().exists_name(name): + return jsonify( + { + 'code': -200, + 'msg': 'Failed. No such name', + 'data': None + } + ), 404 + else: + data = db().fetch(name, nochange) + return jsonify( + { + 'code': 200, + 'msg': 'Success', + 'data': data + } + ), 200 + + +@api.route('/theme/', methods=['GET', 'POST']) +def theme(): + """ + 查询数据库内主题的源数据 + :return: + """ + _theme = request.args.get('name', type=str) + if not db().exists_table(_theme): + return jsonify( + { + 'code': 404, + 'msg': 'Failed. No such theme', + 'data': None + } + ), 404 + else: + data = db().fetching_table(_theme) + return jsonify( + { + 'code': 200, + 'msg': 'Success', + 'data': data + } + ), 200 + + +@api.route('/alltables/', methods=['GET', 'POST']) +def all_tables(): + """ + 查询所有已有表 + :return: + """ + data = db().show_tables + return jsonify( + { + 'code': 200, + 'msg': 'Success', + 'data': data + } + ), 200 + + +@api.route('/export/', methods=['GET', 'POST']) +@auth.login_required +def export(): + """ + 导出数据库文件 + :return: + """ + return send_file('./db/data.sqlite', as_attachment=True), 200 + + +@api.route('/system/', methods=['GET', 'POST']) +@auth.login_required +def system_info(): + """ + 获取当前部署服务器的系统状态信息 + :return: + """ + _platform = platform.system() + _cpu_count = psutil.cpu_count() + _memory_size = psutil.virtual_memory() + _total_memory_size = round(_memory_size[0] / 1024 / 1024 / 1024, 2) + _free_memory_size = round(_memory_size[4] / 1024 / 1024 / 1024, 2) + _used_memory_size = round(_memory_size[3] / 1024 / 1024 / 1024, 2) + _percent = _memory_size[2] + _py_version = sys.version + + return jsonify( + { + 'code': 200, + 'msg': 'Success', + 'data': { + 'Platform': _platform, + 'CPU-Count': _cpu_count, + 'Memory-Size(GigaBytes)': { + 'Total': _total_memory_size, + 'used': _used_memory_size, + 'Free': _free_memory_size, + 'Percent': _percent + }, + 'Python': _py_version + } + } + ), 200 diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..baca203 --- /dev/null +++ b/app/config.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# -- coding:utf-8 -- +# @Author: markushammered@gmail.com +# @Development Tool: PyCharm +# @Create Time: 2022/4/5 +# @File Name: config.py + + +import os +import logging +import platform +from logging.handlers import * + + +class Config: + JSON_SORT_KEYS = False + JSON_AS_ASCII = False + SSL_REDIRECT = False + SECRET_KEY = '' + + @staticmethod + def init_app(app): + if not os.path.exists('./app/logs'): + os.mkdir('./app/logs') + + formatter = logging.Formatter( + fmt='[%(asctime)s] |%(filename)s[%(funcName)s:%(lineno)d] |%(levelname)-8s |%(message)s', + datefmt='%H:%M:%S') + handler = logging.handlers.RotatingFileHandler(filename='./app/logs/access.log', + maxBytes=10240, + backupCount=10) + handler.setFormatter(formatter) + handler.setLevel(logging.DEBUG) + + if platform.system() == 'Linux': + syslog_handler = SysLogHandler() + syslog_handler.setLevel(logging.INFO) + app.logger.addHandler(syslog_handler) + + app.logger.addHandler(handler) + + +class DevelopmentConfig(Config): + DEBUG = True + + +class ProductionConfig(Config): + DEBUG = False + + +class HerokuConfig(ProductionConfig): + SSL_REDIRECT = True + + +class ProfessionalConfig(ProductionConfig): + SSL_REDIRECT = True + + +class VersionConfig: + # 暂未开放/Not available + version = ['v0.2.4'] + + +config = { + 'development': DevelopmentConfig, + 'production': ProductionConfig, + 'professional': ProfessionalConfig, + 'heroku': HerokuConfig, + + 'default': DevelopmentConfig +} diff --git a/app/db/__init__.py b/app/db/__init__.py new file mode 100644 index 0000000..f5cf616 --- /dev/null +++ b/app/db/__init__.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -- coding:utf-8 -- +# @Author: markushammered@gmail.com +# @Development Tool: PyCharm +# @Create Time: 2022/3/25 +# @File Name: __init__.py.py + + +import os +import sys +import requests + + +def download(): + res = requests.get('https://filebase.vercel.app/download/data.sqlite') + with open('./app/db/data.sqlite', 'wb') as fp: + fp.write(res.content) + + +if not os.path.exists('./app/db/data.sqlite'): + try: + download() + except requests.exceptions as error: + sys.exit(1) + +__all__ = [ + 'db' +] diff --git a/app/db/db.py b/app/db/db.py new file mode 100644 index 0000000..57006df --- /dev/null +++ b/app/db/db.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +# -- coding:utf-8 -- +# @Author: markushammered@gmail.com +# @Development Tool: PyCharm +# @Create Time: 2022/3/25 +# @File Name: db.py + + +import sqlite3 +from typing import List, Any + + +class SQLite: + """操作SQLite数据库""" + + def __init__(self) -> None: + """ + 初始化SQLite对象 + 完成后会自动提交, 自动关闭 + """ + self.__path = './app/db/data.sqlite' + self.__conn = sqlite3.connect(self.__path) + self.__cursor = self.__conn.cursor() + + def __del__(self) -> None: + """ + 自动提交 + 自动关闭连接 + :return: + """ + self.__conn.commit() + self.__cursor.close() + self.__conn.close() + + @property + def show_tables(self) -> list: + """ + 列出所有在数据库内的表 + :return: + """ + self.__cursor.execute('select * from sqlite_master where type="table"') + tables = self.__cursor.fetchall() + lst = [] + for table in tables: + if table[1] != 'reqcount': + lst.append(table[1]) + del tables + return lst + + def exists_name(self, name: str) -> bool: + """ + 判断当前名称是否在数据库内 + 调用self.fetch()方法 + :param name: + :return: + """ + status = self.fetch(name, True) + if not bool(status): + return False + else: + return True + + def exists_table(self, table: str) -> bool: + """ + 判断表是否在数据库内 + 调用self.fetching_table()方法 + :param table: + :return: + """ + tables = self.show_tables + if table in tables: + return True + else: + return False + + def insert(self, name: str, times: int = 0) -> bool: + """ + 插入数据 + :param name: + :param times: + :return: + """ + self.__cursor.execute( + 'insert into reqcount (name, times) values("%(name)s", %(times)s)' % {'name': name, 'times': times}) + return True + + def delete(self, name: str) -> bool: + """ + 删除数据 + 暂时不会用到此接口 + :param name: + :return: + """ + self.__cursor.execute('delete from reqcount where name="%(name)s"' % {'name': name}) + return True + + def fetch(self, name: str, is_check: bool = False) -> tuple: + """ + 抓取数据 + :param name: + :param is_check: + :return: + """ + self.__cursor.execute('select * from reqcount where name="%(name)s"' % {'name': name}) + data = self.__cursor.fetchone() + if not is_check: + self.update(name, data[-1]) + self.__cursor.execute('select * from reqcount where name="%(name)s"' % {'name': name}) + data = self.__cursor.fetchone() + return data + + def update(self, name: str, times: int) -> bool: + """ + 更新数据 + :param name: + :param times: + :return: + """ + self.__cursor.execute( + 'update reqcount set times=%(times)s where name="%(name)s"' % {'times': times + 1, 'name': name}) + return True + + def fetching_table(self, table: str) -> List[dict]: + """ + 抓取主题表的数据 + :param table: + :return: + """ + status = self.exists_table(table) + if status: + tables = self.__cursor.execute('select * from %(table)s' % {'table': table}) + lst = [] + for data in tables: + lst.append({'index': data[0], + 'url': data[1], + 'width': data[2], + 'height': data[3]}) + del tables + return lst + else: + return [] + + def exec(self, cmd: str) -> List[Any]: + """ + 执行特殊sql语句 + :param cmd: + :return: + """ + self.__cursor.execute(cmd) + result = self.__cursor.fetchall() + return result diff --git a/app/main/__init__.py b/app/main/__init__.py new file mode 100644 index 0000000..a20857b --- /dev/null +++ b/app/main/__init__.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# -- coding:utf-8 -- +# @Author: markushammered@gmail.com +# @Development Tool: PyCharm +# @Create Time: 2022/3/25 +# @File Name: __init__.py.py + + +from flask import Blueprint + +main = Blueprint('main', __name__) + +from . import views, errors + +__all__ = [ + 'main', + 'views', + 'errors' +] diff --git a/app/main/errors.py b/app/main/errors.py new file mode 100644 index 0000000..c56ea74 --- /dev/null +++ b/app/main/errors.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# -- coding:utf-8 -- +# @Author: markushammered@gmail.com +# @Development Tool: PyCharm +# @Create Time: 2022/3/25 +# @File Name: errors.py + +from . import main +from flask import jsonify + + +@main.app_errorhandler(404) +def page_not_found(e): + return jsonify( + { + 'code': 404, + 'msg': '页面未找到', + 'data': None + } + ), 404 + + +@main.app_errorhandler(500) +def internal_server_error(e): + return jsonify( + { + 'code': 500, + 'msg': '服务器内部错误', + 'data': None + } + ), 500 diff --git a/app/main/views.py b/app/main/views.py new file mode 100644 index 0000000..e56d6a2 --- /dev/null +++ b/app/main/views.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# -- coding:utf-8 -- +# @Author: markushammered@gmail.com +# @Development Tool: PyCharm +# @Create Time: 2022/3/25 +# @File Name: views.py + + +from flask import abort +from flask import request +from flask import make_response +from flask import render_template +from ..db.db import SQLite as db +from ..utils.response import index_ +from . import main + + +@main.route('/', methods=['GET', 'POST']) +def index(): + """ + 主页 + :return: + """ + return render_template('index.html', remote_address=request.remote_addr) + + +@main.route('/count/', methods=['GET', 'POST']) +def home(name: str): + """ + 主视图 + :param name: + :return: + """ + length = request.args.get('length', 7, type=int) + theme = request.args.get('theme', 'lewd', type=str) + if not db().exists_name(name): + db().insert(name) + if not db().exists_table(theme): + abort(500) + if 7 <= length <= 10: + data = db().fetch(name) + if len(str(data[1])) >= 10: # 计数的数据长度超过最大位数将次数设为1 + db().update(name, 0) + # 使用设置的长度减去已有数据的长度, 将结果转换为string类型, 再和数据库内的数据进行拼接 + number = '0' * (length - len(str(data[-1]))) + str(data[-1]) + response = make_response(index_(theme, int(length), name, number)) + response.headers['Content-Type'] = 'image/svg+xml; charset=utf-8' + response.headers['cache-control'] = 'max-age=0, no-cache, no-store, must-revalidate' + return response + return abort(500) diff --git a/app/static/favicon.ico b/app/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..211be1f4cbd2b030abadcbf875aadec469cb0858 GIT binary patch literal 67646 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 R>VZ-ZlzO1l1OInD@L%Q*8ejkb literal 0 HcmV?d00001 diff --git a/app/static/index.css b/app/static/index.css new file mode 100644 index 0000000..88c84ce --- /dev/null +++ b/app/static/index.css @@ -0,0 +1,15 @@ +.intro { + text-align: center; +} + +.head { + text-align: center; +} + +.example { + text-align: center; +} + +a { + color: green; +} \ No newline at end of file diff --git a/app/static/request.js b/app/static/request.js new file mode 100644 index 0000000..3e9cdae --- /dev/null +++ b/app/static/request.js @@ -0,0 +1,10 @@ +function request_async(){ + // 使用fetch()函数进行异步请求如果服务端响应速度极慢则不会阻塞页面 + fetch('/api/v1/test').then( + response => response.text() + ).then( + text => { + document.getElementById('fast_test_example_text').innerText = text + } + ) +} diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..9d50ec9 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,73 @@ + + + + + Request Counter | {{ remote_address }} + + + + + +
+

+ 应用名: 访问次数计数器 +

+

+ 简介: 可以使用SVG矢量图的形式来显示页面的访问次数 +
+ 或者通过API的方式获取原始计数数据 +

+
+
+ + +
+ 注: 调用支持POST 和 GET 方法请求 +
+
+ 项目开源协议: Apache-2.0 即: +
+ 你可以直接使用该项目提供的功能, 无需任何授权 +
+ 你可以在注明来源版权信息的情况下对源代码进行任意分发和修改以及衍生 +
+
+ +
+ 请求地址来源: {{ remote_address }} +
+
+
+
+ fast_test_example_svg + fast_test_example_svg + fast_test_example_svg + fast_test_example_svg + fast_test_example_svg + fast_test_example_svg + fast_test_example_svg + fast_test_example_svg + fast_test_example_svg + fast_test_example_svg +
+
+ +
+ test-example当前计数数据值: +

+
+
+
+ + diff --git a/app/templates/view.html b/app/templates/view.html new file mode 100644 index 0000000..45724bd --- /dev/null +++ b/app/templates/view.html @@ -0,0 +1,11 @@ + + + {{ title }} | {{ address }} + + {% for value in context %} + + {% endfor %} + + diff --git a/app/tests/__init__.py b/app/tests/__init__.py new file mode 100644 index 0000000..d21e6a1 --- /dev/null +++ b/app/tests/__init__.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 +# -- coding:utf-8 -- +# @Author: markushammered@gmail.com +# @Development Tool: PyCharm +# @Create Time: 2022/3/26 +# @File Name: __init__.py.py + diff --git a/app/tests/test_api.py b/app/tests/test_api.py new file mode 100644 index 0000000..76cfbcb --- /dev/null +++ b/app/tests/test_api.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# -- coding:utf-8 -- +# @Author: markushammered@gmail.com +# @Development Tool: Pycharm +# @Create Time: 2022/5/1 +# @File Name: test_api.py + + +import random +import unittest +import requests + + +class TestAPI(unittest.TestCase): + def setUp(self) -> None: + self.api = 'http://127.0.0.1:5000/api/v1/' + + def test_query(self): + res = requests.post(self.api + 'query', params={'name': 'test-example'}) + self.assertEqual(res.status_code, 200) + + def test_query_failed(self): + letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + r_name = ''.join(random.choice(letters) for _ in range(10)) + res = requests.post(self.api + 'query', params={'name': r_name}) + self.assertEqual(res.status_code, 404) + + def test_overall(self): + res = requests.post(self.api + 'overall') + self.assertEqual(res.status_code, 200) + + def test_alltables(self): + res = requests.post(self.api + 'alltables') + self.assertEqual(res.status_code, 200) + + def test_test(self): + res = int(requests.post(self.api + 'test').text) + self.assertIs(type(res), int) diff --git a/app/tests/test_auth.py b/app/tests/test_auth.py new file mode 100644 index 0000000..2626192 --- /dev/null +++ b/app/tests/test_auth.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +# -- coding:utf-8 -- +# @Author: markushammered@gmail.com +# @Development Tool: Pycharm +# @Create Time: 2022/5/2 +# @File Name: test_auth.py + + +import random +import requests +import unittest + + +class TestAuth(unittest.TestCase): + def setUp(self) -> None: + self.api = 'http://127.0.0.1:5000/api/v1/' + + @property + def r_pwd(self): + letters = 'abcdefghijklmnopqrstuvwxyz' + r_pwd = ''.join(random.choice(letters) for _ in range(16)) + return r_pwd + + def test_api_system_header(self): + res = requests.post(self.api + 'system', headers={'Authorization': 'bearer unittest'}) + self.assertEqual(res.status_code, 200) + + def test_api_system_query(self): + res = requests.post(self.api + 'system', params={'key': 'unittest'}) + self.assertEqual(res.status_code, 200) + + def test_api_system_header_failed(self): + res = requests.post(self.api + 'system', headers={'Authorization': f'bearer {self.r_pwd}'}) + self.assertEqual(res.status_code, 403) + + def test_api_system_query_failed(self): + res = requests.post(self.api + 'system', params={'key': self.r_pwd}) + self.assertEqual(res.status_code, 403) diff --git a/app/tests/test_count.py b/app/tests/test_count.py new file mode 100644 index 0000000..746ca7a --- /dev/null +++ b/app/tests/test_count.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +# -- coding:utf-8 -- +# @Author: markushammered@gmail.com +# @Development Tool: Pycharm +# @Create Time: 2022/5/1 +# @File Name: test_count.py + + +import requests +import unittest + + +class TestCount(unittest.TestCase): + def setUp(self) -> None: + self.api = 'http://127.0.0.1:5000/' + + def test_index(self): + res = requests.post(self.api).text + self.assertIsNotNone(res) + + def test_count(self): + res = requests.post(self.api + 'count/test-example').text + self.assertIsNotNone(res) diff --git a/app/tests/test_resources.py b/app/tests/test_resources.py new file mode 100644 index 0000000..78af880 --- /dev/null +++ b/app/tests/test_resources.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# -- coding:utf-8 -- +# @Author: markushammered@gmail.com +# @Development Tool: Pycharm +# @Create Time: 2022/5/1 +# @File Name: test_resources.py + + +import requests +import unittest + + +class TestResources(unittest.TestCase): + def setUp(self) -> None: + self.resources_urls = { + 'docs': [ + 'https://request-counter-docs.vercel.app/#/', + 'https://markusjoe.github.io/RequestCounter/#/' + ], + 'database': 'https://filebase.vercel.app/download/data.sqlite', + 'themes': + { + 'lewd': 'https://static-file-hosting.vercel.app/static/lewd/0', + 'gelbooru': 'https://static-file-hosting.vercel.app/static/gelbooru/0', + 'moebooru': 'https://static-file-hosting.vercel.app/static/moebooru/0', + 'blacked': 'https://static-file-hosting.vercel.app/static/blacked/0', + 'lisu': 'https://static-file-hosting.vercel.app/static/lisu/0' + } + } + + def test_doc_vercel(self): + res = requests.post(self.resources_urls['docs'][0]).text + self.assertIsNotNone(res) + + def test_doc_github(self): + res = requests.post(self.resources_urls['docs'][1]).text + self.assertIsNotNone(res) + + def test_database(self): + res = requests.post(self.resources_urls['database']) + self.assertEqual(res.status_code, 200) + + def test_themes(self): + for t in self.resources_urls['themes']: + res = requests.get(self.resources_urls['themes'][t]) + self.assertEqual(res.status_code, 200) diff --git a/app/tests/test_view.py b/app/tests/test_view.py new file mode 100644 index 0000000..e800557 --- /dev/null +++ b/app/tests/test_view.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -- coding:utf-8 -- +# @Author: markushammered@gmail.com +# @Development Tool: Pycharm +# @Create Time: 2022/5/2 +# @File Name: test_view.py + + +import random +import requests +import unittest + + +class TestViews(unittest.TestCase): + def setUp(self) -> None: + self.api = 'http://127.0.0.1:5000/' + + @property + def r_name(self): + letters = 'abcdefghijklmnopqrstuvwxyz' + r_name = [random.choice(letters) for _ in range(10)] + return r_name + + def test_not_found(self): + res = requests.get(self.api + 'not_exist_page') + self.assertEqual(res.status_code, 404) + + def test_server_internal_error(self): + res = requests.get(self.api + f'count/{self.r_name}', params={'length': 0}) + self.assertEqual(res.status_code, 500) diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000..bcba25c --- /dev/null +++ b/app/utils/__init__.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +# -- coding:utf-8 -- +# @Author: markushammered@gmail.com +# @Development Tool: PyCharm +# @Create Time: 2022/3/25 +# @File Name: __init__.py.py + +__all__ = [ + 'response', + 'password' +] diff --git a/app/utils/password.py b/app/utils/password.py new file mode 100644 index 0000000..fc858b4 --- /dev/null +++ b/app/utils/password.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -- coding:utf-8 -- +# @Author: markushammered@gmail.com +# @Development Tool: PyCharm +# @Create Time: 2022/4/11 +# @File Name: password.py + +import random + + +def generate_pwd(length): + """ + 生成随机密码 + :param length: + :return: + """ + letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + return ''.join(random.choice(letters) for _ in range(length)) diff --git a/app/utils/response.py b/app/utils/response.py new file mode 100644 index 0000000..ba3663d --- /dev/null +++ b/app/utils/response.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +# -- coding:utf-8 -- +# @Author: markushammered@gmail.com +# @Development Tool: PyCharm +# @Create Time: 2022/3/25 +# @File Name: response.py + + +from ..db.db import SQLite as db +from flask import request +from flask import render_template +from typing import Tuple + + +def index_(theme: str, length: int, name: str, count: str) -> str or Tuple[bool, str]: + """ + 渲染模板 + :param theme: + :param length: + :param name: + :param count: + :return: + """ + datas = db().fetching_table(theme) + context = [] + for i in count: + if i == '0': + context.append({'url': datas[0]['url'], + 'width': datas[0]['width'], + 'height': datas[0]['height']}) + elif i == '1': + context.append({'url': datas[1]['url'], + 'width': datas[1]['width'], + 'height': datas[1]['height']}) + elif i == '2': + context.append({'url': datas[2]['url'], + 'width': datas[2]['width'], + 'height': datas[2]['height']}) + elif i == '3': + context.append({'url': datas[3]['url'], + 'width': datas[3]['width'], + 'height': datas[3]['height']}) + elif i == '4': + context.append({'url': datas[4]['url'], + 'width': datas[4]['width'], + 'height': datas[4]['height']}) + elif i == '5': + context.append({'url': datas[5]['url'], + 'width': datas[5]['width'], + 'height': datas[5]['height']}) + elif i == '6': + context.append({'url': datas[6]['url'], + 'width': datas[6]['width'], + 'height': datas[6]['height']}) + elif i == '7': + context.append({'url': datas[7]['url'], + 'width': datas[7]['width'], + 'height': datas[7]['height']}) + elif i == '8': + context.append({'url': datas[8]['url'], + 'width': datas[8]['width'], + 'height': datas[8]['height']}) + elif i == '9': + context.append({'url': datas[9]['url'], + 'width': datas[9]['width'], + 'height': datas[9]['height']}) + + for p, i in zip(context, range(0, length)): + p['position'] = i * p['width'] # 设置每张图片对应的位置 + + general_width = datas[0]['width'] * length # 计算出图片总长度 + general_height = datas[0]['height'] # 总宽度 + + return render_template('view.html', + context=context, + title=name, + address=request.remote_addr, + general_height=general_height, + general_width=general_width) diff --git a/app/utils/update.py b/app/utils/update.py new file mode 100644 index 0000000..1e19ebd --- /dev/null +++ b/app/utils/update.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +# -- coding:utf-8 -- +# @Author: markushammered@gmail.com +# @Development Tool: Pycharm +# @Create Time: 2022/4/29 +# @File Name: update.py + + +import os +import base64 +import logging +import requests +from ..config import VersionConfig +from typing import Dict, List + + +class SelfUpdateVersion: + """更新本体 -> 更新单个文件""" + + def __init__(self, logger: logging.Logger): + self.api = 'https://api.github.com/repos/MarkusJoe/RequestCounter/tags' + self.headers = {'Authorization': 'token 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'} + self.resp = requests.get(self.api, headers=self.headers).json() + self.logger = logger + + def commit_details(self): + """ + 获取提交细节 + :return: + """ + commit_url = self.resp[0]['commit']['url'] + res = requests.get(commit_url, headers=self.headers).json() + files = res['files'] + modified = [] + deleted = [] + + for f in files: + file_path = f['filename'] + base64_content = requests.get(f['contents_url'], headers=self.headers).json()['content'] # 直接获取修改文件的base64 + if f['status'] in ['modified', 'added']: + modified.append({'filename': f'./{file_path}', 'content': base64.b64decode(base64_content)}) + else: + deleted.append(f'./{file_path}') + + self.apply_change(modified, deleted) + + def apply_change(self, modified: List[Dict], deleted: List): + """ + 应用已经更改的文件 + :param modified: + :param deleted: + :return: + """ + for d in deleted: + os.remove(d) + self.logger.info(f'删除了: {d}') + + for m in modified: + with open(m['filename'], 'wb') as modify: + modify.write(m['content']) + self.logger.info(f'修改了: {m["filename"]}') + + with open('./app/config.py', 'wb') as update_config: + content = requests.get('https://api.github.com/repos/MarkusJoe/RequestCounter/contents/app/config.py', + headers=self.headers).json()['content'] + update_config.write(base64.b64decode(content)) + + exit() + + def check(self): + current = VersionConfig.version[0] + newest = self.resp[0]['name'] + if current != newest: + self.logger.warning(f'当前版本已经落后于最新版本. 当前: {current} -> 远程: {newest}') + self.commit_details() + return False + else: + self.logger.info('当前版本为最新版本') + return True diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..803435e --- /dev/null +++ b/docs/README.md @@ -0,0 +1,259 @@ +# 这里是Request Counter的文档 + +> 虽然说项目很小根本不需要文档 + +# 工作原理 + +> Flask 建立服务器来接收请求并返回对应的响应体 + +# 部署 + +> 在部署之前你需要先安装`python3.10.x`以上的版本 + +## 部署到本地服务器 + + ```shell + $ git clone https://github.com/MarkusJoe/RequestCounter.git + $ cd RequestCounter + $ pip3 install -r requirements.txt + $ python3 manage.py + ``` + +# 普通接口 + +## `/count/` 接口 + +> 必选参数: `name`: `str` 可选参数: `length`: `int` ; `theme`: `str` + +### 调用示例 + +```shell +$ curl -L -X https://requestcounter.herokuapp.com/count/test-example/ +``` + +```json +"..." +``` + +# API + +* 注意事项 + +1. `/api/v1/`内的接口需要`key`来访问的有: `/export/` 可以将`key`放入请求头使用也可以作为查询参数使用 +2. `key`从环境变量中获取或者从`config.py`内的`Production`类的`SECRET_KEY`获取, 先判断环境变量是否存在如果不存在就使用`config.py`, 如果两个都存在则使用环境变量 +3. `key`的长度为`16`位的随机英文字母大小写字符串 +4. 目前需要`key`的接口有`export` `system` + +> 地址前缀: `/api/v1/` + +- 接口地址: `/overall/`, `/query/`, `/theme/` +- 请求方法: `GET` `POST` + +## 单元测试 + +> 通用 + +```shell +$ python3 -m unittest app/tests/test_* +``` + +> `Unix (Linux, Mac, etc.)` + +```shell +$ export FLASK_APP=manage.py +$ flask test +``` + +> `Windows CMD` + +```shell +set FLASK_APP=manage.py +flask test +``` + +> `Windows PowerShell` + +```shell +env:FLASK_APP=manage.py +flask test +``` + +## `key 使用示例` + +> `key`的获取优先使用请求头内的数据 + +```python +import requests + + +def login_with_header(): + # 使用请求头登陆 + header = {'Authorization': 'bearer unittest'} + resp = requests.get('http://127.0.0.1:5000/api/v1/system/', headers=header) + print(resp.json()) + + +def login_with_query(): + # 使用查询参数登陆 + resp = requests.get('http://127.0.0.1:5000/api/v1/system/', params={'key': 'unittest'}) + print(resp.json()) + + +if __name__ == '__main__': + login_with_header() + login_with_query() + +``` + +## `/overall/` 接口 + +> 使用此接口获取数据库中所有的数据 + +> 必选参数: `limit: int` > 自定义查询数量, 输入的数字大于等于最大值时, 将返回最大值 + +### 调用示例 + +```shell +$ curl -L -X GET https://requestcounter.herokuapp.com/api/v1/overall/?limit=20 +``` + +```json +{ + "code": 200, + "msg": "Success. Total: 7 row(s)", + "data": [ + { + "name": "sda", + "times": 72 + }, + { + "name": "main", + "times": 59 + } + ] +} +``` + +## `/query/` 接口 + +> 使用此接口获取数据库中指定的数据 + +***使用此接口查询的名称计数数值会加一*** + +> 必选参数: `name: str` > 查询指定名称的计数数据 + +> 可选参数: `nochange: Any` + +> `nochange`: 'Any` > 将参数值设置为任意值则仅查询不增加 + +### 调用示例 + +```shell +$ curl -L -X GET https://requestcounter.herokuapp.com/api/v1/query/?name=main&nochange=1 +``` + +```json +{ + "code": 200, + "msg": "Success", + "data": [ + "main", + 59 + ] +} +``` + +## `/theme/` 接口 + +> 使用此接口获取数据库内原始的base64编码的主题图片 + +> 必选参数: `theme: str` + +### 调用示例 + +```shell +$ curl -L -X GET https://requestcounter.herokuapp.com/api/v1/theme/?name=lewd +``` + +```json + +{ + "code": 200, + "msg": "Success", + "data": [ + { + "index": "...", + "url": "...", + "width": 45, + "height": 100 + }, + "..." + ] +} +``` + +## `/alltables/` 接口 + +> 此接口可以获取数据库内所有的表名 > 返回的数据中不包含`ReqCount` + +> 无参数 + +```shell +$ curl -L -X GET https://requestcounter.herokuapp.com/api/v1/alltables/ +``` + +```json +{ + "code": 200, + "msg": "Success", + "data": [ + "..." + ] +} +``` + +## `/export/` 接口 + +> 此接口可以导出应用的数据库文件 + +> 无参数 + +### 调用示例 + +```shell +$ curl -L -X GET https://requestcounter.herokuapp.com/api/v1/export/ +``` + +> 该接口请求成功后返回文件 + +## `/test/`接口 + +> 此接口仅用于测试 只能增加`test-example`名称的计数次数 + +> 无需`key`即可请求 + +### 调用示例 + +```shell +$ curl -L -X GET https://requestcounter.herokuapp.com/api/v1/test/ +``` + +```json +622 +``` + +> 返回一个`int`数字 + +# 关于 + +## 开源 + +- 本项目以Apache-2.0许可开源, 即: + - 你可以直接使用该项目提供的功能, 无需任何授权 + - 你可以在**注明来源版权信息**的情况下对源代码进行任意分发和修改以及衍生 + +## 关于此页面 + +此文档由 [docsify](https://github.com/docsifyjs/docsify) 生成. docsify 是一个动态生成文档网站的工具。不同于 GitBook、Hexo 的地方是它不会生成将 .md 转成 .html +文件,所有转换工作都是在运行时进行。 + diff --git a/docs/_coverpage.md b/docs/_coverpage.md new file mode 100644 index 0000000..48956df --- /dev/null +++ b/docs/_coverpage.md @@ -0,0 +1,13 @@ +![logo](./icon.svg) + +# RequestCounter + +> Python版的计数器 + +> 使用Python Flask 库完成的访问的次数计数器 + +* 响应速度快 +* 使用Sqlite3数据库操作 + +[GitHub](https://github.com/MarkusJoe/RequestCounter.git) +[Get Started](#部署) \ No newline at end of file diff --git a/docs/icon.svg b/docs/icon.svg new file mode 100644 index 0000000..423a5ed --- /dev/null +++ b/docs/icon.svg @@ -0,0 +1 @@ +arbkx-2njjk-svg \ No newline at end of file diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..0ec9cf1 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,23 @@ + + + + + RequestCounter 帮助文档 + + + + + + +
+ + + + + diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..ba56794 --- /dev/null +++ b/manage.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# -- coding:utf-8 -- +# @Author: markushammered@gmail.com +# @Development Tool: PyCharm +# @Create Time: 2022/3/25 +# @File Name: manage.py + + +import os +import unittest +from app import create_app + +dyno = os.getenv('DYNO') +if dyno is not None: + config = 'heroku' +else: + config = os.getenv('FLASK_CONFIG') or 'default' + +app = create_app(config) + + +@app.cli.command('test') +def tests_cli(): + all_test = unittest.TestLoader().discover('app/tests/') + unittest.TextTestRunner(verbosity=2).run(all_test) + + +if __name__ == '__main__': + from gevent import pywsgi + + app.logger.info('服务运行在 http://127.0.0.1:5000') + try: + server = pywsgi.WSGIServer(('0.0.0.0', 5000), app) + server.serve_forever() + except KeyboardInterrupt: + app.logger.info('已退出') + except OSError: + app.logger.critical('5000 端口已被占用') + exit(1) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7a4e260 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +flask~=2.1.1 +requests~=2.26.0 +gunicorn~=20.1.0 +Werkzeug~=2.0.3 +itsdangerous~=2.1.0 +flask_sslify~=0.1.5 +gevent~=21.12.0 +psutil~=5.9.0 +click~=8.1.2 +flask_httpauth~=4.6.0 \ No newline at end of file