commit 66f89429366042c77599f3a9b8c1a7aecf976a4f Author: retoor 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)