From fefbee83e46c8b15641e19feeabdc193d230a571 Mon Sep 17 00:00:00 2001 From: retoor Date: Tue, 3 Dec 2024 14:16:44 +0100 Subject: [PATCH] Initial commit. --- .gitea/workflows/build.yaml | 26 ++++ .gitignore | 5 + Makefile | 40 +++++ dist/app-1.0.0-py3-none-any.whl | Bin 0 -> 6935 bytes dist/app-1.0.0.tar.gz | Bin 0 -> 5657 bytes pyproject.toml | 3 + setup.cfg | 29 ++++ src/app.egg-info/PKG-INFO | 12 ++ src/app.egg-info/SOURCES.txt | 17 +++ src/app.egg-info/dependency_links.txt | 1 + src/app.egg-info/entry_points.txt | 5 + src/app.egg-info/requires.txt | 3 + src/app.egg-info/top_level.txt | 1 + src/app/__init__.py | 9 ++ src/app/__main__.py | 13 ++ src/app/app.py | 210 ++++++++++++++++++++++++++ src/app/args.py | 36 +++++ src/app/cli.py | 52 +++++++ src/app/kim.py | 93 ++++++++++++ src/app/repl.py | 13 ++ src/app/server.py | 17 +++ src/app/tests.py | 20 +++ 22 files changed, 605 insertions(+) create mode 100644 .gitea/workflows/build.yaml create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 dist/app-1.0.0-py3-none-any.whl create mode 100644 dist/app-1.0.0.tar.gz create mode 100644 pyproject.toml create mode 100644 setup.cfg create mode 100644 src/app.egg-info/PKG-INFO create mode 100644 src/app.egg-info/SOURCES.txt create mode 100644 src/app.egg-info/dependency_links.txt create mode 100644 src/app.egg-info/entry_points.txt create mode 100644 src/app.egg-info/requires.txt create mode 100644 src/app.egg-info/top_level.txt create mode 100644 src/app/__init__.py create mode 100644 src/app/__main__.py create mode 100644 src/app/app.py create mode 100644 src/app/args.py create mode 100644 src/app/cli.py create mode 100644 src/app/kim.py create mode 100644 src/app/repl.py create mode 100644 src/app/server.py create mode 100644 src/app/tests.py diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml new file mode 100644 index 0000000..53a035d --- /dev/null +++ b/.gitea/workflows/build.yaml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5305b94 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.vscode +.history +.venv +__pycache__ +.trigger-2024-12-02 13:37:42 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8f6c50d --- /dev/null +++ b/Makefile @@ -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 diff --git a/dist/app-1.0.0-py3-none-any.whl b/dist/app-1.0.0-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..4fafb46ce70d6e53c49f15d6b5e9d1a759af8063 GIT binary patch literal 6935 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 literal 0 HcmV?d00001 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..07de284 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..28d1c3d --- /dev/null +++ b/setup.cfg @@ -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 diff --git a/src/app.egg-info/PKG-INFO b/src/app.egg-info/PKG-INFO new file mode 100644 index 0000000..82bc7ec --- /dev/null +++ b/src/app.egg-info/PKG-INFO @@ -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 diff --git a/src/app.egg-info/SOURCES.txt b/src/app.egg-info/SOURCES.txt new file mode 100644 index 0000000..33692c7 --- /dev/null +++ b/src/app.egg-info/SOURCES.txt @@ -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 \ No newline at end of file diff --git a/src/app.egg-info/dependency_links.txt b/src/app.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/app.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/app.egg-info/entry_points.txt b/src/app.egg-info/entry_points.txt new file mode 100644 index 0000000..ed722fe --- /dev/null +++ b/src/app.egg-info/entry_points.txt @@ -0,0 +1,5 @@ +[console_scripts] +bench = app.cli:cli_bench +cli = app.cli:main +repl = app.repl:repl +serve = app.__main__:main diff --git a/src/app.egg-info/requires.txt b/src/app.egg-info/requires.txt new file mode 100644 index 0000000..d9ca351 --- /dev/null +++ b/src/app.egg-info/requires.txt @@ -0,0 +1,3 @@ +aiohttp +dataset +zhurnal@ git+https://retoor.molodetz.nl/retoor/zhurnal.git@main diff --git a/src/app.egg-info/top_level.txt b/src/app.egg-info/top_level.txt new file mode 100644 index 0000000..b80f0bd --- /dev/null +++ b/src/app.egg-info/top_level.txt @@ -0,0 +1 @@ +app diff --git a/src/app/__init__.py b/src/app/__init__.py new file mode 100644 index 0000000..7e315cc --- /dev/null +++ b/src/app/__init__.py @@ -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__) diff --git a/src/app/__main__.py b/src/app/__main__.py new file mode 100644 index 0000000..e7713f8 --- /dev/null +++ b/src/app/__main__.py @@ -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() + diff --git a/src/app/app.py b/src/app/app.py new file mode 100644 index 0000000..25277e2 --- /dev/null +++ b/src/app/app.py @@ -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 diff --git a/src/app/args.py b/src/app/args.py new file mode 100644 index 0000000..57cb6f0 --- /dev/null +++ b/src/app/args.py @@ -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() + + diff --git a/src/app/cli.py b/src/app/cli.py new file mode 100644 index 0000000..d5980af --- /dev/null +++ b/src/app/cli.py @@ -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() + + + + diff --git a/src/app/kim.py b/src/app/kim.py new file mode 100644 index 0000000..df61755 --- /dev/null +++ b/src/app/kim.py @@ -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: ")) diff --git a/src/app/repl.py b/src/app/repl.py new file mode 100644 index 0000000..468d723 --- /dev/null +++ b/src/app/repl.py @@ -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() diff --git a/src/app/server.py b/src/app/server.py new file mode 100644 index 0000000..647f214 --- /dev/null +++ b/src/app/server.py @@ -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) diff --git a/src/app/tests.py b/src/app/tests.py new file mode 100644 index 0000000..914c583 --- /dev/null +++ b/src/app/tests.py @@ -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)