commit 66f89429366042c77599f3a9b8c1a7aecf976a4f
Author: retoor <retoor@molodetz.nl>
Date:   Fri Jan 17 23:06:17 2025 +0100

    Initial commit.

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8cb2598
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,166 @@
+.vscode
+.history
+*.db*
+
+# ---> Python
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+#   This is especially recommended for binary packages to ensure reproducibility, and is more
+#   commonly ignored for libraries.
+#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+#   in version control.
+#   https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+#  and can be added to the global gitignore or merged into this file.  For a more nuclear
+#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..d41b81a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,13 @@
+PYTHON=./.venv/bin/python 
+PIP=./.venv/bin/pip 
+APP=./venv/bin/snek.serve
+PORT = 8081
+
+
+install:
+	python3 -m venv .venv 
+	$(PIP) install -e .
+
+run:
+	$(APP) --port=$(PORT)
+	
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b665fb1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,4 @@
+# Boeh
+
+## Description
+Matrix bot written in Python that says boeh everytime that Joe talks. He knows why.
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..07de284
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["setuptools", "wheel"]
+build-backend = "setuptools.build_meta"
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..bb6480d
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,24 @@
+[metadata]
+name = snek
+version = 1.0.0
+description = Snek chat server 
+author = retoor
+author_email = retoor@molodetz.nl
+license = MIT
+long_description = file: README.md
+long_description_content_type = text/markdown
+
+[options]
+packages = find:
+package_dir =
+    = src
+python_requires = >=3.7
+install_requires =
+    app @ git+https://retoor.molodetz.nl/retoor/app
+
+[options.packages.find]
+where = src
+
+[options.entry_points]
+console_scripts =
+    snek.serve = snek.server:cli
diff --git a/src/snek/app.py b/src/snek/app.py
new file mode 100644
index 0000000..feb2fc0
--- /dev/null
+++ b/src/snek/app.py
@@ -0,0 +1,20 @@
+from app.app import Application as BaseApplication
+from snek.forms import RegisterForm
+from aiohttp import web
+
+class Application(BaseApplication):
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.router.add_get("/register", self.handle_register)
+        self.router.add_post("/register", self.handle_register)
+
+    async def handle_register(self, request):
+        if request.method == "GET":
+            return web.json_response({"form": RegisterForm().to_json()})
+        elif request.method == "POST":
+            return self.render("register.html")
+
+if __name__ == '__main__':
+    app = Application()
+    web.run_app(app,port=8081,host="0.0.0.0")
diff --git a/src/snek/forms.py b/src/snek/forms.py
new file mode 100644
index 0000000..f4a7b24
--- /dev/null
+++ b/src/snek/forms.py
@@ -0,0 +1,40 @@
+from snek import models 
+
+class FormElement(models.ModelField):
+
+    def __init__(self,html_type, place_holder=None, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.place_holder = place_holder
+        self.html_type = html_type 
+
+    def to_json(self):
+        data = super().to_json()
+        data["html_type"] = self.html_type
+        data["place_holder"] = self.place_holder
+        return data 
+
+class Form(models.BaseModel):
+    pass
+
+class RegisterForm(Form):
+
+    username = FormElement(
+        name="username", 
+        required=True,
+        min_length=2,
+        max_length=20,
+        regex=r"^[a-zA-Z0-9_]+$",
+        place_holder="Username",
+        html_type="text"
+    )
+    email = FormElement(
+        name="email",
+        required=True,
+        regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$",
+        place_holder="Email address",
+        html_type="email"
+    )
+    password = FormElement(name="password",required=True,regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$",html_type="password")
+
+
+
diff --git a/src/snek/models.py b/src/snek/models.py
new file mode 100644
index 0000000..ec5d9ce
--- /dev/null
+++ b/src/snek/models.py
@@ -0,0 +1,263 @@
+import re
+import uuid
+import json 
+from datetime import datetime , timezone 
+from collections import OrderedDict
+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)
+
+class Validator:
+
+    @property
+    def value(self):
+        return self._value 
+
+    @value.setter 
+    def value(self,val):
+        self._value = json.loads(json.dumps(val,default=str))
+
+    @property
+    def initial_value(self):
+        return None
+
+    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):
+        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.value = value 
+        self.type = kind
+        self.help_text = help_text 
+        self.__dict__.update(kwargs)
+    @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 
+
+        if self.type == float or self.type == int:
+            if self.min_num is not None and self.value < self.min_num:
+                error_list.append("Field should be minimal {}.".format(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))
+        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))
+        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))
+        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.type is None and type(self.value) != self.type:
+            error_list.append("Invalid type. It is supposed to be {}.".format(self.type))
+        return error_list 
+        
+    def validate(self):
+        if self.errors:
+            raise ValueError("\n", self.errors)
+        return True
+
+    @property
+    def is_valid(self):
+        try:
+            self.validate()
+            return True
+        except ValueError:
+            return False
+
+    def to_json(self):
+        return {
+            "required": self.required,
+            "min_num": self.min_num,
+            "max_num": self.max_num,
+            "min_length": self.min_length,
+            "max_length": self.max_length,
+            "regex": self.regex,
+            "value": self.value,
+            "type": self.type,
+            "help_text": self.help_text,
+            "errors": self.errors,
+            "is_valid": self.is_valid
+        }
+
+class ModelField(Validator):
+    def __init__(self,name=None,save=True, *args, **kwargs):
+        self.name = name 
+        
+        self.save = save
+        super().__init__(*args, **kwargs)
+
+
+class CreatedField(ModelField):
+    
+    @property
+    def initial_value(self):
+        return now()
+
+    def update(self):
+        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 
+    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")
+
+    def __init__(self, *args, **kwargs):
+        print(self.__dict__)
+        print(dir(self.__class__))
+        for key in dir(self.__class__):
+            obj = getattr(self.__class__,key)
+
+            if isinstance(obj,Validator):
+                self.__dict__[key] = copy.deepcopy(obj)
+                print("JAAA")
+                self.__dict__[key].value = kwargs.pop(key,self.__dict__[key].initial_value)
+
+    def __setitem__(self, key, value):
+        obj = self.__dict__.get(key)
+        if isinstance(obj,Validator):
+            obj.value = value 
+
+    def __getattr__(self, key):
+        obj = self.__dict__.get(key)
+        if isinstance(obj,Validator):
+            print("HPAPP")
+            return obj.value 
+        return obj
+
+
+    def __getitem__(self, key):
+        obj = self.__dict__.get(key)
+        if isinstance(obj,Validator):
+            return obj.value 
+
+    def __setattr__(self, key, value):
+        obj = getattr(self,key)
+        if isinstance(obj,Validator):
+            obj.value = value
+        else:
+            setattr(self,key,value)
+    #def __getattr__(self, key):
+    #    obj = self.__dict__.get(key)
+    #    if isinstance(obj,Validator):
+    #        return obj.value
+    @property 
+    def record(self):
+        obj = self.to_json()
+        record = {}
+        for key,value in obj.items():
+            if getattr(self,key).save:
+                record[key] = value.get('value')
+        return record
+
+    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(): 
+            if key == "record":
+                continue
+            value = self.__dict__[key]
+            if hasattr(value,"value"):
+                model_data[key] = value.to_json()
+        if encode:
+            return json.dumps(model_data,indent=2)
+        return model_data
+
+class FormElement(ModelField):
+
+    def __init__(self, place_holder=None, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.place_holder = place_holder
+
+
+
+class FormElement(ModelField):
+
+    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["place_holder"] = self.place_holder
+        return data 
+
+
+
+
+class TestModel(BaseModel):
+
+    first_name = FormElement(name="first_name",required=True,min_length=3,max_length=20,regex=r"^[a-zA-Z0-9_]+$",place_holder="First name")
+    last_name = FormElement(name="last_name",required=True,min_length=3,max_length=20,regex=r"^[a-zA-Z0-9_]+$",place_holder="Last name")
+    email = FormElement(name="email",required=True,regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$",place_holder="Email address")  
+    password = FormElement(name="password",required=True,regex=r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$",place_holder="Password")
+
+class Form:
+    username = FormElement(required=True,min_length=3,max_length=20,regex=r"^[a-zA-Z0-9_]+$",place_holder="Username")
+    email = FormElement(required=True,regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$",place_holder="Email address")
+    def __init__(self, *args, **kwargs):
+        self.place_holder = kwargs.pop("place_holder",None) 
+
+
+if __name__ == "__main__":
+    model = TestModel(first_name="John",last_name="Doe",email="n9K9p@example.com",password="Password123")
+    model2 = TestModel(first_name="John",last_name="Doe",email="ddd",password="zzz")
+    model.first_name = "AAA"
+    print(model.first_name)
+    print(model.first_name.value)
+    
+    print(model.first_name)
+    print(model.first_name.value)
+    print(model.to_json(True))
+    print(model2.to_json(True))
+    print(model2.record)