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
.history
.resources
.backup*
docs
*.db*
*.png
# ---> Python

View File

@ -1,5 +1,5 @@
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
ENV FLASK_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0
@ -37,5 +37,5 @@ RUN pip install --upgrade pip
RUN pip install -e .
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"]

View File

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

View File

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

View File

@ -15,7 +15,7 @@ class RegisterForm(Form):
)
email = FormInputElement(
name="email",
required=True,
required=False,
regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$",
place_holder="Email address",
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
class User(BaseModel):
class UserModel(BaseModel):
username = ModelField(
name="username",
@ -11,7 +11,7 @@ class User(BaseModel):
)
email = ModelField(
name="email",
required=True,
required=False,
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,}")

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) {
width:100%;
height:100%;
form {
height:100%;
width: 100%;
width: 80%;
}
}`

View File

@ -26,7 +26,16 @@ class HTMLFrame extends HTMLElement {
throw new Error(`Error: ${response.status} ${response.statusText}`);
}
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) {
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):
def __init__(self,id:str=None, tag:str="div", name:str=None,html:str=None, class_name:str=None, text:str=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
"""
def __init__(self, id=None, tag="div", name=None, html=None, class_name=None, text=None, *args, **kwargs):
self.tag = tag
self.text = text
self.id = id
self.id = id
self.class_name = class_name or name
self.html = html
super().__init__(name=name,*args, **kwargs)
self.html = html
super().__init__(name=name, *args, **kwargs)
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['text'] = self.text
result['id'] = self.id
result['html'] = self.html
result['text'] = self.text
result['id'] = self.id
result['html'] = self.html
result['class_name'] = self.class_name
result['tag'] = self.tag
return result
return result
class FormElement(HTMLElement):
pass
class FormInputElement(FormElement):
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.
"""
def __init__(self, type="text", place_holder=None, *args, **kwargs):
super().__init__(tag="input", *args, **kwargs)
self.place_holder = place_holder
self.place_holder = place_holder
self.type = type
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["place_holder"] = self.place_holder
data["type"] = self.type
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.
return data
:param tag: The tag of the button element (default is "button").
:param args: Additional positional arguments.
:param kwargs: Additional keyword arguments.
"""
class FormButtonElement(FormElement):
def __init__(self, tag="button", *args, **kwargs):
super().__init__(tag=tag, *args, **kwargs)
class Form(model.BaseModel):
@property
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()
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):
"""
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'))
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()
html_elements = {}
for element in elements.keys():
print("DDD!",element,flush=True)
field = getattr(self,element)
if isinstance(field,HTMLElement):
print("QQQQ!",element,flush=True)
field = getattr(self, element)
if isinstance(field, HTMLElement):
try:
html_elements[element] = elements[element]
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
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 = []
for field in self.html_elements:
result += field.errors
return result
result += field.errors
return result
@property
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
import aiohttp
# Written by retoor@molodetz.nl
# 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 bs4 import BeautifulSoup
from urllib.parse import urljoin
import pathlib
import uuid
import imgkit
import pathlib
import uuid
import imgkit
import asyncio
import zlib
import io
import io
async def crc32(data):
try:
data = data.encode()
except:
pass
result = "crc32" + str(zlib.crc32(data))
return result
pass
return "crc32" + str(zlib.crc32(data))
async def get_file(name,suffix=".cache"):
async def get_file(name, suffix=".cache"):
name = await crc32(name)
path = pathlib.Path(".").joinpath("cache")
if not path.exists():
path.mkdir(parents=True,exist_ok=True)
path = path.joinpath(name + suffix)
return path
path.mkdir(parents=True, exist_ok=True)
return path.joinpath(name + suffix)
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()
return path
return path
async def create_site_photo(url):
loop = asyncio.get_event_loop()
if not url.startswith("https"):
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():
return output_path
output_path.touch()
def make_photo():
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):
soup = BeautifulSoup(html_content, "html.parser")
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'])
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'])
print("Fixed: ",tag['src'])
return soup.prettify()
async def is_html_content(content: bytes):
try:
content = content.decode(errors='ignore')
except:
pass
marks = ['<html','<img','<p','<span','<div']
pass
marks = ['<html', '<img', '<p', '<span', '<div']
try:
content = content.lower()
for mark in marks:
if mark in content:
return True
return True
except Exception as ex:
print(ex)
return False
return False
@time_cache_async(120)
async def get(url):
@ -79,5 +101,5 @@ async def get(url):
response = await session.get(url)
content = await response.text()
if await is_html_content(content):
content = (await repair_links(url,content)).encode()
content = (await repair_links(url, content)).encode()
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
async def no_cors_middleware(request, handler):
@ -7,16 +15,15 @@ async def no_cors_middleware(request, handler):
return response
@web.middleware
async def cors_allow_middleware(request ,handler):
async def cors_allow_middleware(request, handler):
response = await handler(request)
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS, PUT, DELETE"
response.headers["Access-Control-Allow-Headers"] = "*"
return response
return response
@web.middleware
async def cors_middleware(request, handler):
# Handle preflight (OPTIONS) requests
if request.method == "OPTIONS":
response = web.Response()
response.headers["Access-Control-Allow-Origin"] = "*"
@ -24,7 +31,6 @@ async def cors_middleware(request, handler):
response.headers["Access-Control-Allow-Headers"] = "*"
return response
# Handle actual requests
response = await handler(request)
response.headers["Access-Control-Allow-Origin"] = "*"
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 uuid
import json
from datetime import datetime , timezone
import json
from datetime import datetime, timezone
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}$"
def now():
return str(datetime.now(timezone.utc))
def add_attrs(**kwargs):
def decorator(func):
for key, value in kwargs.items():
setattr(func, key, value)
return func
return decorator
def validate_attrs(required=False,min_length=None,max_length=None,regex=None,**kwargs):
def decorator(func):
return add_attrs(required=required,min_length=min_length,max_length=max_length,regex=regex,**kwargs)(func)
def validate_attrs(required=False, min_length=None, max_length=None, regex=None, **kwargs):
def decorator(func):
return add_attrs(required=required, min_length=min_length, max_length=max_length, regex=regex, **kwargs)(func)
class Validator:
_index = 0
@property
def value(self):
return self._value
return self._value
@value.setter
def value(self,val):
self._value = json.loads(json.dumps(val,default=str))
@value.setter
def value(self, val):
self._value = json.loads(json.dumps(val, default=str))
@property
def initial_value(self):
@ -39,48 +70,49 @@ class Validator:
def custom_validation(self):
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
Validator._index += 1
self.required = required
self.min_num = min_num
self.required = required
self.min_num = min_num
self.max_num = max_num
self.min_length = min_length
self.max_length = max_length
self.regex = regex
self._value = None
self.min_length = min_length
self.max_length = max_length
self.regex = regex
self._value = None
self.value = value
print("xxxx", value,flush=True)
print("xxxx", value, flush=True)
self.kind = kind
self.help_text = help_text
self.help_text = help_text
self.__dict__.update(kwargs)
@property
@property
def errors(self):
error_list = []
if self.value is None and self.required:
error_list.append("Field is required.")
return error_list
if self.value is None:
return error_list
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:
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:
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:
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:
error_list.append("Field should be maximal {} characters long.".format(self.max_length))
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):
error_list.append("Invalid value.".format(self.regex))
if not self.kind is None and type(self.value) != self.kind:
error_list.append("Invalid kind. It is supposed to be {}.".format(self.kind))
return error_list
error_list.append(f"Field should be maximal {self.max_length} characters long.")
print(self.regex, self.value, flush=True)
if self.regex and self.value and not re.match(self.regex, self.value):
error_list.append("Invalid value.")
if self.kind and not isinstance(self.value, self.kind):
error_list.append(f"Invalid kind. It is supposed to be {self.kind}.")
return error_list
def validate(self):
if self.errors:
raise ValueError("\n", self.errors)
@ -94,8 +126,6 @@ class Validator:
except ValueError:
return False
def to_json(self):
return {
"required": self.required,
@ -109,25 +139,27 @@ class Validator:
"help_text": self.help_text,
"errors": self.errors,
"is_valid": self.is_valid,
"index":self.index
"index": self.index
}
class ModelField(Validator):
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
super().__init__(*args, **kwargs)
def to_json(self):
result = super().to_json()
result['name'] = self.name
return result
return result
class CreatedField(ModelField):
@property
def initial_value(self):
return now()
@ -136,67 +168,99 @@ class CreatedField(ModelField):
if not self.value:
self.value = now()
class UpdatedField(ModelField):
def update(self):
self.value = now()
class DeletedField(ModelField):
def update(self):
self.value = now()
class UUIDField(ModelField):
@property
@property
def initial_value(self):
return str(uuid.uuid4())
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):
print(self.__dict__)
print(dir(self.__class__))
self._mapper = None
self.fields = {}
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)
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]
def __setitem__(self, key, value):
obj = self.__dict__.get(key)
if isinstance(obj,Validator):
obj.value = value
if isinstance(obj, Validator):
obj.value = value
def __getattr__(self, key):
obj = self.__dict__.get(key)
if isinstance(obj,Validator):
if isinstance(obj, Validator):
print("HPAPP")
return obj.value
return obj.value
return obj
def set_user_data(self, data):
for key, value in data.items():
field = self.fields.get(key)
if not field:
continue
continue
if value.get('name'):
value = value.get('value')
field.value = value
@property
@property
def is_valid(self):
for field in self.fields.values():
if not field.is_valid:
@ -205,46 +269,44 @@ class BaseModel:
def __getitem__(self, key):
obj = self.__dict__.get(key)
if isinstance(obj,Validator):
return obj.value
if isinstance(obj, Validator):
return obj.value
def __setattr__(self, key, value):
obj = getattr(self,key)
if isinstance(obj,Validator):
obj = getattr(self, key)
if isinstance(obj, Validator):
obj.value = value
else:
self.__dict__[key] = value #setattr(self,key,value)
#def __getattr__(self, key):
# obj = self.__dict__.get(key)
# if isinstance(obj,Validator):
# return obj.value
@property
self.__dict__[key] = value
@property
def record(self):
obj = self.to_json()
record = {}
for key,value in obj.items():
if getattr(self,key).save:
for key, value in obj.items():
if getattr(self, key).save:
record[key] = value.get('value')
return record
def to_json(self,encode=False):
def to_json(self, encode=False):
model_data = OrderedDict({
"uid": self.uid.value,
"created_at": self.created_at.value,
"updated_at": self.updated_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":
continue
value = self.__dict__[key]
if hasattr(value,"value"):
if hasattr(value, "value"):
model_data[key] = value.to_json()
if encode:
return json.dumps(model_data,indent=2)
return json.dumps(model_data, indent=2)
return model_data
class FormElement(ModelField):
def __init__(self, place_holder=None, *args, **kwargs):
@ -252,15 +314,14 @@ class FormElement(ModelField):
self.place_holder = place_holder
class FormElement(ModelField):
def __init__(self,place_holder=None, *args, **kwargs):
self.place_holder = place_holder
def __init__(self, place_holder=None, *args, **kwargs):
self.place_holder = place_holder
super().__init__(*args, **kwargs)
def to_json(self):
data = super().to_json()
data["name"] = self.name
data["name"] = self.name
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 name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" href="/style.css">
<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="/generic-form.js"></script>
<link rel="stylesheet" href="/html-frame.css"></script>

View File

@ -14,7 +14,8 @@
<fancy-button url="/login" text="Login"></fancy-button>
<span style="padding:10px;">Or</span>
<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>
</body>
</html>

View File

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

View File

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block main %}
<generic-form url="/register-form"></generic-form>
<generic-form url="/register-form.json"></generic-form>
{% 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):

View File

@ -1,5 +1,5 @@
from snek.form.register import RegisterForm
from snek.view.base import BaseView
from snek.system.view import 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
class LoginFormView(BaseFormView):

View File

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

View File

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

View File

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