Complete system.

This commit is contained in:
retoor 2025-01-24 14:00:10 +01:00
parent ba83922660
commit d20079f3ed
39 changed files with 663 additions and 299 deletions

3
.gitignore vendored
View File

@ -1,5 +1,8 @@
.vscode .vscode
.history .history
.resources
.backup*
docs
*.db* *.db*
*.png *.png
# ---> Python # ---> Python

View File

@ -1,5 +1,5 @@
FROM surnet/alpine-wkhtmltopdf:3.21.2-0.12.6-full as wkhtmltopdf FROM surnet/alpine-wkhtmltopdf:3.21.2-0.12.6-full as wkhtmltopdf
FROM python:3.10-alpine FROM python:3.12.8-alpine3.21
WORKDIR /code WORKDIR /code
ENV FLASK_APP=app.py ENV FLASK_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0 ENV FLASK_RUN_HOST=0.0.0.0
@ -37,5 +37,5 @@ RUN pip install --upgrade pip
RUN pip install -e . RUN pip install -e .
EXPOSE 8081 EXPOSE 8081
python -m snek.app CMD ["python","-m","snek.app"]
#CMD ["gunicorn", "-w", "10", "-k", "aiohttp.worker.GunicornWebWorker", "snek.gunicorn:app","--bind","0.0.0.0:8081"] #CMD ["gunicorn", "-w", "10", "-k", "aiohttp.worker.GunicornWebWorker", "snek.gunicorn:app","--bind","0.0.0.0:8081"]

View File

@ -20,6 +20,8 @@ dependencies = [
"beautifulsoup4", "beautifulsoup4",
"gunicorn", "gunicorn",
"imgkit", "imgkit",
"wkhtmltopdf" "wkhtmltopdf",
"jinja-markdown2",
"mistune"
] ]

View File

@ -3,14 +3,16 @@ import pathlib
from aiohttp import web from aiohttp import web
from app.app import Application as BaseApplication from app.app import Application as BaseApplication
from jinja_markdown2 import MarkdownExtension
from snek.system import http from snek.system import http
from snek.system.middleware import cors_middleware from snek.system.middleware import cors_middleware
from snek.view.about import AboutHTMLView, AboutMDView
from snek.view.index import IndexView from snek.view.index import IndexView
from snek.view.login import LoginView from snek.view.login import LoginView
from snek.view.login_form import LoginFormView from snek.view.login_form import LoginFormView
from snek.view.register import RegisterView from snek.view.register import RegisterView
from snek.view.register_form import RegisterFormView from snek.view.register_form import RegisterFormView
from snek.view.view import WebView from snek.view.web import WebView
class Application(BaseApplication): class Application(BaseApplication):
@ -24,6 +26,7 @@ class Application(BaseApplication):
super().__init__( super().__init__(
middlewares=middlewares, template_path=self.template_path, *args, **kwargs middlewares=middlewares, template_path=self.template_path, *args, **kwargs
) )
self.jinja2_env.add_extension(MarkdownExtension)
self.setup_router() self.setup_router()
def setup_router(self): def setup_router(self):
@ -34,12 +37,14 @@ class Application(BaseApplication):
name="static", name="static",
show_index=True, show_index=True,
) )
self.router.add_view("/web", WebView) self.router.add_view("/about.html", AboutHTMLView)
self.router.add_view("/login", LoginView) self.router.add_view("/about.md", AboutMDView)
self.router.add_view("/login-form", LoginFormView) self.router.add_view("/web.html", WebView)
self.router.add_view("/register", RegisterView) self.router.add_view("/login.html", LoginView)
self.router.add_view("/login-form.json", LoginFormView)
self.router.add_view("/register.html", RegisterView)
self.router.add_view("/register-form", RegisterFormView) self.router.add_view("/register-form.json", RegisterFormView)
self.router.add_get("/http-get", self.handle_http_get) self.router.add_get("/http-get", self.handle_http_get)
self.router.add_get("/http-photo", self.handle_http_photo) self.router.add_get("/http-photo", self.handle_http_photo)

View File

@ -15,7 +15,7 @@ class RegisterForm(Form):
) )
email = FormInputElement( email = FormInputElement(
name="email", name="email",
required=True, required=False,
regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$", regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$",
place_holder="Email address", place_holder="Email address",
type="email" type="email"

View File

@ -0,0 +1,12 @@
import functools
from snek.mapper.user import UserMapper
@functools.cache
def get_mappers(app=None):
return dict(
user=UserMapper(app=app)
)
def get_mapper(name, app=None):
return get_mappers(app=app)[name]

6
src/snek/mapper/user.py Normal file
View File

@ -0,0 +1,6 @@
from snek.system.mapper import BaseMapper
from snek.model.user import UserModel
class UserMapper(BaseMapper):
table_name = "user"
model: UserModel

View File

@ -0,0 +1,12 @@
from snek.model.user import UserModel
import functools
@functools.cache
def get_models():
return dict(
user=UserModel
)
def get_model(name):
return get_models()[name]

View File

@ -1,6 +1,6 @@
from snek.system.model import BaseModel,ModelField from snek.system.model import BaseModel,ModelField
class User(BaseModel): class UserModel(BaseModel):
username = ModelField( username = ModelField(
name="username", name="username",
@ -11,7 +11,7 @@ class User(BaseModel):
) )
email = ModelField( email = ModelField(
name="email", name="email",
required=True, required=False,
regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$" regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
) )
password = ModelField(name="password",required=True,regex=r"^[a-zA-Z0-9_.+-]{6,}") password = ModelField(name="password",required=True,regex=r"^[a-zA-Z0-9_.+-]{6,}")

View File

@ -0,0 +1,12 @@
from snek.service.user import UserService
import functools
@functools.cache
def get_services(app):
return dict(
user = UserService(app=app)
)
def get_service(name, app=None):
return get_services(app=app)[name]

16
src/snek/service/user.py Normal file
View File

@ -0,0 +1,16 @@
from snek.system.service import BaseService
from snek.system import security
class UserService:
mapper_name = "user"
async def create_user(self, username, password):
if await self.exists(username=username):
raise Exception("User already exists.")
model = await self.new()
model.username = username
model.password = await security.hash(password)
if await self.save(model):
return model
raise Exception(f"Failed to create user: {model.errors}.")

View File

@ -224,7 +224,11 @@ class GenericForm extends HTMLElement {
} }
@media (max-width: 500px) { @media (max-width: 500px) {
width:100%;
height:100%;
form { form {
height:100%;
width: 100%;
width: 80%; width: 80%;
} }
}` }`

View File

@ -26,7 +26,16 @@ class HTMLFrame extends HTMLElement {
throw new Error(`Error: ${response.status} ${response.statusText}`); throw new Error(`Error: ${response.status} ${response.statusText}`);
} }
const html = await response.text(); const html = await response.text();
this.container.innerHTML = html; if(url.endsWith(".md")){
const parent = this
const markdownElement = document.createElement('div')
markdownElement.innerHTML = html
document.body.appendChild(markdownElement)
//parent.parentElement.appendChild(markdownElement)
}else{
this.container.innerHTML = html;
}
} catch (error) { } catch (error) {
this.container.textContent = `Error: ${error.message}`; this.container.textContent = `Error: ${error.message}`;

View File

@ -0,0 +1,39 @@
class HTMLFrame extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.container = document.createElement('div');
this.shadowRoot.appendChild(this.container);
}
connectedCallback() {
this.container.classList.add("html_frame")
const url = this.getAttribute('url');
if (url) {
const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get")
if(!url.startsWith("/"))
fullUrl.searchParams.set('url', url)
this.loadAndRender(fullUrl.toString());
} else {
this.container.textContent = "No source URL!";
}
}
async loadAndRender(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Error: ${response.status} ${response.statusText}`);
}
const html = await response.text();
this.container.innerHTML = html;
} catch (error) {
this.container.textContent = `Error: ${error.message}`;
}
}
}
customElements.define('markdown-frame', HTMLFrame);

20
src/snek/static/style.css Normal file
View File

@ -0,0 +1,20 @@
h1 {
font-size: 2em;
color: #f05a28;
margin-bottom: 20px;
}
h2 {
font-size: 1.4em;
color: #f05a28;
margin-bottom: 20px;
}
body {
background-color: #000;
color: #efefef;
}
div {
text-align: left;
}

0
src/snek/system/api.py Normal file
View File

View File

@ -1,171 +1,96 @@
from snek.system import model # Written by retoor@molodetz.nl
# This code defines a framework for handling HTML elements as Python objects, including specific classes for HTML, form input, and form button elements. It offers methods to convert these elements to JSON, manipulate them, and validate form data.
# This code uses the `snek.system.model` library for managing model fields.
# MIT License
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from snek.system import model
class HTMLElement(model.ModelField): class HTMLElement(model.ModelField):
def __init__(self,id:str=None, tag:str="div", name:str=None,html:str=None, class_name:str=None, text:str=None, *args, **kwargs): def __init__(self, id=None, tag="div", name=None, html=None, class_name=None, text=None, *args, **kwargs):
"""
Create a new HTMLElement.
:param id: The id of the element
:param tag: The tag of the element
:param name: The name of the element, used to generate a class name if not provided
:param html: The inner html of the element
:param class_name: The class name of the element
:param text: The text of the element
"""
self.tag = tag self.tag = tag
self.text = text self.text = text
self.id = id self.id = id
self.class_name = class_name or name self.class_name = class_name or name
self.html = html self.html = html
super().__init__(name=name,*args, **kwargs) super().__init__(name=name, *args, **kwargs)
def to_json(self): def to_json(self):
"""
Return a json representation of the element.
This will return a dict with the following keys:
- text: The text of the element
- id: The id of the element
- html: The inner html of the element
- class_name: The class name of the element
- tag: The tag of the element
:return: A json representation of the element
:rtype: dict
"""
result = super().to_json() result = super().to_json()
result['text'] = self.text result['text'] = self.text
result['id'] = self.id result['id'] = self.id
result['html'] = self.html result['html'] = self.html
result['class_name'] = self.class_name result['class_name'] = self.class_name
result['tag'] = self.tag result['tag'] = self.tag
return result return result
class FormElement(HTMLElement): class FormElement(HTMLElement):
pass pass
class FormInputElement(FormElement): class FormInputElement(FormElement):
def __init__(self, type="text", place_holder=None, *args, **kwargs):
def __init__(self,type="text",place_holder=None, *args, **kwargs):
"""
Initialize a FormInputElement with specified attributes.
:param type: The type of the input element (default is "text").
:param place_holder: The placeholder text for the input element.
:param args: Additional positional arguments.
:param kwargs: Additional keyword arguments.
"""
super().__init__(tag="input", *args, **kwargs) super().__init__(tag="input", *args, **kwargs)
self.place_holder = place_holder self.place_holder = place_holder
self.type = type self.type = type
def to_json(self): def to_json(self):
"""
Return a json representation of the element.
This will return a dict with the following keys:
- place_holder: The placeholder text for the input element
- type: The type of the input element
:return: A json representation of the element
:rtype: dict
"""
data = super().to_json() data = super().to_json()
data["place_holder"] = self.place_holder data["place_holder"] = self.place_holder
data["type"] = self.type data["type"] = self.type
return data return data
class FormButtonElement(FormElement):
# Just use the label text property to assign a button label.
def __init__(self, tag="button", *args, **kwargs):
"""
Initialize a FormButtonElement with specified attributes.
:param tag: The tag of the button element (default is "button"). class FormButtonElement(FormElement):
:param args: Additional positional arguments. def __init__(self, tag="button", *args, **kwargs):
:param kwargs: Additional keyword arguments.
"""
super().__init__(tag=tag, *args, **kwargs) super().__init__(tag=tag, *args, **kwargs)
class Form(model.BaseModel): class Form(model.BaseModel):
@property @property
def html_elements(self): def html_elements(self):
"""
Return a list of all :class:`HTMLElement` objects in the form.
This is a convenience property that filters the :attr:`fields` list to only
include elements that are instances of :class:`HTMLElement`.
:return: A list of :class:`HTMLElement` objects
:rtype: list
"""
json_elements = super().to_json() json_elements = super().to_json()
return [element for element in self.fields if isinstance(element,HTMLElement)] return [element for element in self.fields if isinstance(element, HTMLElement)]
def set_user_data(self, data): def set_user_data(self, data):
"""
Set user data for the form by updating the fields with the provided data.
This method extracts the 'fields' key from the provided data dictionary
and passes it to the parent class's `set_user_data` method to update the
form fields accordingly.
:param data: A dictionary containing the form data, expected to have a
'fields' key with the data to update the form fields.
"""
return super().set_user_data(data.get('fields')) return super().set_user_data(data.get('fields'))
def to_json(self, encode=False): def to_json(self, encode=False):
"""
Return a JSON representation of the form, including field values and metadata.
This method returns a dictionary with the following keys:
- ``fields``: A dictionary of field names to their current values.
- ``is_valid``: A boolean indicating whether the form is valid.
- ``errors``: A dictionary of field names to lists of error strings.
If the ``encode`` argument is ``True``, the dictionary will be JSON-encoded
before being returned. Otherwise, the dictionary is returned directly.
:param encode: If ``True``, JSON-encode the returned dictionary.
:type encode: bool
:return: A JSON representation of the form.
:rtype: dict
"""
elements = super().to_json() elements = super().to_json()
html_elements = {} html_elements = {}
for element in elements.keys(): for element in elements.keys():
print("DDD!",element,flush=True) field = getattr(self, element)
field = getattr(self,element) if isinstance(field, HTMLElement):
if isinstance(field,HTMLElement):
print("QQQQ!",element,flush=True)
try: try:
html_elements[element] = elements[element] html_elements[element] = elements[element]
except KeyError: except KeyError:
pass pass
return dict(fields=html_elements, is_valid=self.is_valid, errors=self.errors)
return dict(fields=html_elements,is_valid=self.is_valid,errors=self.errors)
@property @property
def errors(self): def errors(self):
"""
Return a list of all error strings from all fields in the form.
The list will be empty if all fields are valid.
:return: A list of error strings.
:rtype: list
"""
result = [] result = []
for field in self.html_elements: for field in self.html_elements:
result += field.errors result += field.errors
return result return result
@property @property
def is_valid(self): def is_valid(self):
return all(element.is_valid for element in self.html_elements) return all(element.is_valid for element in self.html_elements)

View File

@ -1,77 +1,99 @@
from aiohttp import web # Written by retoor@molodetz.nl
import aiohttp
# This script enables downloading, processing, and caching web content, including taking website screenshots and repairing links in HTML content.
# Imports used: aiohttp, aiohttp.web for creating web servers and handling async requests; app.cache for caching utilities; BeautifulSoup from bs4 for HTML parsing; imgkit for creating screenshots.
# The MIT License (MIT)
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from aiohttp import web
import aiohttp
from app.cache import time_cache_async from app.cache import time_cache_async
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from urllib.parse import urljoin from urllib.parse import urljoin
import pathlib import pathlib
import uuid import uuid
import imgkit import imgkit
import asyncio import asyncio
import zlib import zlib
import io import io
async def crc32(data): async def crc32(data):
try: try:
data = data.encode() data = data.encode()
except: except:
pass pass
result = "crc32" + str(zlib.crc32(data)) return "crc32" + str(zlib.crc32(data))
return result
async def get_file(name,suffix=".cache"): async def get_file(name, suffix=".cache"):
name = await crc32(name) name = await crc32(name)
path = pathlib.Path(".").joinpath("cache") path = pathlib.Path(".").joinpath("cache")
if not path.exists(): if not path.exists():
path.mkdir(parents=True,exist_ok=True) path.mkdir(parents=True, exist_ok=True)
path = path.joinpath(name + suffix) return path.joinpath(name + suffix)
return path
async def public_touch(name=None): async def public_touch(name=None):
path = pathlib.Path(".").joinpath(str(uuid.uuid4())+name) path = pathlib.Path(".").joinpath(str(uuid.uuid4()) + name)
path.open("wb").close() path.open("wb").close()
return path return path
async def create_site_photo(url): async def create_site_photo(url):
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
if not url.startswith("https"): if not url.startswith("https"):
url = "https://" + url url = "https://" + url
output_path = await get_file("site-screenshot-" + url,".png") output_path = await get_file("site-screenshot-" + url, ".png")
if output_path.exists(): if output_path.exists():
return output_path return output_path
output_path.touch() output_path.touch()
def make_photo(): def make_photo():
imgkit.from_url(url, output_path.absolute()) imgkit.from_url(url, output_path.absolute())
return output_path return output_path
return await loop.run_in_executor(None,make_photo) return await loop.run_in_executor(None, make_photo)
async def repair_links(base_url, html_content): async def repair_links(base_url, html_content):
soup = BeautifulSoup(html_content, "html.parser") soup = BeautifulSoup(html_content, "html.parser")
for tag in soup.find_all(['a', 'img', 'link']): for tag in soup.find_all(['a', 'img', 'link']):
if tag.has_attr('href') and not tag['href'].startswith("http"): # For <a> and <link> tags if tag.has_attr('href') and not tag['href'].startswith("http"):
tag['href'] = urljoin(base_url, tag['href']) tag['href'] = urljoin(base_url, tag['href'])
if tag.has_attr('src') and not tag['src'].startswith("http"): # For <img> tags if tag.has_attr('src') and not tag['src'].startswith("http"):
tag['src'] = urljoin(base_url, tag['src']) tag['src'] = urljoin(base_url, tag['src'])
print("Fixed: ",tag['src'])
return soup.prettify() return soup.prettify()
async def is_html_content(content: bytes): async def is_html_content(content: bytes):
try: try:
content = content.decode(errors='ignore') content = content.decode(errors='ignore')
except: except:
pass pass
marks = ['<html','<img','<p','<span','<div'] marks = ['<html', '<img', '<p', '<span', '<div']
try: try:
content = content.lower() content = content.lower()
for mark in marks: for mark in marks:
if mark in content: if mark in content:
return True return True
except Exception as ex: except Exception as ex:
print(ex) print(ex)
return False return False
@time_cache_async(120) @time_cache_async(120)
async def get(url): async def get(url):
@ -79,5 +101,5 @@ async def get(url):
response = await session.get(url) response = await session.get(url)
content = await response.text() content = await response.text()
if await is_html_content(content): if await is_html_content(content):
content = (await repair_links(url,content)).encode() content = (await repair_links(url, content)).encode()
return content return content

64
src/snek/system/mapper.py Normal file
View File

@ -0,0 +1,64 @@
DEFAULT_LIMIT = 30
from snek.system.model import BaseModel
from snek.app import Application
import types
class Mapper:
model_class:BaseModel = None
default_limit:int = DEFAULT_LIMIT
table_name:str = None
def __init__(self, app:Application, table_name:str, model_class:BaseModel):
self.app = app
if not self.model_class:
raise ValueError("Mapper configuration error: model_class is not set.")
self.model_class = model_class
self.table_name = table_name
if not self.table_name:
raise ValueError("Mapper configuration error: table_name is not set.")
self.default_limit = self.__class__.default_limit
@property
def db(self):
return self.app.db
async def new(self):
return self.model_class(mapper=self)
@property
def table(self):
return self.db[self.table_name]
async def get(self, uid:str=None, **kwargs) -> types.Optional[BaseModel]
if uid:
kwargs['uid'] = uid
model = self.new()
record = self.table.find_one(**kwargs)
return self.model_class.from_record(mapper=self,record=record)
async def exists(self, **kwargs):
return self.table.exists(**kwargs)
async def count(self, **kwargs) -> int:
return self.table.count(**kwargs)
async def save(self, model:BaseModel) -> bool:
record = model.record
if not record.get('uid'):
raise Exception(f"Attempt to save without uid: {record}.")
return self.table.upsert(record,['uid'])
async def find(self, **kwargs) -> types.List[BaseModel]:
if not kwargs.get("_limit"):
kwargs["_limit"] = self.default_limit
for record in self.table.find(**kwargs):
yield self.model_class.from_record(mapper=self,record=record)
async def delete(self, kwargs=None)-> int:
if not kwargs or not isinstance(kwargs, dict):
raise Exception("Can't execute delete with no filter.")
return self.table.delete(**kwargs)

View File

@ -0,0 +1,43 @@
# Original source: https://brandonjay.dev/posts/2021/render-markdown-html-in-python-with-jinja2
from mistune import escape
from mistune import Markdown
from mistune import HTMLRenderer
from pygments import highlight
from pygments.lexers import get_lexer_by_name
from pygments.formatters import html
from pygments.styles import get_style_by_name
class MarkdownRenderer(HTMLRenderer):
def __init__(self, app, template):
self.template = template
self.app = app
self.env = self.app.jinja2_env
formatter = html.HtmlFormatter()
self.env.globals['highlight_styles'] = formatter.get_style_defs()
def _escape(self,str):
return str ##escape(str)
def block_code(self, code, lang=None,info=None):
if not lang:
lang = info
if not lang:
return f"<div>{code}</div>"
#return '\n<pre><code>%s</code></pre>\n' % escape(code)
lexer = get_lexer_by_name(lang, stripall=True)
formatter = html.HtmlFormatter(lineseparator="<br>")
print(code, lang,info, flush=True)
return highlight(code, lexer, formatter)
def render(self):
markdown_string = self.app.template_path.joinpath(self.template).read_text()
renderer = MarkdownRenderer(self.app,self.template)
markdown = Markdown(renderer=renderer)
return markdown(markdown_string)
async def render_markdown(app, markdown_string):
renderer = MarkdownRenderer(app,None)
markdown = Markdown(renderer=renderer)
return markdown(markdown_string)

View File

@ -1,4 +1,12 @@
from aiohttp import web # Written by retoor@molodetz.nl
# This code provides middleware functions for an aiohttp server to manage and modify CORS (Cross-Origin Resource Sharing) headers.
# Imports from 'aiohttp' library are used to create middleware; they are not part of Python's standard library.
# MIT License: This code is distributed under the MIT License.
from aiohttp import web
@web.middleware @web.middleware
async def no_cors_middleware(request, handler): async def no_cors_middleware(request, handler):
@ -7,16 +15,15 @@ async def no_cors_middleware(request, handler):
return response return response
@web.middleware @web.middleware
async def cors_allow_middleware(request ,handler): async def cors_allow_middleware(request, handler):
response = await handler(request) response = await handler(request)
response.headers["Access-Control-Allow-Origin"] = "*" response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS, PUT, DELETE" response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS, PUT, DELETE"
response.headers["Access-Control-Allow-Headers"] = "*" response.headers["Access-Control-Allow-Headers"] = "*"
return response return response
@web.middleware @web.middleware
async def cors_middleware(request, handler): async def cors_middleware(request, handler):
# Handle preflight (OPTIONS) requests
if request.method == "OPTIONS": if request.method == "OPTIONS":
response = web.Response() response = web.Response()
response.headers["Access-Control-Allow-Origin"] = "*" response.headers["Access-Control-Allow-Origin"] = "*"
@ -24,7 +31,6 @@ async def cors_middleware(request, handler):
response.headers["Access-Control-Allow-Headers"] = "*" response.headers["Access-Control-Allow-Headers"] = "*"
return response return response
# Handle actual requests
response = await handler(request) response = await handler(request)
response.headers["Access-Control-Allow-Origin"] = "*" response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS" response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"

View File

@ -1,36 +1,67 @@
# Written by retoor@molodetz.nl
# The script defines a flexible validation and field management system for models, with capabilities for setting attributes, validation, error handling, and JSON conversion. It includes classes for managing various field types with specific properties such as UUID, timestamps for creation and updates, and custom validation rules.
# This script utilizes external Python libraries such as 're' for regex operations, 'uuid' for generating unique identifiers, and 'json' for data interchange. The 'datetime' and 'timezone' modules from the Python standard library are used for date and time operations. 'OrderedDict' from 'collections' provides enhanced dictionary capabilities, and 'copy' allows deep copying of objects.
# MIT License
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import re import re
import uuid import uuid
import json import json
from datetime import datetime , timezone from datetime import datetime, timezone
from collections import OrderedDict from collections import OrderedDict
import copy import copy
TIMESTAMP_REGEX = r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}\+\d{2}:\d{2}$" TIMESTAMP_REGEX = r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}\+\d{2}:\d{2}$"
def now(): def now():
return str(datetime.now(timezone.utc)) return str(datetime.now(timezone.utc))
def add_attrs(**kwargs): def add_attrs(**kwargs):
def decorator(func): def decorator(func):
for key, value in kwargs.items(): for key, value in kwargs.items():
setattr(func, key, value) setattr(func, key, value)
return func return func
return decorator return decorator
def validate_attrs(required=False,min_length=None,max_length=None,regex=None,**kwargs):
def decorator(func): def validate_attrs(required=False, min_length=None, max_length=None, regex=None, **kwargs):
return add_attrs(required=required,min_length=min_length,max_length=max_length,regex=regex,**kwargs)(func) def decorator(func):
return add_attrs(required=required, min_length=min_length, max_length=max_length, regex=regex, **kwargs)(func)
class Validator: class Validator:
_index = 0 _index = 0
@property @property
def value(self): def value(self):
return self._value return self._value
@value.setter @value.setter
def value(self,val): def value(self, val):
self._value = json.loads(json.dumps(val,default=str)) self._value = json.loads(json.dumps(val, default=str))
@property @property
def initial_value(self): def initial_value(self):
@ -39,48 +70,49 @@ class Validator:
def custom_validation(self): def custom_validation(self):
return True return True
def __init__(self,required=False,min_num=None,max_num=None,min_length=None,max_length=None,regex=None,value=None,kind=None,help_text=None,**kwargs): def __init__(self, required=False, min_num=None, max_num=None, min_length=None, max_length=None, regex=None, value=None, kind=None, help_text=None, **kwargs):
self.index = Validator._index self.index = Validator._index
Validator._index += 1 Validator._index += 1
self.required = required self.required = required
self.min_num = min_num self.min_num = min_num
self.max_num = max_num self.max_num = max_num
self.min_length = min_length self.min_length = min_length
self.max_length = max_length self.max_length = max_length
self.regex = regex self.regex = regex
self._value = None self._value = None
self.value = value self.value = value
print("xxxx", value,flush=True) print("xxxx", value, flush=True)
self.kind = kind self.kind = kind
self.help_text = help_text self.help_text = help_text
self.__dict__.update(kwargs) self.__dict__.update(kwargs)
@property
@property
def errors(self): def errors(self):
error_list = [] error_list = []
if self.value is None and self.required: if self.value is None and self.required:
error_list.append("Field is required.") error_list.append("Field is required.")
return error_list return error_list
if self.value is None:
return error_list
if self.kind == float or self.kind == int: if self.value is None:
return error_list
if self.kind in [int, float]:
if self.min_num is not None and self.value < self.min_num: if self.min_num is not None and self.value < self.min_num:
error_list.append("Field should be minimal {}.".format(self.min_num)) error_list.append(f"Field should be minimal {self.min_num}.")
if self.max_num is not None and self.value > self.max_num: if self.max_num is not None and self.value > self.max_num:
error_list.append("Field should be maximal {}.".format(self.max_num)) error_list.append(f"Field should be maximal {self.max_num}.")
if self.min_length is not None and len(self.value) < self.min_length: if self.min_length is not None and len(self.value) < self.min_length:
error_list.append("Field should be minimal {} characters long.".format(self.min_length)) error_list.append(f"Field should be minimal {self.min_length} characters long.")
if self.max_length is not None and len(self.value) > self.max_length: if self.max_length is not None and len(self.value) > self.max_length:
error_list.append("Field should be maximal {} characters long.".format(self.max_length)) error_list.append(f"Field should be maximal {self.max_length} characters long.")
print(self.regex, self.value,flush=True) print(self.regex, self.value, flush=True)
if not self.regex is None and not self.value is None and not re.match(self.regex, self.value): if self.regex and self.value and not re.match(self.regex, self.value):
error_list.append("Invalid value.".format(self.regex)) error_list.append("Invalid value.")
if not self.kind is None and type(self.value) != self.kind: if self.kind and not isinstance(self.value, self.kind):
error_list.append("Invalid kind. It is supposed to be {}.".format(self.kind)) error_list.append(f"Invalid kind. It is supposed to be {self.kind}.")
return error_list return error_list
def validate(self): def validate(self):
if self.errors: if self.errors:
raise ValueError("\n", self.errors) raise ValueError("\n", self.errors)
@ -94,8 +126,6 @@ class Validator:
except ValueError: except ValueError:
return False return False
def to_json(self): def to_json(self):
return { return {
"required": self.required, "required": self.required,
@ -109,25 +139,27 @@ class Validator:
"help_text": self.help_text, "help_text": self.help_text,
"errors": self.errors, "errors": self.errors,
"is_valid": self.is_valid, "is_valid": self.is_valid,
"index":self.index "index": self.index
} }
class ModelField(Validator): class ModelField(Validator):
index = 1 index = 1
def __init__(self,name=None,save=True, *args, **kwargs):
self.name = name def __init__(self, name=None, save=True, *args, **kwargs):
self.name = name
self.save = save self.save = save
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def to_json(self): def to_json(self):
result = super().to_json() result = super().to_json()
result['name'] = self.name result['name'] = self.name
return result return result
class CreatedField(ModelField): class CreatedField(ModelField):
@property @property
def initial_value(self): def initial_value(self):
return now() return now()
@ -136,67 +168,99 @@ class CreatedField(ModelField):
if not self.value: if not self.value:
self.value = now() self.value = now()
class UpdatedField(ModelField): class UpdatedField(ModelField):
def update(self): def update(self):
self.value = now() self.value = now()
class DeletedField(ModelField): class DeletedField(ModelField):
def update(self): def update(self):
self.value = now() self.value = now()
class UUIDField(ModelField): class UUIDField(ModelField):
@property @property
def initial_value(self): def initial_value(self):
return str(uuid.uuid4()) return str(uuid.uuid4())
class BaseModel: class BaseModel:
uid = UUIDField(name="uid",required=True)
created_at = CreatedField(name="created_at",required=True, regex=TIMESTAMP_REGEX, place_holder="Created at")
updated_at = UpdatedField(name="updated_at",regex=TIMESTAMP_REGEX,place_holder="Updated at")
deleted_at = DeletedField(name="deleted_at",regex=TIMESTAMP_REGEX, place_holder="Deleted at")
uid = UUIDField(name="uid", required=True)
created_at = CreatedField(name="created_at", required=True, regex=TIMESTAMP_REGEX, place_holder="Created at")
updated_at = UpdatedField(name="updated_at", regex=TIMESTAMP_REGEX, place_holder="Updated at")
deleted_at = DeletedField(name="deleted_at", regex=TIMESTAMP_REGEX, place_holder="Deleted at")
@classmethod
def from_record(cls, record, mapper):
model = cls.__new__()
model.mapper = mapper
model.record = record
return model
@property
def mapper(self):
return self._mapper
@mapper.setter
def mapper(self, value):
self._mapper = value
@property
def record(self):
return {field.name: field.value for field in self.fields}
@record.setter
def record(self, value):
for key, value in self._record.items():
field = self.fields.get(key)
if not field:
continue
field.value = value
return self
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
print(self.__dict__) print(self.__dict__)
print(dir(self.__class__)) print(dir(self.__class__))
self._mapper = None
self.fields = {} self.fields = {}
for key in dir(self.__class__): for key in dir(self.__class__):
obj = getattr(self.__class__,key) obj = getattr(self.__class__, key)
if isinstance(obj,Validator): if isinstance(obj, Validator):
self.__dict__[key] = copy.deepcopy(obj) self.__dict__[key] = copy.deepcopy(obj)
print("JAAA") print("JAAA")
self.__dict__[key].value = kwargs.pop(key,self.__dict__[key].initial_value) self.__dict__[key].value = kwargs.pop(key, self.__dict__[key].initial_value)
self.fields[key] = self.__dict__[key] self.fields[key] = self.__dict__[key]
def __setitem__(self, key, value): def __setitem__(self, key, value):
obj = self.__dict__.get(key) obj = self.__dict__.get(key)
if isinstance(obj,Validator): if isinstance(obj, Validator):
obj.value = value obj.value = value
def __getattr__(self, key): def __getattr__(self, key):
obj = self.__dict__.get(key) obj = self.__dict__.get(key)
if isinstance(obj,Validator): if isinstance(obj, Validator):
print("HPAPP") print("HPAPP")
return obj.value return obj.value
return obj return obj
def set_user_data(self, data): def set_user_data(self, data):
for key, value in data.items(): for key, value in data.items():
field = self.fields.get(key) field = self.fields.get(key)
if not field: if not field:
continue continue
if value.get('name'): if value.get('name'):
value = value.get('value') value = value.get('value')
field.value = value field.value = value
@property
@property
def is_valid(self): def is_valid(self):
for field in self.fields.values(): for field in self.fields.values():
if not field.is_valid: if not field.is_valid:
@ -205,46 +269,44 @@ class BaseModel:
def __getitem__(self, key): def __getitem__(self, key):
obj = self.__dict__.get(key) obj = self.__dict__.get(key)
if isinstance(obj,Validator): if isinstance(obj, Validator):
return obj.value return obj.value
def __setattr__(self, key, value): def __setattr__(self, key, value):
obj = getattr(self,key) obj = getattr(self, key)
if isinstance(obj,Validator): if isinstance(obj, Validator):
obj.value = value obj.value = value
else: else:
self.__dict__[key] = value #setattr(self,key,value) self.__dict__[key] = value
#def __getattr__(self, key):
# obj = self.__dict__.get(key) @property
# if isinstance(obj,Validator):
# return obj.value
@property
def record(self): def record(self):
obj = self.to_json() obj = self.to_json()
record = {} record = {}
for key,value in obj.items(): for key, value in obj.items():
if getattr(self,key).save: if getattr(self, key).save:
record[key] = value.get('value') record[key] = value.get('value')
return record return record
def to_json(self,encode=False): def to_json(self, encode=False):
model_data = OrderedDict({ model_data = OrderedDict({
"uid": self.uid.value, "uid": self.uid.value,
"created_at": self.created_at.value, "created_at": self.created_at.value,
"updated_at": self.updated_at.value, "updated_at": self.updated_at.value,
"deleted_at": self.deleted_at.value "deleted_at": self.deleted_at.value
}) })
for key,value in self.__dict__.items(): for key, value in self.__dict__.items():
if key == "record": if key == "record":
continue continue
value = self.__dict__[key] value = self.__dict__[key]
if hasattr(value,"value"): if hasattr(value, "value"):
model_data[key] = value.to_json() model_data[key] = value.to_json()
if encode: if encode:
return json.dumps(model_data,indent=2) return json.dumps(model_data, indent=2)
return model_data return model_data
class FormElement(ModelField): class FormElement(ModelField):
def __init__(self, place_holder=None, *args, **kwargs): def __init__(self, place_holder=None, *args, **kwargs):
@ -252,15 +314,14 @@ class FormElement(ModelField):
self.place_holder = place_holder self.place_holder = place_holder
class FormElement(ModelField): class FormElement(ModelField):
def __init__(self,place_holder=None, *args, **kwargs): def __init__(self, place_holder=None, *args, **kwargs):
self.place_holder = place_holder self.place_holder = place_holder
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def to_json(self): def to_json(self):
data = super().to_json() data = super().to_json()
data["name"] = self.name data["name"] = self.name
data["place_holder"] = self.place_holder data["place_holder"] = self.place_holder
return data return data

View File

@ -0,0 +1,20 @@
import hashlib
DEFAULT_SALT = b"snekker-de-snek-"
async def hash(data,salt=DEFAULT_SALT):
try:
data = data.encode(errors="ignore")
except AttributeError:
pass
try:
salt = salt.encode(errors="ignore")
except AttributeError:
pass
salted = salt + data
obj = hashlib.sha256(salted)
return obj.hexdigest()
async def verify(string:str, hashed:str):
return await hash(string) == hashed

View File

@ -0,0 +1,40 @@
from snek.mapper import get_mapper
from snek.system.mapper import BaseMapper
from snek.model.user import UserModel
class BaseService:
mapper_name:BaseMapper = None
def __init__(self, app):
self.app = app
if self.mapper_name:
self.mapper = get_mapper(self.mapper_name, app=self.app)
else:
self.mapper = None
async def exists(self, **kwargs):
return self.mapper.exists(**kwargs)
async def count(self, **kwargs):
return self.mapper.count(**kwargs)
async def new(self, **kwargs):
return await self.mapper.new()
async def get(self, **kwargs):
return await self.mapper.get(**kwargs)
async def save(self, model:UserModel):
if model.is_valid:
return self.mapper.save(model) and True
return False
async def find(self, **kwargs):
return await self.mapper.find(**kwargs)
async def delete(self, **kwargs):
return await self.mapper.delete(**kwargs)

38
src/snek/system/view.py Normal file
View File

@ -0,0 +1,38 @@
from aiohttp import web
from snek.system.markdown import render_markdown
class BaseView(web.View):
@property
def app(self):
return self.request.app
@property
def db(self):
return self.app.db
async def json_response(self, data):
return web.json_response(data)
async def render_template(self, template_name, context=None):
if template_name.endswith(".md"):
response = await self.request.app.render_template(template_name,self.request,context)
body = await render_markdown(self.app, response.body.decode())
return web.Response(body=body,content_type="text/html")
return await self.request.app.render_template(template_name, self.request,context)
class BaseFormView(BaseView):
form = None
async def get(self):
form = self.form()
return await self.json_response(form.to_json())
async def post(self):
form = self.form()
post = await self.request.json()
form.set_user_data(post['form'])
return await self.json_response(form.to_json())

View File

@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block main %}
<html-frame url="/about.md"></html-frame>
<fancy-button text="Back" url="/web.html"></fancy-button>
{% endblock %}

View File

@ -0,0 +1,15 @@
# Snek
## What is a snek?
A snek is a danger noodle.
## Design choices
I made several design choices:
- Implemented **the worst 3rd party markdown to html renderer ever**. See this nice *bullet list*.
- Only password requirement is thats it requires six characters. Users are responsibly for their own security. Snek is not so arrogant to determine if a password is strong enough. It's up to what user prefers. Snek does not have a forgot-my-password service tho.
- Email is not required for registration. Email is (maybe) used in future for resetting password.
- Database is SQLite by default. Postgres is also possible. In that case you have to change `db_path` prefix to `postgres:///` and add a postgres docker container to the docker setup.
- Homebrew made ORM framework based on dataset.
- Homebrew made Form framework based on the homebrew made ORM. Most forms are ModelForms but always require an service to be saved for sake of consistency and structure.
- !DRY for HMTL/jinja2 templates. For templates Snek does prefer to repeat itself to implement exceptions for a page easier. For Snek it's preffered do a few updates instead of maintaining a complex generic system that requires maintenance regarding templates.
- No existing chat backend like `inspircd` (Popular decent IRC server written in the language of angels) because I prefer to know what is exactly going on above performance and concurrency limit. Also, this approach reduces as networking layer / gateway layer.

View File

@ -4,9 +4,8 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{% endblock %}</title> <title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" href="/style.css">
<script src="/fancy-button.js"></script> <script src="/fancy-button.js"></script>
<link rel="stylesheet" href="/style.css">
<link rel="stylesheet" href="/generic-form.css">
<script src="/html-frame.js"></script> <script src="/html-frame.js"></script>
<script src="/generic-form.js"></script> <script src="/generic-form.js"></script>
<link rel="stylesheet" href="/html-frame.css"></script> <link rel="stylesheet" href="/html-frame.css"></script>

View File

@ -14,7 +14,8 @@
<fancy-button url="/login" text="Login"></fancy-button> <fancy-button url="/login" text="Login"></fancy-button>
<span style="padding:10px;">Or</span> <span style="padding:10px;">Or</span>
<fancy-button url="/register" text="Register"></fancy-button> <fancy-button url="/register" text="Register"></fancy-button>
<a href="/about.html">Design choices</a>
<a href="/web.html">See web Application so far</a>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,5 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block main %} {% block main %}
<generic-form url="/login-form"></generic-form> <generic-form url="/login-form.json"></generic-form>
{% endblock %} {% endblock %}

View File

@ -1,5 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block main %} {% block main %}
<generic-form url="/register-form"></generic-form> <generic-form url="/register-form.json"></generic-form>
{% endblock %} {% endblock %}

14
src/snek/view/about.py Normal file
View File

@ -0,0 +1,14 @@
from snek.system.view import BaseView
class AboutHTMLView(BaseView):
async def get(self):
return await self.render_template("about.html")
class AboutMDView(BaseView):
async def get(self):
return await self.render_template("about.md")

View File

@ -1,31 +0,0 @@
from aiohttp import web
class BaseView(web.View):
@property
def app(self):
return self.request.app
@property
def db(self):
return self.app.db
def json_response(self, data):
return web.json_response(data)
def render_template(self, template_name, context=None):
return self.request.app.render_template(template_name, self.request,context)
class BaseFormView(BaseView):
form = None
async def get(self):
form = self.form()
return self.json_response(form.to_json())
async def post(self):
form = self.form()
post = await self.request.json()
form.set_user_data(post['form'])
return self.json_response(form.to_json())

View File

@ -1,4 +1,4 @@
from snek.view.base import BaseView from snek.system.view import BaseView
class IndexView(BaseView): class IndexView(BaseView):

View File

@ -1,5 +1,5 @@
from snek.form.register import RegisterForm from snek.form.register import RegisterForm
from snek.view.base import BaseView from snek.system.view import BaseView
class LoginView(BaseView): class LoginView(BaseView):

View File

@ -1,4 +1,4 @@
from snek.view.base import BaseFormView from snek.system.view import BaseFormView
from snek.form.login import LoginForm from snek.form.login import LoginForm
class LoginFormView(BaseFormView): class LoginFormView(BaseFormView):

View File

@ -1,4 +1,4 @@
from snek.view.base import BaseView from snek.system.view import BaseView
class RegisterView(BaseView): class RegisterView(BaseView):

View File

@ -1,5 +1,5 @@
from snek.form.register import RegisterForm from snek.form.register import RegisterForm
from snek.view.base import BaseFormView from snek.system.view import BaseFormView
class RegisterFormView(BaseFormView): class RegisterFormView(BaseFormView):
form = RegisterForm form = RegisterForm

View File

@ -1,4 +1,4 @@
from snek.view.base import BaseView from snek.system.view import BaseView
class WebView(BaseView): class WebView(BaseView):