The Python3

Posh Framework

A Python3 minimal framework to getting tasks done.



The Python3 Posh framework is a web framework to getting tasks done quickly. A 7 Python files framework and mycgi.py utilizing the pip package multipart.

Basically, once Apache2.4 and mod_cgi, mod_cgid are installed and ready to go, a Python3 Posh framework can get you up and running in minutes complete with a database connected. There are 3 simple files that are at the core of a Python3 Posh website on the client/Internet-side. The server-side has 4 simple files also, to 1. create cookie sessions, to 2. connect to a database, and 3. to process the cookie, and 4. to handle errors. Finally, I will also use a custom class, mycgi.py, that works specifically with Python3 Apache2 cgi to allow to upload files because the cgi.py module is removed after Python3 version 3.13.


client/Internet side


These 3 files are to be located on the include_web_root variable path, for example, '/var/example/www/html/Example.com'

1. index page 

2. init page

3. page or pages of content (this can be one page or many pages depending on your project)

Let's begin...

1. The index page 

    I will use Example.com as the domain name to get us started.
    
    Therefore, your index is recommended to be called the following.
    
    index_example_com.py
    
    
    And create the page with the following code:

1. The index page

filename: index_example_com.py
    
#!/usr/bin/python3

import os
import sys
sys.stdout.reconfigure(encoding='utf-8')
from html import escape

include_server_side = '/var/example/www'
include_web_root = '/var/example/www/html/Example.com'

with open(f'''{include_server_side}/db_example_pg.py''') as f: exec(f.read())
cursor_content = database_content.cursor(cursor_factory = psycopg2.extras.RealDictCursor)   # postgresql
cursor_data = database_data.cursor(cursor_factory = psycopg2.extras.RealDictCursor)         # postgresql
#   #cursor_content = database_content.cursor(pymysql.cursors.DictCursor)                   # mysql
#   #cursor_data = database_data.cursor(pymysql.cursors.DictCursorr)                        # mysql

with open(f'''{include_server_side}/cookie_session_auth.py''') as f: exec(f.read())
session_cookie_value, session = session_cookie(session)

with open(f'''{include_server_side}/process_cookie.py''') as f: exec(f.read())
process_cookie_session()

with open(f'''{include_server_side}/mycgi.py''') as f: exec(f.read()) # forms, files
mycgi = MyCGI(os.environ)
mycgi.FieldStorage()

with open(f'''{include_web_root}/pages/display404.py''') as f: exec(f.read())

with open(f'''{include_server_side}/error_handler.py''') as f: exec(f.read())
sys.excepthook = exception_handler

with open(f'''{include_web_root}/example_init.py''') as f: exec(f.read())
mapper = Mapper(os.environ)


if __name__ == '__main__':
    
    html = ''
    
    print( session_cookie_value )
    print("Content-Type:text/html;charset=utf-8;")
    print() 
    
    if mapper.display404:
       print(show404(''))
       exit()

    int_pages = ['login.py', 'sign_up.py']

    if mapper.page in int_pages:
        html += mapper.connect(mapper.page, True)
    else:
        html += mapper.connect('example.py', True)
    
    print(html, end="")

2. The init page

filename: example_init.py


#!/usr/bin/python3

import os
import sys
from io import StringIO
from contextlib import redirect_stdout, redirect_stderr
from html import escape
import urllib


MAX_ARGS = 4


class Mapper:

    WEBSITE_DIRECTORY = ''
    BIZ = {}
    display404 = False
    page = ''

    def __init__(self, e):

        self.WEBSITE_DIRECTORY = os.path.dirname(e["SCRIPT_FILENAME"])
        v = []
        v = e["REQUEST_URI"].split('/')
        
        if e["REQUEST_URI"][-1] == '/':     # make trailing slash optional
            v.pop(len(v) - 1)
            
        if v[0] == '':                      # first slash empty string, remove
            v.pop(0)
        
        v_len = len(v)                      # how many items, trailing / optional
        
        if v_len >= 1:
            self.page = v[0]
            
            found_page = self.page.find('?')
            if found_page != -1:
                self.page = self.page[:found_page]
        else:
            self.page = 'example'


        if v_len == 1:                          #       /userName_companyName
            self.BIZ['userName_companyName'] = v[0]
        elif v_len == 2:                        #       /product_name/product_id
            self.BIZ['product_name'] = v[0]
            self.BIZ['product_id'] = v[1]
        elif v_len == 3:                        #       /company/product_name/product-id/
            self.BIZ['company'] = v[0]
            self.BIZ['product_name'] = v[1]
            self.BIZ['product_id'] = v[2]
        elif v_len == MAX_ARGS:                 #       /company/product_name/product-id/us/
            self.BIZ['company'] = v[0]
            self.BIZ['product_name'] = v[1]
            self.BIZ['product_id'] = v[2]
            self.BIZ['country'] = v[3]
        elif v_len > MAX_ARGS:
            pass
            #self.display404 = True
        
        if self.page == '':
            self.page = 'example'
            
        
    def connect(self, file, buffer_store=False):
        
        if buffer_store:
            with StringIO() as buf, redirect_stdout(buf), redirect_stderr(buf):
            
                logger.addHandler(logging.StreamHandler(stream=buf))
                
                with open(f'''{self.WEBSITE_DIRECTORY}/{file}''') as f: 
                    exec(f.read())
                    
                return buf.getvalue()
        else:
            with open(f'''{self.WEBSITE_DIRECTORY}/{file}''') as f:                         
                exec(f.read())



3. The actual page of content (can be several pages)

filename: example.py

#!/usr/bin/python3

def example(mapper):

    html = ''

    html += f'''
Example.com

Example.com
''' if 'userName_companyName' in mapper.BIZ: html += f'''BIZ['userName_companyName']: {mapper.BIZ['userName_companyName']} <br>''' return html html = example(mapper) print(html, end="")



Server-side 4 simple files:

These 4 files are to be located on the include_server_side variable path, for example, '/var/example/www'

1. db_example_pg.py             # to connect to a database

2. cookie_session_auth.py       # to create a cookie (for a session)  

3. process_cookie.py            # to process the cookie to create a session

4. error_handler.py             # to handle any error

1. The database connect file

filename: db_example_pg.py
#!/usr/bin/python3

import psycopg2
import psycopg2.extras

HOST = '127.0.0.1'
USERNAME = ''       # sometimes this may be 'postgres'
PASSWORD = ''       # some hard to guess password


DATABASE = 'example_content'

database_content = psycopg2.connect(f'''dbname={DATABASE} user={USERNAME} password={PASSWORD} host={HOST}''')


DATABASE = 'example_data'

database_data = psycopg2.connect(f'''dbname={DATABASE} user={USERNAME} password={PASSWORD} host={HOST}''')



#   #mysql
#   #import pymysql
#   #database_content = pymysql.connect(HOST,USERNAME,PASSWORD,DATABASE)
#   #database_data = pymysql.connect(HOST,USERNAME,PASSWORD,DATABASE)

2. The cookie create file

filename: cookie_session_auth.py

        psuedo code
        
            from http import cookies
                
                write cookie method, function
                    create a Session class
                        utilize the     cookies.SimpleCookie()
                                        simple_cookie.output()  # output with a generated a unique cookie session id

                read cookie method, function
                
                
                email me at:  opensource3 at yahoo com

3. The cookie process file

filename: process_cookie.py
        
        psudeo code
        
            process cookie session method, function 
                connect to a db and some code
            
            
            email me at:  opensource3 at yahoo com

4. The error handle file

filename: error_handler.py
#!/usr/bin/python3

import logging
from datetime import datetime

log_filename = f'''{include_server_side}/tmp_errors/''' + datetime.now().strftime("%Y-%m-%d %H-%M-%S")
log_formatter = logging.Formatter('%(asctime)s | %(levelname)s | %(message)s')
logger = logging.getLogger()
fh = logging.FileHandler(filename=log_filename, mode='w', delay=True)  
fh.setFormatter(log_formatter)
logger.addHandler(fh)

def exception_handler(exc_type, exc_value, exc_traceback):
    if issubclass(exc_type, KeyboardInterrupt):
        # Let the system handle things like CTRL+C
        sys.__excepthook__(*args)
    logger.error('Exception: ', exc_info=(exc_type, exc_value, exc_traceback))
    if os.path.exists(log_filename):
        print(show404(''))        



mycgi.py is utilized to replace the removed cgi.py module starting with Python 3.13


install the pip package multipart from https://github.com/defnull/multipart

pip install multipart
Note:

modify multipart.py line 15 to include MultiDict as so:
__all__ = ["MultiDict", "MultipartError", "MultipartParser", "MultipartPart", "parse_form_data"]




filename: mycgi.py

#!/usr/bin/python3

# The MIT License
# Copyright 2023 BestInternetSearch.com Inc.
# 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 os
import sys
from html import escape
import urllib
from multipart import MultipartParser, MultiDict, parse_options_header

forms, files = MultiDict(), MultiDict()

class MyCGI():
    
    _os_environ = []
    _escape = True
    _data = {}
    
    def __init__(self, os_environ, escape=True):
        self._os_environ = os_environ
        self._escape = escape
        
    def FieldStorage(self):
        e = self._os_environ
        if 'REQUEST_METHOD' in e:
            if e['REQUEST_METHOD'] == 'GET':
                self._data = urllib.parse.parse_qs(e['QUERY_STRING'])
            elif e['REQUEST_METHOD'] == 'POST':
                if 'CONTENT_TYPE' in e:
                    content_type, options = parse_options_header(e['CONTENT_TYPE'])
                    if 'multipart/form-data' in e['CONTENT_TYPE']:
                        if 'boundary' in e['CONTENT_TYPE']:
                            boundary = options.get("boundary", "")
                            if 'CONTENT_LENGTH' in e:
                                content_length = int(os.environ.get("CONTENT_LENGTH", "-1"))
                                stream = sys.stdin.buffer
                                for part in MultipartParser(stream, boundary, content_length):
                                    if part.filename or not part.is_buffered():
                                        files[part.name] = part
                    if 'application/x-www-form-urlencoded' in e['CONTENT_TYPE']:
                        self._data = urllib.parse.parse_qs(str(sys.stdin.read()), keep_blank_values=True)

    def getvalue(self, arg_key, default=''):
        if arg_key in self._data:
            value = self._data[arg_key]
            if isinstance(value, list):
                if self._escape == True:
                    return escape(self._data[arg_key][0])
                else:
                    self._data[arg_key][0]
            else:
                if self._escape == True:
                    return escape(value)
                else:
                    return value
        else:
            return default 



Usage of mycgi.py
include_server_side = '/var/awesome_website/www'
with open(f'''{{include_server_side}}/mycgi.py''') as f: exec(f.read())
mycgi = MyCGI(os.environ)
mycgi.FieldStorage()
user = mycgi.getvalue('user')      

Uploading files example #1 POST statement:
curl -F "text=default" -F "filename=@/home/computer/Desktop/aa.png" -F "filename=@/home/computer/Desktop/bb.png" http://example_website.com

filenames = files.getall('filename')
for x, file in enumerate(filenames):
    file.save_as(f"""/home/computer/Desktop/file{x}.png""")

Posh Framework Download

Finally, download the .1 version posh_project.zip. It includes a sample website, some sample files of login, sign_up, admin and an index page. Including authenticating of pages with a postgresql database, etc. And, Posh Project is created so that all files go to the index_example.py file. The only files that the url path goes directly to the files are the ajax_scripts, images, and videos. This is done with the following apache2.4 code:
	<Directory "/var/example/www/html/Example.com">
                Options +ExecCGI -Indexes
                AddHandler cgi-script .py
                AddHandler default-handler .jpg .png .gif .css .js .ico
                AllowOverride None
                Require all granted	

                RewriteEngine On

                RewriteCond %{REQUEST_URI} ^/ajax_scripts/              # direct url access to all the ajax_scripts back end files
                RewriteRule ^/ajax_scripts/(.*)$ ajax_scripts/$1 [L]

                RewriteCond %{REQUEST_URI} ^(.*).py                     # the pages all redirect to one controller file 
                RewriteRule (.*)$ index_example.py [L]

                RewriteCond %{REQUEST_URI} ^(.*)                        # this is for any other file, including js, css, jpg, png, etc.
                RewriteRule (.*)$ $1 [L]
                    
	</Directory>