This commit is contained in:
commit
fefbee83e4
26
.gitea/workflows/build.yaml
Normal file
26
.gitea/workflows/build.yaml
Normal file
@ -0,0 +1,26 @@
|
||||
name: Build Base Application
|
||||
run-name: Build Base Application
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
Build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
- name: Update repo
|
||||
run: git pull
|
||||
- name: List files in the repository
|
||||
run: |
|
||||
ls ${{ gitea.workspace }}
|
||||
- name: Update apt
|
||||
run: apt update
|
||||
- name: Installing system dependencies
|
||||
run: apt install python3 python3-pip python3-venv make -y
|
||||
- name: Run Make
|
||||
- run: make
|
||||
- run: git add .
|
||||
- run: git config --global user.email "bot@molodetz.com"
|
||||
- run: git config --global user.name "bot"
|
||||
- run: git commit -a -m "Automated update of Base Application package."
|
||||
- run: git push
|
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
.vscode
|
||||
.history
|
||||
.venv
|
||||
__pycache__
|
||||
.trigger-2024-12-02 13:37:42
|
40
Makefile
Normal file
40
Makefile
Normal file
@ -0,0 +1,40 @@
|
||||
BIN = ./.venv/bin/
|
||||
PYTHON = ./.venv/bin/python
|
||||
PIP = ./.venv/bin/pip
|
||||
|
||||
APP_NAME=app
|
||||
|
||||
all: install build test
|
||||
|
||||
ensure_repo:
|
||||
-@git init
|
||||
|
||||
ensure_env: ensure_repo
|
||||
-@python3 -m venv .venv
|
||||
|
||||
install: ensure_env
|
||||
$(PIP) install -e .
|
||||
|
||||
format: ensure_env
|
||||
$(PIP) install shed
|
||||
. $(BIN)/activate && shed
|
||||
|
||||
build: ensure_env
|
||||
$(MAKE) format
|
||||
$(PIP) install build
|
||||
$(PYTHON) -m build
|
||||
|
||||
serve: ensure_env
|
||||
$(BIN)serve --host=0.0.0.0 --port=8888
|
||||
|
||||
run: ensure_env
|
||||
$(BIN)zhurnal "make serve" "make bench"
|
||||
|
||||
bench: ensure_env
|
||||
$(BIN)bench --url=http://127.0.0.1:8888/
|
||||
|
||||
cli: ensure_env
|
||||
$(BIN)cli --url=wss://localhost:8888
|
||||
|
||||
test: ensure_env
|
||||
$(PYTHON) -m unittest $(APP_NAME).tests
|
BIN
dist/app-1.0.0-py3-none-any.whl
vendored
Normal file
BIN
dist/app-1.0.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
dist/app-1.0.0.tar.gz
vendored
Normal file
BIN
dist/app-1.0.0.tar.gz
vendored
Normal file
Binary file not shown.
3
pyproject.toml
Normal file
3
pyproject.toml
Normal file
@ -0,0 +1,3 @@
|
||||
[build-system]
|
||||
requires = ["setuptools", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
29
setup.cfg
Normal file
29
setup.cfg
Normal file
@ -0,0 +1,29 @@
|
||||
[metadata]
|
||||
name = app
|
||||
version = 1.0.0
|
||||
description = Base application
|
||||
author = retoor
|
||||
author_email = retoor@molodetz.nl
|
||||
license = MIT
|
||||
long_description = file: README.md
|
||||
long_description_content_type = text/markdown
|
||||
|
||||
[options]
|
||||
packages = find:
|
||||
package_dir =
|
||||
= src
|
||||
python_requires = >=3.7
|
||||
install_requires =
|
||||
aiohttp
|
||||
dataset
|
||||
zhurnal @ git+https://retoor.molodetz.nl/retoor/zhurnal.git@main
|
||||
|
||||
[options.packages.find]
|
||||
where = src
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
serve = app.__main__:main
|
||||
bench = app.cli:cli_bench
|
||||
cli = app.cli:main
|
||||
repl = app.repl:repl
|
12
src/app.egg-info/PKG-INFO
Normal file
12
src/app.egg-info/PKG-INFO
Normal file
@ -0,0 +1,12 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: app
|
||||
Version: 1.0.0
|
||||
Summary: Base application
|
||||
Author: retoor
|
||||
Author-email: retoor@molodetz.nl
|
||||
License: MIT
|
||||
Requires-Python: >=3.7
|
||||
Description-Content-Type: text/markdown
|
||||
Requires-Dist: aiohttp
|
||||
Requires-Dist: dataset
|
||||
Requires-Dist: zhurnal@ git+https://retoor.molodetz.nl/retoor/zhurnal.git@main
|
17
src/app.egg-info/SOURCES.txt
Normal file
17
src/app.egg-info/SOURCES.txt
Normal file
@ -0,0 +1,17 @@
|
||||
pyproject.toml
|
||||
setup.cfg
|
||||
src/app/__init__.py
|
||||
src/app/__main__.py
|
||||
src/app/app.py
|
||||
src/app/args.py
|
||||
src/app/cli.py
|
||||
src/app/kim.py
|
||||
src/app/repl.py
|
||||
src/app/server.py
|
||||
src/app/tests.py
|
||||
src/app.egg-info/PKG-INFO
|
||||
src/app.egg-info/SOURCES.txt
|
||||
src/app.egg-info/dependency_links.txt
|
||||
src/app.egg-info/entry_points.txt
|
||||
src/app.egg-info/requires.txt
|
||||
src/app.egg-info/top_level.txt
|
1
src/app.egg-info/dependency_links.txt
Normal file
1
src/app.egg-info/dependency_links.txt
Normal file
@ -0,0 +1 @@
|
||||
|
5
src/app.egg-info/entry_points.txt
Normal file
5
src/app.egg-info/entry_points.txt
Normal file
@ -0,0 +1,5 @@
|
||||
[console_scripts]
|
||||
bench = app.cli:cli_bench
|
||||
cli = app.cli:main
|
||||
repl = app.repl:repl
|
||||
serve = app.__main__:main
|
3
src/app.egg-info/requires.txt
Normal file
3
src/app.egg-info/requires.txt
Normal file
@ -0,0 +1,3 @@
|
||||
aiohttp
|
||||
dataset
|
||||
zhurnal@ git+https://retoor.molodetz.nl/retoor/zhurnal.git@main
|
1
src/app.egg-info/top_level.txt
Normal file
1
src/app.egg-info/top_level.txt
Normal file
@ -0,0 +1 @@
|
||||
app
|
9
src/app/__init__.py
Normal file
9
src/app/__init__.py
Normal file
@ -0,0 +1,9 @@
|
||||
import logging
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
13
src/app/__main__.py
Normal file
13
src/app/__main__.py
Normal file
@ -0,0 +1,13 @@
|
||||
from aiohttp import web
|
||||
from .app import create_app
|
||||
from .args import parse_args
|
||||
from .server import serve
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
serve(args.host, args.port)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
210
src/app/app.py
Normal file
210
src/app/app.py
Normal file
@ -0,0 +1,210 @@
|
||||
from aiohttp import web
|
||||
import time
|
||||
from . import log
|
||||
import json
|
||||
import uuid
|
||||
import dataset
|
||||
|
||||
def get_timestamp():
|
||||
from datetime import datetime
|
||||
now = datetime.now()
|
||||
formatted_datetime = now.strftime("%Y-%m-%d %H:%M:%S")
|
||||
return formatted_datetime
|
||||
|
||||
class BaseApplication(web.Application):
|
||||
|
||||
def __init__(self, username = None, password=None, cookie_name=None,session=None, *args, **kwargs):
|
||||
self.cookie_name = cookie_name or str(uuid.uuid4())
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.session = session or {}
|
||||
middlewares = kwargs.pop("middlewares",[])
|
||||
middlewares.append(self.request_middleware)
|
||||
middlewares.append(self.base64_auth_middleware)
|
||||
middlewares.append(self.session_middleware)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
async def authenticate(self, username, password):
|
||||
return self.username == username and self.password == password
|
||||
|
||||
@web.middleware
|
||||
async def base64_auth_middleware(request, handler):
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if not self.username:
|
||||
return await handler(request)
|
||||
if not auth_header or not auth_header.startswith("Basic "):
|
||||
return web.Response(
|
||||
status=401,
|
||||
text="Unauthorized",
|
||||
headers={"WWW-Authenticate": 'Basic realm="Restricted"'}
|
||||
)
|
||||
|
||||
try:
|
||||
encoded_credentials = auth_header.split(" ", 1)[1]
|
||||
decoded_credentials = base64.b64decode(encoded_credentials).decode("utf-8")
|
||||
username, password = decoded_credentials.split(":", 1)
|
||||
except (ValueError, base64.binascii.Error):
|
||||
return web.Response(status=400, text="Invalid Authorization Header")
|
||||
|
||||
if not await self.authenticate(username, password):
|
||||
return web.Response(
|
||||
status=401,
|
||||
text="Invalid Credentials",
|
||||
headers={"WWW-Authenticate": 'Basic realm="Restricted"'}
|
||||
)
|
||||
|
||||
return await handler(request)
|
||||
@web.middleware
|
||||
async def request_middleware(self, request, handler):
|
||||
time_start = time.time()
|
||||
created = get_timestamp()
|
||||
request = await handler(request)
|
||||
time_end = time.time()
|
||||
await self.insert("http_access",dict(
|
||||
created=created,
|
||||
path=request.path,
|
||||
duration=time_end - time_start,
|
||||
))
|
||||
return request
|
||||
|
||||
@web.middleware
|
||||
async def session_middleware(self,request, handler):
|
||||
# Process the request and get the response
|
||||
cookies = request.cookies
|
||||
session_id = cookies.get(self.cookie_name, None)
|
||||
setattr(request,"session", self.session.get(session_id,{}))
|
||||
response = await handler(request)
|
||||
|
||||
if not session_id:
|
||||
session_id = str(uuid.uuid4())
|
||||
response.set_cookie(
|
||||
self.cookie_name,
|
||||
session_id,
|
||||
max_age=3600,
|
||||
httponly=True
|
||||
)
|
||||
return response
|
||||
|
||||
class WebDbApplication(BaseApplication):
|
||||
|
||||
def __init__(self, db=None, db_web=False,db_path="sqlite:///:memory:",*args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.db_web = db_web
|
||||
self.db_path = db_path
|
||||
self.db = db or dataset.connect(self.db_path)
|
||||
if not self.db_web:
|
||||
return
|
||||
self.router.add_post("/insert", self.insert_handler)
|
||||
self.router.add_post("/update", self.update_handler)
|
||||
self.router.add_post("/upsert", self.upsert_handler)
|
||||
self.router.add_post("/find", self.find_handler)
|
||||
self.router.add_post("/find_one", self.find_one_handler)
|
||||
self.router.add_post("/delete", self.delete_handler)
|
||||
|
||||
async def insert_handler(self, request):
|
||||
obj = await request.json()
|
||||
response = await self.insert(request.get("table"), request.get("data"))
|
||||
return web.json_response(response)
|
||||
|
||||
async def update_handler(self, request):
|
||||
obj = await request.json()
|
||||
response = await self.update(request.get('table'), request.get('data'), request.get('where',{}))
|
||||
return web.json_response(response)
|
||||
|
||||
async def upsert_handler(self, request):
|
||||
obj = await request.json()
|
||||
response = await self.upsert(request.get('table'), request.get('data'), request.get('keys',[]))
|
||||
return web.json_response(response)
|
||||
|
||||
async def find_handler(self, request):
|
||||
obj = await request.json()
|
||||
response = await self.find(request.get('table'), requesst.get('where',{}))
|
||||
return web.json_response(response)
|
||||
|
||||
async def find_one_handler(self, request):
|
||||
obj = await request.json()
|
||||
response = await self.find_one(request.get('table'), requesst.get('where',{}))
|
||||
return web.json_response(response)
|
||||
|
||||
async def delete_handler(self, request):
|
||||
obj = await request.json()
|
||||
response = await self.delete(request.get('table'), requesst.get('where',{}))
|
||||
return web.json_response(response)
|
||||
|
||||
async def set(self, key, value):
|
||||
value = json.dumps(value, default=str)
|
||||
self.db['kv'].upsert(dict(key=key,value=value),['key'])
|
||||
|
||||
async def get(self, key, default=None):
|
||||
record = self.db['kv'].find_one(key=key)
|
||||
if record:
|
||||
return json.loads(record.get('value','null'))
|
||||
return default
|
||||
|
||||
async def insert(self, table_name, data):
|
||||
return self.db[table_name].insert(data)
|
||||
|
||||
async def update(self, table_name, data, where):
|
||||
return self.db[table_name].update(data,where)
|
||||
|
||||
async def upsert(self, table_name, data, keys):
|
||||
return self.db[table_name].upsert(data,keys or [])
|
||||
|
||||
async def find(self, table_name, filters):
|
||||
if not filters:
|
||||
filters = {}
|
||||
return [dict(record) for record in self.db[table_name].find(**filters)]
|
||||
|
||||
async def find_one(self, table_name, filters):
|
||||
if not filters:
|
||||
filters = {}
|
||||
try:
|
||||
return dict(self.db[table_name].find_one(**filters))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
async def delete(self, table_name, where):
|
||||
return self.db[table_name].delete(**where)
|
||||
|
||||
|
||||
|
||||
|
||||
class Application(WebDbApplication):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.on_startup.append(self.on_startup_task)
|
||||
self.router.add_get("/", self.index_handler)
|
||||
self.request_count = 0
|
||||
self.time_started = time.time()
|
||||
self.running_since = None
|
||||
|
||||
@property
|
||||
def uptime(self):
|
||||
return time.time() - self.time_started
|
||||
|
||||
async def on_startup_task(self, app):
|
||||
log.debug("App starting.")
|
||||
self.running_since = get_timestamp()
|
||||
|
||||
async def inc_request_count(self):
|
||||
request_count = await self.get("root_request_count",0)
|
||||
request_count += 1
|
||||
await self.set("root_request_count", request_count)
|
||||
return request_count
|
||||
|
||||
async def index_handler(self,request):
|
||||
|
||||
return web.json_response(dict(
|
||||
request_count=await self.inc_request_count(),
|
||||
timestamp=get_timestamp(),
|
||||
uptime=self.uptime,
|
||||
running_since=self.running_since
|
||||
),content_type="application/json")
|
||||
|
||||
|
||||
def create_app(*args, **kwargs):
|
||||
app = Application(*args, **kwargs)
|
||||
|
||||
return app
|
36
src/app/args.py
Normal file
36
src/app/args.py
Normal file
@ -0,0 +1,36 @@
|
||||
import argparse
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Async web service"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--host',
|
||||
type=str,
|
||||
required=False,
|
||||
default="0.0.0.0",
|
||||
help='Host to serve on. Default: 0.0.0.0.'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--port',
|
||||
type=int,
|
||||
required=False,
|
||||
default=8888,
|
||||
help='Port to serve on Default: 8888.'
|
||||
)
|
||||
|
||||
|
||||
|
||||
parser.add_argument(
|
||||
'--url',
|
||||
type=str,
|
||||
default="http://localhost:8888",
|
||||
required=False,
|
||||
help='Base URL.'
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
52
src/app/cli.py
Normal file
52
src/app/cli.py
Normal file
@ -0,0 +1,52 @@
|
||||
import asyncio
|
||||
from aiohttp import ClientSession
|
||||
from .args import parse_args
|
||||
import time
|
||||
from . import log
|
||||
|
||||
|
||||
|
||||
|
||||
async def cli_client(url):
|
||||
while True:
|
||||
sentence = input("> ")
|
||||
async with ClientSession() as session:
|
||||
async with session.post("http://localhost:8080",json=sentence) as response:
|
||||
try:
|
||||
print(await response.json())
|
||||
except Exception as ex:
|
||||
print(ex)
|
||||
print(await response.text())
|
||||
|
||||
async def bench(url):
|
||||
index = 0
|
||||
while True:
|
||||
index += 1
|
||||
try:
|
||||
time_start = time.time()
|
||||
|
||||
async with ClientSession() as session:
|
||||
async with session.get(url) as response:
|
||||
print(await response.text())
|
||||
#print(await response.json())
|
||||
time_end = time.time()
|
||||
print("Request {}. Duration: {}".format(index,time_end-time_start))
|
||||
except Exception as ex:
|
||||
log.exception(ex)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
def cli_bench():
|
||||
args = parse_args()
|
||||
asyncio.run(bench(args.url))
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
asyncio.run(cli_client(args.url))
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
|
||||
|
||||
|
93
src/app/kim.py
Normal file
93
src/app/kim.py
Normal file
@ -0,0 +1,93 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import sys
|
||||
import shutil
|
||||
from readchar import readkey
|
||||
|
||||
|
||||
def text_editor(init='', prompt=''):
|
||||
'''
|
||||
Allow user to edit a line of text complete with support for line wraps
|
||||
and a cursor | you can move back and forth with the arrow keys.
|
||||
init = initial text supplied to edit
|
||||
prompt = Decoration presented before the text (not editable and not returned)
|
||||
'''
|
||||
|
||||
term_width = shutil.get_terminal_size()[0]
|
||||
ptr = len(init)
|
||||
text = list(init)
|
||||
prompt = list(prompt)
|
||||
|
||||
c = 0
|
||||
while True:
|
||||
if ptr and ptr > len(text):
|
||||
ptr = len(text)
|
||||
|
||||
copy = prompt + text.copy()
|
||||
if ptr < len(text):
|
||||
copy.insert(ptr + len(prompt), '|')
|
||||
|
||||
# Line wraps support:
|
||||
if len(copy) > term_width:
|
||||
cut = len(copy) + 3 - term_width
|
||||
if ptr > len(copy) / 2:
|
||||
copy = ['<'] * 3 + copy[cut:]
|
||||
else:
|
||||
copy = copy[:-cut] + ['>'] * 3
|
||||
|
||||
|
||||
# Display current line
|
||||
print('\r' * term_width + ''.join(copy), end=' ' * (term_width - len(copy)))
|
||||
|
||||
|
||||
# Read new character into c
|
||||
if c in (53, 54):
|
||||
# Page up/down bug
|
||||
c = readkey()
|
||||
if c == '~':
|
||||
continue
|
||||
else:
|
||||
c = readkey()
|
||||
|
||||
if len(c) > 1:
|
||||
# Control Character
|
||||
c = ord(c[-1])
|
||||
if c == 68: # Left
|
||||
ptr -= 1
|
||||
elif c == 67: # Right
|
||||
ptr += 1
|
||||
elif c == 53: # PgDn
|
||||
ptr -= term_width // 2
|
||||
elif c == 54: # PgUp
|
||||
ptr += term_width // 2
|
||||
elif c == 70: # End
|
||||
ptr = len(text)
|
||||
elif c == 72: # Home
|
||||
ptr = 0
|
||||
else:
|
||||
print("\nUnknown control character:", c)
|
||||
print("Press ctrl-c to quit.")
|
||||
continue
|
||||
if ptr < 0:
|
||||
ptr = 0
|
||||
if ptr > len(text):
|
||||
ptr = len(text)
|
||||
|
||||
else:
|
||||
num = ord(c)
|
||||
if num in (13, 10): # Enter
|
||||
print()
|
||||
return ''.join(text)
|
||||
elif num == 127: # Backspace
|
||||
if text:
|
||||
text.pop(ptr - 1)
|
||||
ptr -= 1
|
||||
elif num == 3: # Ctrl-C
|
||||
sys.exit(1)
|
||||
else:
|
||||
# Insert normal character into text.
|
||||
text.insert(ptr, c)
|
||||
ptr += 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Result =", text_editor('Edit this text', prompt="Prompt: "))
|
13
src/app/repl.py
Normal file
13
src/app/repl.py
Normal file
@ -0,0 +1,13 @@
|
||||
import code
|
||||
|
||||
|
||||
def repl(**kwargs):
|
||||
varlables = {}
|
||||
variables.update(globals().copy())
|
||||
variables.update(locals())
|
||||
variables.update(kwargs)
|
||||
code.interact(local=variables)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
repl()
|
17
src/app/server.py
Normal file
17
src/app/server.py
Normal file
@ -0,0 +1,17 @@
|
||||
from aiohttp import web
|
||||
from .app import create_app
|
||||
from . import log
|
||||
import time
|
||||
|
||||
def serve(host="0.0.0.0",port=8888):
|
||||
app = create_app()
|
||||
log.info("Serving on {}:{}".format(host,port))
|
||||
while True:
|
||||
try:
|
||||
web.run_app(app,host=host,port=port)
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
except Exception as ex:
|
||||
log.exception(ex)
|
||||
log.error("Server crashed. Waiting one second before reboot")
|
||||
time.sleep(1)
|
20
src/app/tests.py
Normal file
20
src/app/tests.py
Normal file
@ -0,0 +1,20 @@
|
||||
from .app import WebDbApplication
|
||||
import unittest
|
||||
import asyncio
|
||||
|
||||
|
||||
|
||||
class WebDbApplicationTestCase(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
|
||||
def setUp(self):
|
||||
self.db = WebDbApplication()
|
||||
|
||||
async def test_insert(self):
|
||||
|
||||
self.assertEqual(await self.db.insert("test",dict(test=True)), 1)
|
||||
self.assertEqual(len(await self.db.find("test",dict(test=True, id=1))), 1)
|
||||
|
||||
async def test_find(self):
|
||||
print(await self.db.find("test",None))
|
||||
# self.assertEqual(len(await self.db.find("test",dict(test=True, id=1))), 1)
|
Loading…
Reference in New Issue
Block a user