diff --git a/.gitignore b/.gitignore
index 3747073..ece77be 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,8 @@
.vscode
.history
+.resources
+.backup*
+docs
*.db*
*.png
# ---> Python
diff --git a/Dockerfile b/Dockerfile
index 47c0ece..9af8e87 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -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"]
diff --git a/pyproject.toml b/pyproject.toml
index d98557e..cc36846 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -20,6 +20,8 @@ dependencies = [
"beautifulsoup4",
"gunicorn",
"imgkit",
- "wkhtmltopdf"
+ "wkhtmltopdf",
+ "jinja-markdown2",
+ "mistune"
]
diff --git a/src/snek/app.py b/src/snek/app.py
index 0e2ed63..deac5d3 100644
--- a/src/snek/app.py
+++ b/src/snek/app.py
@@ -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)
diff --git a/src/snek/form/register.py b/src/snek/form/register.py
index 60399fb..7dff3e4 100644
--- a/src/snek/form/register.py
+++ b/src/snek/form/register.py
@@ -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"
diff --git a/src/snek/mapper/__init__.py b/src/snek/mapper/__init__.py
new file mode 100644
index 0000000..dc9e047
--- /dev/null
+++ b/src/snek/mapper/__init__.py
@@ -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]
\ No newline at end of file
diff --git a/src/snek/mapper/user.py b/src/snek/mapper/user.py
new file mode 100644
index 0000000..5b8671e
--- /dev/null
+++ b/src/snek/mapper/user.py
@@ -0,0 +1,6 @@
+from snek.system.mapper import BaseMapper
+from snek.model.user import UserModel
+
+class UserMapper(BaseMapper):
+ table_name = "user"
+ model: UserModel
\ No newline at end of file
diff --git a/src/snek/model/__init__.py b/src/snek/model/__init__.py
index e69de29..52af21a 100644
--- a/src/snek/model/__init__.py
+++ b/src/snek/model/__init__.py
@@ -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]
diff --git a/src/snek/model/user.py b/src/snek/model/user.py
index 44553f8..254b6c9 100644
--- a/src/snek/model/user.py
+++ b/src/snek/model/user.py
@@ -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,}")
diff --git a/src/snek/service/__init__.py b/src/snek/service/__init__.py
new file mode 100644
index 0000000..4038f70
--- /dev/null
+++ b/src/snek/service/__init__.py
@@ -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]
\ No newline at end of file
diff --git a/src/snek/service/user.py b/src/snek/service/user.py
new file mode 100644
index 0000000..cde4b8c
--- /dev/null
+++ b/src/snek/service/user.py
@@ -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}.")
+
\ No newline at end of file
diff --git a/src/snek/static/generic-form.js b/src/snek/static/generic-form.js
index 11fea47..58a67e2 100644
--- a/src/snek/static/generic-form.js
+++ b/src/snek/static/generic-form.js
@@ -224,7 +224,11 @@ class GenericForm extends HTMLElement {
}
@media (max-width: 500px) {
+ width:100%;
+ height:100%;
form {
+ height:100%;
+ width: 100%;
width: 80%;
}
}`
diff --git a/src/snek/static/html-frame.js b/src/snek/static/html-frame.js
index 22581ce..0d5d4c9 100644
--- a/src/snek/static/html-frame.js
+++ b/src/snek/static/html-frame.js
@@ -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}`;
diff --git a/src/snek/static/markdown-frame.js b/src/snek/static/markdown-frame.js
new file mode 100644
index 0000000..e2b7a77
--- /dev/null
+++ b/src/snek/static/markdown-frame.js
@@ -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);
\ No newline at end of file
diff --git a/src/snek/static/style.css b/src/snek/static/style.css
new file mode 100644
index 0000000..990fcf9
--- /dev/null
+++ b/src/snek/static/style.css
@@ -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;
+
+}
\ No newline at end of file
diff --git a/src/snek/system/api.py b/src/snek/system/api.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/snek/system/form.py b/src/snek/system/form.py
index 68f7c0f..f9ebebb 100644
--- a/src/snek/system/form.py
+++ b/src/snek/system/form.py
@@ -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)
\ No newline at end of file
diff --git a/src/snek/system/http.py b/src/snek/system/http.py
index 0b16bee..b5e8b4f 100644
--- a/src/snek/system/http.py
+++ b/src/snek/system/http.py
@@ -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 and 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 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 = [' 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)
diff --git a/src/snek/system/markdown.py b/src/snek/system/markdown.py
new file mode 100644
index 0000000..bded949
--- /dev/null
+++ b/src/snek/system/markdown.py
@@ -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"