[PY 3.6+] Trinity-py - A flask based CMS for Python 3.6+

Status
Not open for further replies.

griimnak

You're a slave to the money then you die
Jul 20, 2013
955
794
I'm not talking about routing or React. Just how the coding structure is in React / Redux
ooh, yeah pm me with that.
I was thinking earlier, i'm gonna have to start structuring the js because i can't call functions that aren't needed on pages that don't need them like below:
( )
 

MayoMayn

BestDev
Oct 18, 2016
1,423
683
Let me give it a go.
Just gonna setup the CMS and install the necessary tools really quickly.
 

griimnak

You're a slave to the money then you die
Jul 20, 2013
955
794
RLmwuQK.png
Project name changed
Since the thought of this project, the name it was assigned with was "Trinity3". The "3" was supposed to represent Python 3. Because at the time of this project taking place, Python 2 was still well supported and somewhat mainstream. Seeing that this project has been developing slowly but surely for 3 years now, I figured i'd at least fix the name. I went with Trinity-py because version naming looks cleaner, less numbers.
RIP
k8I15Gh.png


FlXTUkT.png&hash=57eaa134a805a2ea2f8308e312ded770

lmao god, looks so shit.

Performance improvements & changes
  • Ditched ConfigParser() for ujson
    benchmark-json-python3.png

  • Using argon2 from passlib to encrypt passwords. ( )
  • Mimetypes can be added, removed or modified in config.json ( )
  • Following MVT instead of MVC (was basically doing that from the beginning so i removed all association with mvc)
  • Re-wrote __init__.py 46lns vs 95lns ( ) vs ( )
  • Re-wrote config.py implemented ujson as config reader 46lns vs 31lns and json is much faster and flexible( ) vs ( )

Improved project structure
  • Moved login system to app/auth ( )
  • Moved self scripts and systems to app/modules ( )
  • Removed "views" folder and gave admin templates their own folder. ( )
  • Static assets are more tidy ( )
  • Moved routes to app.py root ( )

All changes have been pushed to
**note some modules like content_editor.py are still messy from the old port, beware

Admin panel & Screenshots
  • Completely re-wrote all of my old IsoLite css that was kinda bloaty. 6kb 350lns ( )

New login:

0pZTvVniQBifxXy-Gz20og.png


Old login:

h3cGCGC1Rue0HUkteoPEaw.png


--

New dash:

ESgedUiRRHu76CDZODdFDg.png

fully responsive so far
F1LCPuQYTRC3wr_w5zXEGA.png



Old dash:

-MN-_eHZQNKlkJVUqPC0rQ.png

 
Progression Log (not committed)

(admin theme)
  • Ditched Font Awesome for IonIcons (actually really good!) and tweaked side nav
    OzNTJBSOSDupiXs_8XgO1A.png
  • 315lns > 350lns trinity-admin.css
  • Started route manager
7BMzXgeuRQKz-DasUFVx_g.png


rqybhSw0RviFNVJfyZMMNg.png

 


Progression Log (committed)
UJOl5v_7T2ywvwOkq8ahKg.png


  • Statistics box finished. (will add more stats later)
  • Routes box finished 100%

All pushed to
 
Big Commit
This is pretty juicy, i saved the thread post until i had more finished. This is a log of the past 3 days.

Admin panel
  • Login patched; was vulnerable to re-submission spamming, login re-written
    ( ) and model: ( )

  • Added live clock to admin panel
    piAavxK.gif


  • Finished basic modal to counter accidental button smack
  • Started Python explorer/editor/haven't really came up with a proper name for it

  • Writing is done
    vRmh-v1nTq_Z7zPnTD9org.png


  • Here's a snip of how the Map returns files to the view etc
    EDIT:
    nCJ5MF3tRfyXQoOqik9YzQ.png


    VIEW:
    xCOPzK2MQKKHVrRUwS9HUQ.png

Misc
  • Renamed controllers folder to views (MVT)
  • trinity-admin.css is now nearing 12kb (11kb currently) @ 515 lines ( )

  • Everything is currently working on both windows (dev) and linux ( )
    k6xhyAxrSFCuco2491TBlA.png
 
Last edited:

Marcel

You're the guy who stole my car
Jul 17, 2015
466
208
Nice release man, always expect good projects to be coming out of a stoner like you. ;)
 

griimnak

You're a slave to the money then you die
Jul 20, 2013
955
794
  • Added functionality to generate a new blank file
  • Added functionality to delete a file from a directory within app
  • All actions have modals to ensure no accidental actions
  • Started using python 3.6 f-strings
    f-strings-1.png

y-j1_hl9RquPZpYfBHNCOw.png


updated
 

  • Started Trinity Shell
  • ( )
  • ( )
 

Build tr4.5
RLmwuQK.png
Re-written once again.
Minified dependency list
* Old dependency list (7):
flask flask-compress pymysql dbutils ujson passlib argon2_cffi (encryption backend)

* New dependency list (4):
pymysql dbutils passlib argon2_cffi (encryption backend)


Ditched Flask in favor of raw/pure WSGI
What is WSGI?
W
eb Server Gateway Interface
is a specification for a standardized interface between Web servers and Python Web frameworks/applications.

Why go raw WSGI over Flask?
Flask is a Python web framework, WSGI is the low level interface to make web frameworks.
Python, out of the box, only has a low level wsgi module, Flask is essentially a layer ontop of the wsgi module, to speed up development.
As a result of going raw WSGI over Flask, i , removed dependencies, and added more deployment compatibility.


Structure
Code:
 - app
    - __init__
    - configs
        - routes.py
        - config.py
    - models
        - services
    - views
    - templates

- trinity
    - __init__
    - database
    - router
        - response.py
        - request.py
    - template
    -utilites
        - reloader.py

So, as you can see from above.. Trinity is no longer just an "app" instance / cms. It's a web framework with the "app" instance / cms built in.

Performance gains
Check it out. vs

Trinity app instance
PHP:
import pymysql
from DBUtils.PersistentDB import PersistentDB

from trinity import Trinity

from app.configs.config import config
from app.configs.routes import urls


app = Trinity(
    routes=urls,
    tpl_dir="templates"
)


def connect_db():
    return PersistentDB(
        creator=pymysql,
        host=config['mysql']['host'],
        user=config['mysql']['user'],
        password=config['mysql']['passw'],
        database=config['mysql']['database'],
        autocommit=True,
        charset='utf8mb4',
        cursorclass=pymysql.cursors.DictCursor
    )

def get_db():
    if not hasattr(app, 'db'):
        app.db = connect_db()

    return app.db.connection()

(I tried to make the app instance kinda like Flask's)
PHP:
app = Flask(
__name__,
template_folder="views/",
static_folder="../pub"
)

A view
PHP:
from . import tpl
from trinity.router import Response

def View(request):
    text = "World!"
    return Response(tpl.render("home/main.html", test=text))

A template
PHP:
<!DOCTYPE html>
<html>
<head>
  <title>HOME</title>
</head>
<body>
    Hello {{test}}
</body>
</html>

Trinity Framework
__init__.py

PHP:
import re

from trinity.template import to_regex
from trinity.router import Request

from app.views import not_found


class Trinity(object):
    """
    Main WSGI application. Sets routes and tpl_dir on init
    and dispatches Request object on __call__. Dispatcher
    then matches _req_url with _urls set by Trinity.
    """
    def __init__(self, routes=dict(), tpl_dir=""):
        self._routes = routes
        self._tpl_dir = tpl_dir

    def __call__(self, environ, start_response):
        response = self.dispatch(Request(environ))
        start_response(response.status, response.wsgi_headers)
        # Yield dispatcher response
        yield response.body.encode('UTF-8')

    def dispatch(self, request):
        _req_url = request.path
        for _url, View in self._routes.items():
            match = re.search(to_regex(_url), _req_url)
            if match is not None:
                # Send request params and return View
                request.params = match.groups()
                return View(request)
        # If match is None
        return not_found.View(request)

Router -> webObjects
request.py

PHP:
from urllib.parse import parse_qs


class Request(object):
    """
    Creates a request object.
    """
    def __init__(self, environ):
        self.environ = environ
        self.path = environ["PATH_INFO"]
        self.method = environ['REQUEST_METHOD']
        self.headers = {k: v for k, v in environ.items() if k.startswith("HTTP_")}
        self.params = None
        self.GET = parse_qs(environ["QUERY_STRING"])
        self.POST = {}

        # Experimental
        if environ.get('HTTPS', 'off') in ('on', '1'):
            self.environ['wsgi.url_scheme'] = 'https'
        else:
            self.environ['wsgi.url_scheme'] = 'http'

        if self.method == "POST":
            try:
                content_length = int(environ.get("CONTENT_LENGTH", 0))
            except ValueError:
                content_length = 0

            query_string = environ["wsgi.input"].read(content_length)
            self.POST = parse_qs(query_string)

response.py
PHP:
class Response(object):
    """
    Creates response object, binding headers, status and body.
    """
    def __init__(self, body, status="200 OK"):
        self.status = status
        self.body = str(body) or ""
        self.headers = {
            "Content-Type": "text/html",
            "Content-Length": str(len(self.body)),
            "X-Powered-By": "Trinity-py-tr4.5"
        }

    @property
    def wsgi_headers(self):
        return [(k, v) for k, v in self.headers.items()]

template
PHP:
import re
from jinja2 import Environment, PackageLoader

class Jinja2:
    """
    Jinja2 templating wrapper
    """
    def __init__(self, tpls_dir):
        self.jinja2_env = Environment(
            loader=PackageLoader("app", tpls_dir),
            autoescape=True)

    def render(self, name, **ctx):
        t = self.jinja2_env.get_template(name)
        return t.render(ctx)

def to_regex(template):
    """
    Converts string to templated regex
    """
    regex = ''
    var_regex = re.compile(r'''\{(\w+)(?::([^}]+))? \}''', re.VERBOSE)
    last_pos = 0
    for match in var_regex.finditer(template):
        regex += re.escape(template[last_pos:match.start()])
        var_name = match.group(1)
        expr = match.group(2) or '[^/]+'
        expr = '(?P<%s>%s)' % (var_name, expr)
        regex += expr
        last_pos = match.end()
    regex += re.escape(template[last_pos:])
    regex = '^%s$' % regex
    return regex

Everything has been pushed to
 
Development Server
Finished local development server, command line and gui.

Command line / server
X2tw4F6zR4Kdum6aJb-s-Q.png


GUI wrapper
YSW_hRVIQASECOXGx5YpwQ.png


_yiHHfnQQN2XzT416sooZw.png


Reloaded:
oQP-vX8TR2CzTjfuVNV-fw.png



Code:

dev_server.py
PHP:
from datetime import datetime
from sys import version_info

from wsgiref.simple_server import make_server

from app import app
from app.configs.config import config


class TrinityDevServer:
    """ Trinity development server """
    def __init__(self):
        print("[DEV][INFO] Note: Not intended for production use, refer to readme.md for more info.")
        self.show_splash()
        self.load_config()
        self.create_server()

    def show_splash(self):
        _pyver = ".".join(map(str, version_info[:3]))
        _splash = '''
         _______    _       _
        (_______)  (_)     (_)  _
            _  ____ _ ____  _ _| |_ _   _ _____ ____  _   _
           | |/ ___) |  _ \\| (_   _) | | (_____)  _ \\| | | |
           | | |   | | | | | | | |_| |_| |     | |_| | |_| |
           |_|_|   |_|_| |_|_|  \\__)\\__  |     |  __/ \\__  |
                                   (____/      |_|   (____/
        '''

        print('{:^60}'.format(_splash))
        print(f"[DEV][ OK ] Python {_pyver}\n")

    def load_config(self):
        _error = False
        print("[DEV][INFO] Requesting config ..")

        try:
            self.config = config
        except Exception as error:
            _error = True
            print("\n[DEV][ERRO] "+str(error))
            exit()
        if _error != True: print("[DEV][ OK ] Configuration loaded! ")

    def create_server(self):
        _error = False
        _parsed = str(self.config['trinity']['ip'])+\
        ":"+str(self.config['trinity']['port'])
        print(f"\n[DEV][INFO] Binding to {_parsed}..")

        try:
            with make_server(
                self.config['trinity']['ip'],
                self.config['trinity']['port'], app) as httpd:
                if _error != True:
                    _date = datetime.now().strftime('%H:%M%p - %m-%d-%Y')
                    print(f"[DEV][ OK ] READY! ({_parsed}) ({_date})\n")
                httpd.serve_forever()
        except Exception as error:
            _error = True
            print("\n[DEV][ERRO] "+str(error))
            exit()


if __name__ == '__main__':
    TrinityDevServer()

dev_server_gui.py
PHP:
import sys
import os
import time
import threading
import subprocess
import tkinter.ttk as ttk

from tkinter import *
from tkinter import messagebox

from dev_server import TrinityDevServer

class GUI:
    def __init__(self, master, arg=""):
        _width = 650
        _height = 460

        self.active = None
        self.master = master
        _title = master.title("Trinity Dev Server GUI")
        master.geometry(("%dx%d")%(_width, _height))
        master.maxsize(1000, 500)

        style = ttk
        style.Style().theme_use("clam")

        self.start_button = style.Button(master,
            text="Start server",
            command=self.load_server_instance
        )

        self.start_button.place(x = 10,y = 15)

        self.reload_button = style.Button(master,
            text="Force reload",
            command=self.reload_server_instance
        )

        self.reload_button.place(x = 120,y = 15)

        #areaOutput
        self.areaOutput = Text(master, fg="orange", bg="black")
        self.areaOutput.pack(expand=1, side='left')
        self.areaOutput.place(y=60)
        sys.stdout = self

        if arg == 'restart':
            print("[DEV][ OK ] Server restarted ! \n")
            self.load_server_instance()
        else:
            print(arg)
    def load_server_instance(self):
        if self.active != True:
            self.active = True
            def callback():
                self.t = threading.Thread(target=TrinityDevServer)
                self.t.daemon = True
                self.t.start()
            self.master.after_idle(callback)
        else:
            print("[DEV][ERRO] Server instance already active")
    def reload_server_instance(self):
        def callback():
            if self.active == True:
                subprocess.Popen(
                    [sys.executable, os.path.abspath(__file__), 'restart'])
                time.sleep(0.200)
                exit()
            else:
                print("[DEV][INFO] No server instance to reload.")
        self.master.after_idle(callback)

    def write(self, txt):
        self.areaOutput.insert(END, str(txt))

root = Tk()

try:
    arg = sys.argv[1]
except:
    arg = '''
Welcome to the Trinity Dev Server GUI.\n
Click `Start server` above to start serving.\n
Click `Force reload` to reload the server instance
'''
gui = GUI(root, arg)
root.mainloop()


All pushed to
 
Readme updated

It explains how to use virtualenv now. Should be great for python noobs

Added some deployment examples
Passenger (used on )
passenger_wsgi.py
PHP:
""" Wsgi for passenger phusion """

from app import app as application

Gunicorn
PHP:
[/B][/SIZE]
$ gunicorn app:app -w 4 -b 0.0.0.0:8000

Waitress
PHP:
waitress-serve --listen=127.0.0.1:8000 app:app
[SIZE=4][B]
CGI

cgi_wsgi.cgi
PHP:
from wsgiref.handlers import CGIHandler
from app import app

CGIHandler().run(app)
FastCGI
pip install flup
PHP:
fastcgi_wsgi.fcgi

from flup.server.fcgi import WSGIServer
from app import app as application

WSGIServer(application).run()
 

griimnak

You're a slave to the money then you die
Jul 20, 2013
955
794
Trinity-py is finished as a Flask CMS, there's some pretty useful admin stuff in it so I'm keeping the repo up.
I already started merging the recent Trinity Framework (Not the cms) code into a new Project called Gorton which will be a Pure ASGI Framework (no flask)

Links
Download:
Demo site:

Download:
Demo site:

This thread may now close
 
Last edited:
Status
Not open for further replies.

Users who are viewing this thread

Top