This commit is contained in:
Armin Friedl 2020-09-12 12:58:38 +02:00
commit f78c9d349a
27 changed files with 4998 additions and 0 deletions

427
.gitignore vendored Normal file
View file

@ -0,0 +1,427 @@
# Created by https://www.toptal.com/developers/gitignore/api/python,flask,node,emacs,linux,windows,macos
# Edit at https://www.toptal.com/developers/gitignore?templates=python,flask,node,emacs,linux,windows,macos
### Emacs ###
# -*- mode: gitignore; -*-
*~
\#*\#
/.emacs.desktop
/.emacs.desktop.lock
*.elc
auto-save-list
tramp
.\#*
# Org-mode
.org-id-locations
*_archive
# flymake-mode
*_flymake.*
# eshell files
/eshell/history
/eshell/lastdir
# elpa packages
/elpa/
# reftex files
*.rel
# AUCTeX auto folder
/auto/
# cask packages
.cask/
dist/
# Flycheck
flycheck_*.el
# server auth directory
/server/
# projectiles files
.projectile
# directory configuration
.dir-locals.el
# network security
/network-security.data
### Flask ###
instance/*
!instance/.gitignore
.webassets-cache
### Flask.Python Stack ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
pytestdebug.log
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
doc/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
### Linux ###
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Node ###
# Logs
logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env.test
# parcel-bundler cache (https://parceljs.org/)
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
### Python ###
# Byte-compiled / optimized / DLL files
# C extensions
# Distribution / packaging
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
# Installer logs
# Unit test / coverage reports
# Translations
# Django stuff:
# Flask stuff:
# Scrapy stuff:
# Sphinx documentation
# PyBuilder
# Jupyter Notebook
# IPython
# pyenv
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
# Celery stuff
# SageMath parsed files
# Environments
# Spyder project settings
# Rope project settings
# mkdocs documentation
# mypy
# Pyre type checker
# pytype static type analyzer
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/python,flask,node,emacs,linux,windows,macos

15
Pipfile Normal file
View file

@ -0,0 +1,15 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
flask = "*"
walrus = "*"
flask-wtf = "*"
wtforms = "*"
[requires]
python_version = "3.8"

125
Pipfile.lock generated Normal file
View file

@ -0,0 +1,125 @@
{
"_meta": {
"hash": {
"sha256": "f58e03f82108622c44893db80327f71087a066a46a5232716aa9e9f3e0cfcdb5"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.8"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"click": {
"hashes": [
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
],
"version": "==7.1.2"
},
"flask": {
"hashes": [
"sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060",
"sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"
],
"index": "pypi",
"version": "==1.1.2"
},
"flask-wtf": {
"hashes": [
"sha256:57b3faf6fe5d6168bda0c36b0df1d05770f8e205e18332d0376ddb954d17aef2",
"sha256:d417e3a0008b5ba583da1763e4db0f55a1269d9dd91dcc3eb3c026d3c5dbd720"
],
"index": "pypi",
"version": "==0.14.3"
},
"itsdangerous": {
"hashes": [
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
],
"version": "==1.1.0"
},
"jinja2": {
"hashes": [
"sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
"sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
],
"version": "==2.11.2"
},
"markupsafe": {
"hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
],
"version": "==1.1.1"
},
"redis": {
"hashes": [
"sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",
"sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"
],
"version": "==3.5.3"
},
"walrus": {
"hashes": [
"sha256:6752420331b0110af6b3c6d32e61252dbafbd05ae2fc1a5fbfa6d42d2382062a"
],
"index": "pypi",
"version": "==0.8.1"
},
"werkzeug": {
"hashes": [
"sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43",
"sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"
],
"version": "==1.0.1"
},
"wtforms": {
"hashes": [
"sha256:7b504fc724d0d1d4d5d5c114e778ec88c37ea53144683e084215eed5155ada4c",
"sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c"
],
"index": "pypi",
"version": "==2.3.3"
}
},
"develop": {}
}

31
api.py Normal file
View file

@ -0,0 +1,31 @@
from flask import request, jsonify
from walrus import Walrus
from time import time
import uuid
import struct
from countdown import api_api
db = Walrus(host='localhost', port=6379, db=0)
@app.route('/api/vi1/<uuid:id>', methods=['GET'])
def get_countdown(id):
ct = db.Hash(str(id))
resp = ct.as_dict(decode=True)
resp['left'] = float(ct['total']) - (time() - float(ct['start']))
return resp
@api_v1.route('/api/v1', methods=['POST'])
def create_countdown():
countdown = request.json
ct_id = str(uuid.uuid4())
ct = db.Hash(ct_id)
ct.update(start=time(), total=countdown['total'])
resp = ct.as_dict(decode=True)
resp['id'] = ct_id
return resp

0
api/__init__.py Normal file
View file

7
api/v1/__init__.py Normal file
View file

@ -0,0 +1,7 @@
from flask import Blueprint
api_v1 = Blueprint('api.v1', __name__)
import api.v1.clock
import api.v1.countdown

7
api/v1/clock.py Normal file
View file

@ -0,0 +1,7 @@
from time import time
from api.v1 import api_v1
@api_v1.route('/time/<float:t1>')
def netime_time(t1: float) -> str:
return str(time())

31
api/v1/countdown.py Normal file
View file

@ -0,0 +1,31 @@
from flask import request, jsonify
from walrus import Walrus
from time import time
import uuid
import struct
from api.v1 import api_v1
db = Walrus(host='localhost', port=6379, db=0)
@api_v1.route('/countdown/<uuid:id>', methods=['GET'])
def get_countdown(id):
ct = db.Hash(str(id))
resp = ct.as_dict(decode=True)
resp['left'] = float(ct['total']) - (time() - float(ct['start']))
return resp
@api_v1.route('/countdown', methods=['POST'])
def create_countdown():
countdown = request.json
ct_id = str(uuid.uuid4())
ct = db.Hash(ct_id)
ct.update(start=time(), total=countdown['total'])
resp = ct.as_dict(decode=True)
resp['id'] = ct_id
return resp

6
countdown/__init__.py Normal file
View file

@ -0,0 +1,6 @@
from flask import Blueprint
app = Blueprint('countdown', __name__)
import views
import apii

4
countdown/forms.py Normal file
View file

@ -0,0 +1,4 @@
from flask_wtf import FlaskForm, TimeField
class CountdownAdminForm(FlaskForm):
totalTime = TimeField('Time')

19
countdown/views.py Normal file
View file

@ -0,0 +1,19 @@
from flask import render_template, request, flash, redirect, url_for
from countdown import app, forms
@app.route('/<uuid:id>', methods=['GET'])
def countdown(id):
return render_template('countdown.html', id)
@app.route('/', methods=['GET', 'POST', 'PUT'])
def countdown_admin():
form = CountdownAdminForm(request.form)
if request.method == 'POST' and form.validate():
user = User(form.username.data, form.email.data,
form.password.data)
db_session.add(user)
flash('Thanks for registering')
return redirect(url_for('login'))
return render_template('countdown_admin.html', form=form, clock=None)

3
css/netclock.scss Normal file
View file

@ -0,0 +1,3 @@
body {
color: red;
}

4
forms.py Normal file
View file

@ -0,0 +1,4 @@
from flask_wtf import FlaskForm, TimeField
class CountdownAdminForm(FlaskForm):
totalTime = TimeField('Time')

24
js/countdown.js Normal file
View file

@ -0,0 +1,24 @@
import log from 'loglevel';
import $ from 'jquery';
const updateCountdown = () => $.getJSON({
url: "api/v1/countdown/070f478f-e168-488c-918e-adb37c9c0cbd",
data: Date.now(),
success: function(countdown) {
if(countdown.left <= 0) {
clearInterval(scheduledUpdater);
$('#clock').text(0);
return;
}
let floor = Math.floor(countdown.left);
let frac = countdown.left - floor;
let milli = Math.floor(frac * 1000); // get in millisecond resolution
setTimeout(() => $('#clock').text(floor), milli);
}
});
let scheduledUpdater = setInterval(updateCountdown, 1000);

6
js/netclock.js Normal file
View file

@ -0,0 +1,6 @@
import '../css/netclock.scss';
import log from 'loglevel';
if (process.env.LOG_LEVEL) {
log.setDefaultLevel(process.env.LOG_LEVEL);
}

7
netclock.py Normal file
View file

@ -0,0 +1,7 @@
from flask import Flask
app = Flask(__name__)
from api.v1 import api_v1
app.register_blueprint(api_v1, url_prefix="/api/v1")
import views

4155
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

27
package.json Normal file
View file

@ -0,0 +1,27 @@
{
"name": "netclock",
"version": "0.0.1",
"description": "A collection of time widgets for the web",
"private": true,
"scripts": {
"build": "webpack --config webpack.dev.js",
"watch": "webpack --watch --config webpack.dev.js",
"publish": "webpack --config webpack.prod.js"
},
"author": "Armin Friedl",
"license": "MIT",
"devDependencies": {
"clean-webpack-plugin": "^3.0.0",
"css-loader": "^4.2.2",
"sass": "^1.26.10",
"sass-loader": "^10.0.2",
"style-loader": "^1.2.1",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12",
"webpack-merge": "^5.1.3"
},
"dependencies": {
"jquery": "^3.5.1",
"loglevel": "^1.7.0"
}
}

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

16
templates/base.html Normal file
View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Netclock {% if self.title() %} - {% endif %}{% block title %}{% endblock title %}</title>
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
</head>
<body>
{% block body %}{% endblock body %}
<script src="{{ url_for('static', filename='dist/netclock.bundle.js') }}"></script>
{% block scripts %}{% endblock scripts %}
</body>
</html>

11
templates/countdown.html Normal file
View file

@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block title %}Countdown{% endblock title %}
{% block body %}
Hello from Countdown
<div id="clock"></div>
{% endblock body %}
{% block scripts %}
<script src="{{ url_for('static', filename='dist/countdown.bundle.js') }}"></script>
{% endblock scripts %}

View file

@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block title %}Countdown{% endblock title %}
{% block body %}
Hello from Countdown
<div id="clock"></div>
{% endblock body %}
{% block scripts %}
<script src="{{ url_for('static', filename='dist/countdown.bundle.js') }}"></script>
{% endblock scripts %}

3
templates/netclock.html Normal file
View file

@ -0,0 +1,3 @@
{% extends "base.html" %}
{% block body %}Index{% endblock body %}

9
views.py Normal file
View file

@ -0,0 +1,9 @@
from flask import Flask, render_template, request, flash
from netclock import app
from forms import CountdownAdminForm
@app.route('/')
def index():
return render_template('netclock.html')

29
webpack.common.js Normal file
View file

@ -0,0 +1,29 @@
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const path = require('path');
module.exports = {
entry: {
netclock: './js/netclock.js',
countdown: './js/countdown.js'
},
plugins: [
new CleanWebpackPlugin()
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'static', 'dist')
},
module: {
rules: [{
test: /\.s[ac]ss$/i,
use: [
// Creates `style` nodes from JS strings
'style-loader',
// Translates CSS into CommonJS
'css-loader',
// Compiles Sass to CSS
'sass-loader',
]
}]
}
};

14
webpack.dev.js Normal file
View file

@ -0,0 +1,14 @@
const common = require('./webpack.common.js');
const webpack = require('webpack');
const { merge } = require('webpack-merge');
const path = require('path');
module.exports = merge(common, {
mode: 'development',
devtool: 'eval-source-map',
plugins: [
new webpack.EnvironmentPlugin({
LOG_LEVEL: 'trace'
})
]
});

7
webpack.prod.js Normal file
View file

@ -0,0 +1,7 @@
const common = require('./webpack.common.js');
const { merge } = require('webpack-merge');
const path = require('path');
module.exports = merge(common, {
mode: 'production'
});