├── .gitignore ├── LICENSE ├── README.md ├── app ├── __init__.py ├── cache.py ├── config.py ├── controllers │ ├── __init__.py │ ├── history_graph.py │ └── home.py ├── error_handlers.py ├── exceptions │ └── __init__.py ├── fields │ └── __init__.py ├── models │ ├── __init__.py │ ├── codestats_user.py │ ├── daily_language_xp.py │ └── history_graph_config.py ├── schemas │ ├── __init__.py │ ├── daily_language_xp.py │ └── history_graph_config.py ├── templates │ ├── error.svg │ ├── index.html │ └── validation_error.svg └── utils │ ├── __init__.py │ └── svgo.py ├── config ├── __init__.py ├── custom_config.py └── svgo.yml ├── gunicorn_config.py ├── requirements.txt └── run.py /.gitignore: -------------------------------------------------------------------------------- 1 | data/ 2 | config/custom_config_local.py 3 | 4 | .vscode/ 5 | .idea/ 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | env/ 17 | venv/ 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | 57 | # Sphinx documentation 58 | docs/_build/ 59 | 60 | # PyBuilder 61 | target/ 62 | 63 | # ========================= 64 | # Operating System Files 65 | # ========================= 66 | 67 | # OSX 68 | # ========================= 69 | 70 | .DS_Store 71 | .AppleDouble 72 | .LSOverride 73 | 74 | # Thumbnails 75 | ._* 76 | 77 | # Files that might appear on external disk 78 | .Spotlight-V100 79 | .Trashes 80 | 81 | # Directories potentially created on remote AFP share 82 | .AppleDB 83 | .AppleDesktop 84 | Network Trash Folder 85 | Temporary Items 86 | .apdisk 87 | 88 | # Windows 89 | # ========================= 90 | 91 | # Windows image file caches 92 | Thumbs.db 93 | ehthumbs.db 94 | 95 | # Folder config file 96 | Desktop.ini 97 | 98 | # Recycle Bin used on file shares 99 | $RECYCLE.BIN/ 100 | 101 | # Windows Installer files 102 | *.cab 103 | *.msi 104 | *.msm 105 | *.msp 106 | 107 | # Windows shortcuts 108 | *.lnk 109 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 WEGFan 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
Get dynamically generated Code::Stats stats on your profile readme!
4 | 5 | 6 |7 | View Demo 8 | · 9 | Report Bug 10 | · 11 | Request Feature 12 |
13 | 14 | --- 15 | 16 | > Note that Github's server aborts the request if it reaches the 4-second timeout. So maybe the images won't show because my server is waiting for response from Code::Stats, try refreshing and see if it shows. 17 | > To prevent heavy load from Code::Stats server, all data from same user will be cached for 30 minutes before updating data. 18 | 19 | ## Features 20 | 21 | - [Code::Stats History Graph](#codestats-history-graph) 22 | - [Increasing history days](#increasing-history-days) 23 | - [Increasing maximum languages](#increasing-maximum-languages) 24 | - [Customizing colors](#customizing-colors) 25 | - [More customization](#more-customization) 26 | - [Demo](#demo) 27 | - [Deploy on your own server](#deploy-on-your-own-server) 28 | 29 | ## Code::Stats History Graph 30 | 31 | Simply add this to your markdown and change `username` to your Code::Stats username. 32 | 33 | ```markdown 34 |  35 | ``` 36 | 37 | ### Increasing history days 38 | 39 | You can increase history days up to 30 days by adding a query parameter `?history_days=` 40 | 41 | ```markdown 42 |  43 | ``` 44 | 45 | ### Increasing maximum languages 46 | 47 | You can increase maximum languages up to 15 by adding a query parameter `?max_languages=` 48 | 49 | ```markdown 50 |  51 | ``` 52 | 53 | ### Customizing colors 54 | 55 | You can customize the colors of grid, text, zeroline and bars by adding `grid_color`, `text_color`, `zeroline_color` and `language_colors` query parameters. 56 | 57 | The `language_colors` is a list of colors seperated by comma and wrapped with `[]`, each color should be wrapped with quotes (e.g. `["red","hsl(0,100%,50%)","rgba(255,0,0,0.5)"]`) 58 | 59 | ```markdown 60 |  61 | ``` 62 | 63 | See [color string](#color-string) below for supported color representations. 64 | 65 | ### More customization 66 | 67 | | query parameter | type | description | default value | 68 | | --------------- | ----------------------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | 69 | | history_days | integer | how many days to show in the graph, maximum 30 | 14 | 70 | | max_languages | integer | how many languages to show before grouping into "Others", maximum 15 | 8 | 71 | | timezone | [timezone string](#timezone-string) | what timezone to use to calculate day ranges | 00:00 | 72 | | width | integer | the width of image (in pixels) | 900 | 73 | | height | integer | the height of image (in pixels) | 450 | 74 | | show_legend | boolean | whether to show graph legend on the right | true | 75 | | bg_color | [color string](#color-string) | the color of the image's background | ffffff | 76 | | grid_color | [color string](#color-string) | the color of the grid | e8e8e8 | 77 | | text_color | [color string](#color-string) | the color of the text | 666666 | 78 | | zeroline_color | [color string](#color-string) | the color of the zero-line | ababab | 79 | | language_colors | [color string](#color-string) list | the colors of each langauge and "Others" (loops if length is less than `max_languages`) | \["3e4053","f15854","5da5da", "faa43a","60bd68","f17cb0", "b2912f","decf3f","b276b2"\] | 80 | 81 | **Special types:** 82 | 83 | Color string 84 | 85 | - A hex string without `#` (e.g. `ff0000`, `ADE` for `aaddee`) 86 | - An rgb/rgba string (e.g. `rgb(255,0,0)`, `rgba(0,128,128,0.5)`) 87 | - An hsl/hsla string (e.g. `hsl(0,100%,50%)`, `hsla(60,50%,50%,0.7)`) 88 | - An hsv/hsva string (e.g. `hsv(0,100%,100%)`, `hsva(120,100%,100%,0.5)`) 89 | - A named CSS color, full list [here](http://www.w3.org/TR/css3-color/#svg-color) 90 | 91 | Timezone string 92 | 93 | - A string describing a timezone (e.g. `US/Pacific`, `Europe/Berlin`) 94 | - A string in ISO 8601 style (e.g. `07:30`, `-05:00`, `0630`, `-08`) 95 | 96 | ### Demo 97 | 98 | - Default 99 | 100 |  101 | 102 | - Smaller with fewer history days & languages 103 | 104 |  105 | 106 | - Customizing colors 107 | 108 |  109 | 110 | --- 111 | 112 | ## Deploy on your own server 113 | 114 | You can also deploy this project on your own server by following the instructions below. 115 | 116 | Prerequisites: 117 | 118 | - [Python 3.6 64-bit](https://www.python.org/downloads/) or later. (64-bit is required because [kaleido](https://github.com/plotly/Kaleido) is currently not providing 32-bit pre-compiled wheels) 119 | - **(Optional)** [Redis](https://redis.io/download/) for caching. If not installed, the app will use a file system cache instead, which is not very performant thus it is highly recommended that you configure a Redis server. 120 | - **(Optional)** [SVGO](https://github.com/svg/svgo) for optimizing SVGs (also requires [Node.js](https://nodejs.org/en/download/)) 121 | 122 | 1. Clone the project: `git clone https://github.com/WEGFan/codestats-profile-readme && cd codestats-profile-readme` 123 | 2. Install requirements: `pip install -r requirements.txt` 124 | 3. Edit your config in [config/custom_config.py](config/custom_config.py). 125 | 4. Run: `gunicorn -c gunicorn_config.py run:app`, and you should be able to access it by `http://127.0.0.1:2012` on your server 126 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | import logging.handlers 4 | from pathlib import Path 5 | 6 | from flask import Flask 7 | from marshmallow.exceptions import ValidationError 8 | from werkzeug.exceptions import HTTPException 9 | 10 | from app import config 11 | from app.cache import cache 12 | from app.controllers.history_graph import history_graph 13 | from app.controllers.home import home 14 | from app.error_handlers import http_exception, internal_server_error, user_not_found, validation_error 15 | from app.exceptions import UserNotFoundException 16 | 17 | 18 | def create_app(config_object=config.Config): 19 | app = Flask(__name__, root_path=config_object.APP_PATH) 20 | with app.app_context(): 21 | app.config.from_object(config_object) 22 | 23 | log_file_path = Path(app.config['LOG_PATH']) 24 | log_file_path.mkdir(parents=True, exist_ok=True) 25 | 26 | handler = logging.FileHandler(Path(log_file_path, app.config['LOG_FILENAME']), encoding='utf-8') 27 | handler.setFormatter(logging.Formatter(app.config['LOG_FORMAT'])) 28 | app.logger.setLevel(app.config['LOG_LEVEL']) 29 | app.logger.addHandler(handler) 30 | 31 | app.jinja_env.auto_reload = True 32 | 33 | app.register_blueprint(home) 34 | app.register_blueprint(history_graph) 35 | 36 | app.register_error_handler(UserNotFoundException, user_not_found) 37 | app.register_error_handler(ValidationError, validation_error) 38 | app.register_error_handler(HTTPException, http_exception) 39 | app.register_error_handler(Exception, internal_server_error) 40 | 41 | cache.init_app(app) 42 | 43 | return app 44 | -------------------------------------------------------------------------------- /app/cache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from typing import Optional 3 | 4 | from flask_caching import Cache 5 | from flask_caching.backends import RedisCache 6 | 7 | cache = Cache() 8 | 9 | 10 | def get_key_with_prefix(key: str, prefix: Optional[str]): 11 | if not prefix: 12 | return key 13 | return f'{prefix}/{key}' 14 | 15 | 16 | def set_redis_expire_seconds(key: str, expire: int): 17 | if not isinstance(cache.cache, RedisCache): 18 | return 19 | cache.cache._write_client.expire(name=cache.cache._get_prefix() + key, time=expire) 20 | -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | import os 4 | from pathlib import Path 5 | 6 | try: 7 | import config.custom_config_local as custom_config 8 | except ImportError as err: 9 | import config.custom_config as custom_config 10 | 11 | 12 | class Config(object): 13 | APP_PATH: str = str(Path(__file__).parent.resolve()) 14 | PROJECT_PATH: str = str(Path(APP_PATH).parent.resolve()) 15 | 16 | LOG_PATH: str = str(Path(PROJECT_PATH, custom_config.LOG_PATH).resolve()) 17 | LOG_FILENAME: str = custom_config.LOG_FILENAME 18 | LOG_FORMAT: str = '[%(asctime)s] %(levelname)s - [%(module)s] [%(filename)s:%(lineno)s]: %(message)s' 19 | LOG_LEVEL: int = logging.DEBUG if os.getenv('FLASK_DEBUG') == '1' else logging.INFO 20 | 21 | CACHE_REDIS_URL: str = custom_config.REDIS_URL 22 | 23 | if CACHE_REDIS_URL: 24 | CACHE_TYPE: str = 'redis' 25 | CACHE_KEY_PREFIX: str = 'codestats_readme:' 26 | else: 27 | # if redis url is empty just use filesystem cache 28 | CACHE_TYPE: str = 'filesystem' 29 | CACHE_DIR: str = str(Path(PROJECT_PATH, './data/cache').resolve()) 30 | CACHE_THRESHOLD: int = 500 31 | CACHE_DEFAULT_TIMEOUT: int = 15 32 | 33 | SVG_OPTIMIZE_ENABLE: bool = custom_config.SVG_OPTIMIZE_ENABLE 34 | SVGO_PATH: str = custom_config.SVGO_PATH 35 | SVGO_CONFIG_PATH: str = str(Path(PROJECT_PATH, custom_config.SVGO_CONFIG_PATH).resolve()) 36 | -------------------------------------------------------------------------------- /app/controllers/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /app/controllers/history_graph.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import bisect 3 | import math 4 | from typing import List 5 | 6 | import arrow 7 | import plotly.graph_objects as go 8 | from flask import Blueprint, current_app, request, Response 9 | from marshmallow import EXCLUDE, ValidationError 10 | 11 | from app.models.codestats_user import User 12 | from app.models.daily_language_xp import DailyLanguageXp 13 | from app.models.history_graph_config import GraphConfig 14 | from app.schemas.history_graph_config import GraphConfigSchema 15 | from app.utils.svgo import try_optimize_svg 16 | 17 | history_graph = Blueprint('history_graph', __name__, url_prefix='/history-graph') 18 | 19 | 20 | def calculate_best_range(y_max): 21 | exponent = math.floor(math.log10(y_max)) 22 | fraction = y_max / 10 ** exponent 23 | possible_fractions = [1, 2, 5, 10] 24 | new_fraction = possible_fractions[bisect.bisect_left(possible_fractions[:-1], fraction)] 25 | tick_delta = new_fraction * 10 ** (exponent - 1) 26 | return math.ceil(y_max / tick_delta) * tick_delta 27 | 28 | 29 | def get_graph(day_language_xp_list: List[DailyLanguageXp], config: GraphConfig): 30 | today = arrow.utcnow().to(config.timezone) 31 | first_day = today.shift(days=-config.history_days + 1) 32 | date_array = [day.date() for day in arrow.Arrow.range('day', first_day, today)] 33 | 34 | language_xp_dict = {} 35 | 36 | # accumulate xp per day for every language 37 | for obj in day_language_xp_list: 38 | try: 39 | pos = date_array.index(obj.date) 40 | except ValueError as err: 41 | continue 42 | xp_per_day = language_xp_dict.setdefault(obj.language, [0] * config.history_days) 43 | xp_per_day[pos] += obj.xp 44 | 45 | # sort by the sum of xp 46 | language_xp_list = list(language_xp_dict.items()) 47 | language_xp_list.sort(key=lambda k_v: sum(k_v[1]), reverse=True) 48 | 49 | # not using day_language_xp_list because it may contains data not in date range 50 | no_data = True if not language_xp_list else False 51 | 52 | # if language number exceeds max_languages, group them into others 53 | if len(language_xp_list) > config.max_languages: 54 | others_xp_per_day_list = [ 55 | xp_per_day 56 | for (language, xp_per_day) in language_xp_list[config.max_languages:] 57 | ] 58 | others_xp_per_day_sum = [ 59 | sum(x) for x in zip(*others_xp_per_day_list) 60 | ] 61 | language_xp_list = (language_xp_list[:config.max_languages] + 62 | [('Others', others_xp_per_day_sum)]) 63 | 64 | # calculate the best range for y axis 65 | if no_data: 66 | # if the user has no data 67 | max_daily_xp = 0 68 | else: 69 | max_daily_xp = max( 70 | sum(x) 71 | for x in zip(*[v for (k, v) in language_xp_list]) 72 | ) 73 | max_daily_xp = max(max_daily_xp, 1) # prevent math error caused by no data 74 | y_range = calculate_best_range(max_daily_xp) 75 | 76 | x_axis = [date.strftime('%b %e') for date in date_array] 77 | if no_data: 78 | # make a empty bar 79 | bars = [ 80 | go.Bar( 81 | name='', x=x_axis, y=[0] * config.history_days, width=0.7, 82 | marker=go.bar.Marker( 83 | line=go.bar.marker.Line( 84 | width=0 85 | ) 86 | ) 87 | ) 88 | ] 89 | else: 90 | bars = [ 91 | go.Bar( 92 | name=language, x=x_axis, y=xp_per_day, width=0.7, 93 | marker=go.bar.Marker( 94 | color=config.language_colors[idx % len(config.language_colors)], 95 | line=go.bar.marker.Line( 96 | width=0 97 | ) 98 | ) 99 | ) 100 | for (idx, (language, xp_per_day)) in enumerate(language_xp_list) 101 | ] 102 | 103 | fig = go.Figure( 104 | data=bars, 105 | layout=go.Layout( 106 | paper_bgcolor=config.bg_color, 107 | plot_bgcolor=config.bg_color, 108 | width=config.width, 109 | height=config.height, 110 | barmode='stack', 111 | showlegend=False if no_data else config.show_legend, 112 | legend=go.layout.Legend( 113 | traceorder='normal', 114 | x=1, 115 | font=go.layout.legend.Font( 116 | color=config.text_color 117 | ) 118 | ), 119 | xaxis=go.layout.XAxis( 120 | type='category', 121 | tickmode='array', 122 | ticks='outside', 123 | ticklen=4, 124 | tickwidth=1, 125 | tickcolor=config.grid_color, 126 | tickson='boundaries', 127 | tickfont=go.layout.xaxis.Tickfont( 128 | color=config.text_color, 129 | ), 130 | tickangle=-45, 131 | gridcolor=config.grid_color, 132 | gridwidth=1, 133 | dtick=1, 134 | showline=False, 135 | linecolor=config.grid_color 136 | ), 137 | yaxis=go.layout.YAxis( 138 | title=go.layout.yaxis.Title( 139 | text='XP', 140 | standoff=10, 141 | font=go.layout.yaxis.title.Font( 142 | color=config.text_color 143 | ) 144 | ), 145 | ticks='outside', 146 | ticklen=4, 147 | tickwidth=1, 148 | tickcolor=config.grid_color, 149 | tickformat=',d', 150 | tickfont=go.layout.yaxis.Tickfont( 151 | color=config.text_color 152 | ), 153 | gridcolor=config.grid_color, 154 | gridwidth=1, 155 | showline=True, 156 | linecolor=config.grid_color, 157 | zeroline=True, 158 | zerolinecolor=config.zeroline_color, 159 | zerolinewidth=1, 160 | separatethousands=True, 161 | range=[0, y_range] 162 | ), 163 | margin=go.layout.Margin( 164 | t=0, 165 | b=0, 166 | l=0, 167 | r=0, 168 | pad=0 169 | ) 170 | ) 171 | ) 172 | # manually add top border 173 | fig.add_shape( 174 | type='line', 175 | x0=-0.5, 176 | y0=y_range, 177 | x1=config.history_days - 0.5, 178 | y1=y_range, 179 | line=go.layout.shape.Line( 180 | color=config.grid_color, 181 | width=2 182 | ) 183 | ) 184 | 185 | return fig 186 | 187 | 188 | @history_graph.route('/Dynamically generated Code::Stats stats for your profile readme on Github.
13 |Check out https://github.com/WEGFan/codestats-profile-readme for more info.
14 | 15 | 16 | -------------------------------------------------------------------------------- /app/templates/validation_error.svg: -------------------------------------------------------------------------------- 1 | 39 | -------------------------------------------------------------------------------- /app/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /app/utils/svgo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import subprocess 3 | 4 | from flask import current_app 5 | 6 | 7 | def try_optimize_svg(original: str) -> str: 8 | if not current_app.config['SVG_OPTIMIZE_ENABLE']: 9 | return original 10 | args = [ 11 | current_app.config['SVGO_PATH'], 12 | '--input', '-', 13 | '--output', '-', 14 | '--config', current_app.config['SVGO_CONFIG_PATH'] 15 | ] 16 | try: 17 | with subprocess.Popen(args, encoding='utf-8', 18 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc: 19 | try: 20 | (stdout, stderr) = proc.communicate(original, timeout=3) 21 | if stderr: 22 | current_app.logger.warning('svg optimize error: %s', stderr) 23 | return original 24 | current_app.logger.info('svg optimized %d bytes -> %d bytes', len(original), len(stdout)) 25 | return stdout 26 | except subprocess.TimeoutExpired as err: 27 | current_app.logger.warning('svg optimize timeout') 28 | except Exception as err: 29 | current_app.logger.exception(err) 30 | return original 31 | -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WEGFan/codestats-profile-readme/0306049f4e3b4a6a1d26dc4ebec4a3d2f391a851/config/__init__.py -------------------------------------------------------------------------------- /config/custom_config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # NOTE: 4 | # You can duplicate this file as custom_config_local.py in this folder, the application will try to load config in 5 | # custom_config_local.py before custom_config.py. 6 | # 7 | # All paths below are relative to the project folder (which is where run.py at) 8 | # Absolute paths also supported. e.g. /var/log/ 9 | 10 | # WORKERS: The number of worker processes for handling requests. 11 | WORKERS: int = 4 12 | 13 | # LOG_PATH: Where log file stores in. Default is ./data/logs 14 | LOG_PATH: str = r'./data/logs' 15 | 16 | # LOG_FILENAME: The log file's filename. 17 | LOG_FILENAME: str = 'app.log' 18 | 19 | # REDIS_URL: The URL to connect to a Redis server. e.g. redis://user:password@localhost:6379/0 20 | # If empty, a filesystem cache will be used under ./data/cache 21 | REDIS_URL: str = '' 22 | 23 | # SVG_OPTIMIZE_ENABLE: Whether or not to optimize SVGs with SVGO before response. 24 | # npm and SVGO is needed to be installed. 25 | SVG_OPTIMIZE_ENABLE: bool = False 26 | 27 | # SVGO_PATH: Specifies the path to SVGO. 28 | SVGO_PATH: str = r'/usr/bin/svgo' 29 | 30 | # SVGO_CONFIG_PATH: Specifies the path to the config file of SVGO. 31 | SVGO_CONFIG_PATH: str = r'./config/svgo.yml' 32 | -------------------------------------------------------------------------------- /config/svgo.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/svg/svgo/blob/master/.svgo.yml 2 | multipass: false 3 | full: true 4 | floatPrecision: 2 5 | plugins: 6 | - cleanupAttrs: true 7 | - inlineStyles: true 8 | - removeDoctype: true 9 | - removeXMLProcInst: true 10 | - removeComments: true 11 | - removeMetadata: true 12 | - removeTitle: true 13 | - removeDesc: true 14 | - removeUselessDefs: true 15 | - removeXMLNS: false 16 | - removeEditorsNSData: true 17 | - removeEmptyAttrs: true 18 | - removeHiddenElems: true 19 | - removeEmptyText: true 20 | - removeEmptyContainers: true 21 | - removeViewBox: true 22 | - cleanupEnableBackground: true 23 | - minifyStyles: true 24 | - convertStyleToAttrs: true 25 | - convertColors: true 26 | - convertPathData: true 27 | - convertTransform: true 28 | - removeUnknownsAndDefaults: true 29 | - removeNonInheritableGroupAttrs: true 30 | - removeUselessStrokeAndFill: true 31 | - removeUnusedNS: true 32 | - prefixIds: false 33 | - cleanupIDs: true 34 | - cleanupNumericValues: true 35 | - cleanupListOfValues: false 36 | - moveElemsAttrsToGroup: true 37 | - moveGroupAttrsToElems: true 38 | - collapseGroups: true 39 | - removeRasterImages: true 40 | - mergePaths: true 41 | - convertShapeToPath: true 42 | - convertEllipseToCircle: true 43 | - sortAttrs: true 44 | - sortDefsChildren: true 45 | - removeDimensions: false 46 | - removeAttrs: false 47 | - removeAttributesBySelector: false 48 | - removeElementsByAttr: false 49 | - addClassesToSVGElement: false 50 | - addAttributesToSVGElement: false 51 | - removeOffCanvasPaths: true 52 | - removeStyleElement: true 53 | - removeScriptElement: true 54 | - reusePaths: false 55 | js2svg: 56 | pretty: false 57 | indent: 2 58 | -------------------------------------------------------------------------------- /gunicorn_config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | from pathlib import Path 4 | 5 | try: 6 | import config.custom_config_local as custom_config 7 | except ImportError as err: 8 | import config.custom_config as custom_config 9 | 10 | os.makedirs(custom_config.LOG_PATH, exist_ok=True) 11 | 12 | workers = custom_config.WORKERS 13 | worker_class = 'gevent' 14 | bind = '127.0.0.1:2012' 15 | daemon = True 16 | accesslog = str(Path(custom_config.LOG_PATH, 'gunicorn_access.log').resolve()) 17 | errorlog = str(Path(custom_config.LOG_PATH, 'gunicorn.log').resolve()) 18 | loglevel = 'info' 19 | reload = True 20 | timeout = 60 21 | access_log_format = '%(t)s %(l)s %({X-Real-IP}i)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' 22 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests~=2.24.0 2 | arrow~=0.15.7 3 | Flask~=1.1.2 4 | Flask-Caching~=1.9.0 5 | kaleido~=0.0.1 6 | marshmallow~=3.7.0 7 | plotly~=4.9.0 8 | gunicorn~=20.0.4; platform_system != 'Windows' 9 | redis~=3.5.3 10 | gevent~=20.6.2 -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from app import create_app 3 | 4 | app = create_app() 5 | 6 | if __name__ == '__main__': 7 | app.run() 8 | --------------------------------------------------------------------------------