First commit

This commit is contained in:
Dita Aji Pratama 2023-08-13 21:55:18 +07:00
commit 0e5052676b
17 changed files with 765 additions and 0 deletions

0
.nojekyll Normal file
View File

1
LICENSE.md Normal file
View File

@ -0,0 +1 @@
This work is licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported License. To view a copy of this license, visit https://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA.

22
README.md Normal file
View File

@ -0,0 +1,22 @@
# CostaPy
Python Web Framework. Build with CherryPy and Mako.
## License
CostaPy
Copyright (C) 2022 Dita Aji Pratama
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see https://www.gnu.org/licenses/.

30
_sidebar.md Normal file
View File

@ -0,0 +1,30 @@
* [Home](/)
* [Getting Starter](pages/getting-starter.md)
* [Known the structure](pages/structure.md)
* core (extension script)
* html
* authentication
* [loggorilla](pages/core/loggorilla.md)
* uploading
* mailme
* Configuration
* [Server](pages/configuration/server.md)
* [Global Variable](pages/configuration/globalvar.md)
* [Directory](pages/configuration/directory.md)
* [Templating](pages/configuration/template.md)
* [Database](pages/configuration/database.md)
* content
* [Handler](pages/content/handler.md)
* [Import the modules](pages/content/handler?id=import-the-modules)
* [Routing the handler](pages/content/handler?id=routing-the-handler)
* [Add modules into handler](pages/content/handler?id=add-modules-into-handler)
* [Request with JSON](pages/content/handler?id=request-with-json)
* [Request with POST](pages/content/handler?id=request-with-post)
* [Explanation](pages/content/handler?id=explanation)
* [Session](pages/content/handler?id=session)
* [Sample](pages/content/handler?id=sample)
* [Modules](pages/content/modules.md)
* static
* error pages
* [Main Process](pages/main-process.md)
* Sessioning

BIN
images/structure.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

24
index.html Normal file
View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CostaPy</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="description" content="Description">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/dark.css">
</head>
<body>
<div id="app"></div>
<script>
window.$docsify = {
name: 'CostaPy',
repo: '',
loadSidebar: true
}
</script>
<!-- Docsify v4 -->
<script src="//cdn.jsdelivr.net/npm/docsify@4"></script>
<script src="//cdn.jsdelivr.net/npm/docsify/lib/plugins/emoji.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,29 @@
# Database (config/database.py)
This is the sample template for configure it
db_default = {
'host' : 'localhost',
'user' : 'root',
'password' : '',
'database' : 'your_db',
'autocommit' : True,
}
You also can make more than 1 database configuration like this
db_default = {
'host' : 'localhost',
'user' : 'root',
'password' : '',
'database' : 'your_db',
'autocommit' : True,
}
db_other = {
'host' : 'localhost',
'user' : 'root',
'password' : '',
'database' : 'other_db',
'autocommit' : True,
}

View File

@ -0,0 +1,34 @@
# Directory (config/directory.py)
`directory.py` is the place for storing your path. It is useful to calling the path more efficiently. there is 2 method that you can store your path. store it in variable for templating configuration, and store it as object for routing the url.
This is for a static error pages. This variable will be use in `server` configuration.
def erpadir(err):
return f'static/error/{err}.html'
This is example that use for templating. This variable will be use in `template` configuration.
user_page = "static/user/page"
user_template = "static/user/template"
admin_page = "static/admin/page"
admin_template = "static/admin/template"
email_page = "static/email/page"
email_template = "static/email/template"
And this is example that use for routing the url
dirconfig = {
'/' :
{
'tools.sessions.on' : True ,
'tools.staticdir.root' : os.path.abspath(os.getcwd()) ,
},
'/your_dir' :
{
'tools.staticdir.on' : True ,
'tools.staticdir.dir' : './static/your-dir' ,
},
}

View File

@ -0,0 +1,11 @@
# Global Variable (config/globalvar.py)
`globalvar.py` is the place for storing your Global Variable.
GV_base_url </br>
Is the variable for your base URL (without `/` in the end).
GV_title </br>
Is the variable for your web title.
You can put anything in here. like a variable or def.

View File

@ -0,0 +1,27 @@
# Server (config/server.py)
`server` is the place to configure the server things.
`tools.sessions.on` <br>
**Default:** `True` <br>
**Description:** Enable a sessions
`engine.autoreload.on` <br>
**Default:** `False` <br>
**Description:** Auto Reload when source code change. Don't use it in production.
`request.show_tracebacks` <br>
**Default:** `False` <br>
**Description:** Show traceback for debugging in development purposes.
It have the configuration for the static error page for error code 403, 404, and 500 too.
from config import directory
update = {
...
'error_page.403' : directory.erpadir(403),
'error_page.404' : directory.erpadir(404),
'error_page.500' : directory.erpadir(500),
...
}

View File

@ -0,0 +1,34 @@
# Templating (config/template.py)
Templating is useful when you had more than 1 website template for difference use case. For an example, when you had user and admin in the use case actor, let say we can do the website for user have a navbar and footer, and the website for admin have a navbar and sidebar.
Before you create a template, make sure your `directory` configuration is ready for storing templates and pages. For an example:
user_page = "static/user/pages"
user_template = "static/user/template"
To create the template, you need to insert this code in `def __init__(self)`
self.html_user_pages = html.main.get_html(directory.user_page)
self.html_user_template = html.main.get_html(directory.user_template)
if you had admin template or email template, you just need to add the code. for the example like this
self.html_user_pages = html.main.get_html(directory.user_page)
self.html_user_template = html.main.get_html(directory.user_template)
self.html_admin_pages = html.main.get_html(directory.admin_page)
self.html_admin_template = html.main.get_html(directory.admin_template)
self.html_email_pages = html.main.get_html(directory.email_page)
self.html_email_template = html.main.get_html(directory.email_template)
and then you need create function for each of your template in main class like this
def user(self, page):
params_list = {
"template" : self.html_user_template ["user.html" ],
"topnav" : self.html_user_template ["user-topnav.html" ],
"container" : self.html_user_pages [page+".html" ]
}
return params_list

243
pages/content/handler.md Normal file
View File

@ -0,0 +1,243 @@
# Handling the modules
## Import the modules
import modules.api.jwt as api_jwt
We can see the `modules.api.jwt` in the import, It mean `modules/api/jwt.py`.
## Routing the handler
The routing is starting in this class:
@cherrypy.tools.accept(media="application/json")
class handler(pages.main):
def __init__(self):
pages.main.__init__(self)
def index(self, **kwargs):
...
index.exposed = True
def <page_name>(self, **kwargs):
...
<page_name>.exposed = True
The `index` on there is `yourdomain.com/`.
let say the `<page_name>` is `about` page. so the route for the `about` is `yourdomain.com/about`.
How about if you want to create a route like this?: `yourdomain.com/about/profile` & `yourdomain.com/about/contact`.
The syntax will like this:
@cherrypy.tools.accept(media="application/json")
class handler(pages.main):
def __init__(self):
pages.main.__init__(self)
def index(self, **kwargs):
kwargs["params_page"] = pages.main().user("home")
return user_home.main().html(kwargs)
index.exposed = True
class about(pages.main):
def profile(self, **kwargs):
...
profile.exposed = True
def contact(self, **kwargs):
...
contact.exposed = True
about=about()
## Add modules into handler
The handler request have a 2 kind method:
- Request with JSON
- Request with POST
### Request with JSON
when you create the handler with JSON request, the pattern will look like this:
def <page_name>(self, **kwargs):
<authentication_token_check>
<session_token>
<default_response>
if cherrypy.request.method == 'OPTIONS':
cherrypy_cors.preflight(allowed_methods=['GET', 'POST'])
if cherrypy.request.method == 'POST':
try:
cherrypy.serving.response.headers['Content-Type'] = 'application/json'
kwargs["body"] = cherrypy.request.body.read()
...
<template_parameter>
<summon_module>
<response>
...
except Exception as e:
<error_response>
return <response>
<page_name>.exposed = True
### Request with POST
The handler with HTML request pattern look more simple than JSON request one. It look like this:
def <page_name>(self, **kwargs):
<authentication_token_check>
<session_token>
<template_parameter>
<summon_module>
<response>
return <response>
<page_name>.exposed = True
You can make it simple the pattern to look like this:
def <page_name>(self, **kwargs):
<authentication_token_check>
<session_token>
<template_parameter>
return <summon_module>
<page_name>.exposed = True
### Explanation
The `<page_name>` is where you name the page.
`<authentication_token_check>` is the optional. It looks like this:
authentication.token_check(f"{globalvar.GV_base_url}/?message=forbidden")
`<session_token>` is optional. It will use to get the session token and bring it into modules. Here is the example:
kwargs["session_token"] = cherrypy.session.get("token")
`<default_response>` is the optional one. It is for safely giving return.
When you want to use a template, we can use `<template_parameter>` on there.
For an example we want to use `user` and `email` template, so this is a sample:
kwargs["template_user" ] = pages.main().user("register")
kwargs["template_email" ] = pages.main().email("contact")
The `template_user` and `template_email` now can be use in the module.
`<summon_module>`, `<response>`, and `<error_response>` is the custom things.
You can create a JSON response like this:
module = api_jwt.main().change(kwargs)
response = json.dumps(module, indent=2)
return response.encode()
or HTML response like this:
return page_register.main().html(kwargs)
For the `return`, you can change it into `raise` for redirect. Here is the example:
...
response = api_auth.main().register(kwargs)
if response["status"] == "success":
raise cherrypy.HTTPRedirect(f"{globalvar.GV_base_url}/?message=success")
else:
raise cherrypy.HTTPRedirect(f"{globalvar.GV_base_url}/?message=failed")
...
### Session
To keep session, we can use `cherrypy.session` like this:
...
response = api_auth.main().login(kwargs)
if response["status"] == "success":
token = response["data"]["token" ]
username = response["data"]["username" ]
try:
cherrypy.session["token" ] = str( token )
cherrypy.session["username" ] = str( username )
raise cherrypy.HTTPRedirect('/?message=success')
except Exception as e:
print(f"Error: {e}")
raise cherrypy.HTTPRedirect('/login?message=failed')
else:
raise cherrypy.HTTPRedirect('/login?message=failed')
...
And for the logout session, you can use this:
...
cherrypy.lib.sessions.expire()
raise cherrypy.HTTPRedirect(f"{globalvar.GV_base_url}/?message=logout")
...
## Sample
### JSON method
def change(self, **kwargs):
response = '{}'
if cherrypy.request.method == 'OPTIONS':
cherrypy_cors.preflight(allowed_methods=['GET', 'POST'])
if cherrypy.request.method == 'POST':
try:
cherrypy.serving.response.headers['Content-Type'] = 'application/json'
kwargs["body"] = cherrypy.request.body.read()
module = api_jwt.main().change(kwargs)
response = json.dumps(module, indent = 2)
except Exception as e:
response = '{}'
return response.encode()
change.exposed = True
### POST method
For giving JSON response:
def change(self, **kwargs):
module = api_jwt.main().change(kwargs)
response = json.dumps(module, indent=2)
return response.encode()
change.exposed = True
For giving HTML response with Template page:
def register(self, **kwargs):
kwargs["params_page"] = pages.main().user("register")
return page_register.main().html(kwargs)
register.exposed = True
as you can see in here `pages.main().user("register")`, you need to add template pages in the parameter if you want to use a template.
### Combine method
Here is another JSON response that use a Template page:
def contact(self, **kwargs):
balikan = '{}'
if cherrypy.request.method == 'OPTIONS':
cherrypy_cors.preflight(allowed_methods=['GET', 'POST'])
if cherrypy.request.method == 'POST':
try:
cherrypy.serving.response.headers['Content-Type'] = 'application/json'
kwargs["body"] = cherrypy.request.body.read()
kwargs["email"] = pages.main().contact()
response = api_contact.main().contact(kwargs)
balikan = json.dumps(response, indent = 2)
except Exception as e:
print(f"DEBUG ERROR: {e}")
balikan = '{}'
return balikan.encode()
contact.exposed = True
you can add the Template on the parameter like this:
kwargs["email"] = pages.main().contact()

60
pages/content/modules.md Normal file
View File

@ -0,0 +1,60 @@
# CostaPy Modules
## 1. Create APIADDR variable for LogGorilla
APIADDR = "/your/api/page/directory"
## 2. Declare default response
response = {}
## 3. Connect database
main_db = mariadb.connect(**database.main_db)
cursor = main_db.cursor(dictionary=True)
## 4. Declare parameters
For POST method
jwt_token = params["jwt" ]
id = params["id" ]
name = params["name" ]
email = params["email" ]
For JSON method
body = params["body"].decode()
form_param = json.loads(body)
jwt_token = form_param["jwt" ]
id = form_param["id" ]
name = form_param["name" ]
email = form_param["email" ]
## 5. Get payload from JWT
public_key = open('.ssh/id_rsa.pub', 'r').read()
key = serialization.load_ssh_public_key(public_key.encode())
header_data = jwt.get_unverified_header(jwt_token)
payload = jwt.decode(
jwt = jwt_token,
key = key,
algorithms = [header_data['alg'], ]
)
session_id = payload["session_id"]
## 6. Main progress (try/except)
See `main-process.md` for the detail.
## 7. Close database
main_db.close()
## 8. Return response
return response

18
pages/core/loggorilla.md Normal file
View File

@ -0,0 +1,18 @@
# LogGorilla
`loggorilla` is the extension script for logging.
There is 4 type for this logging availability:
- `prcss` for logging a process
- `fyinf` for logging a value
- `accss` for logging an access
- `error` for logging the error
Here is the example
APIADDR = "/your/api/page/directory"
loggorilla.prcss(APIADDR, f"Checking authority" )
loggorilla.fyinf(APIADDR, f"Username: {username}" )
loggorilla.prcss(APIADDR, f"{username} try to logged in and failed" )
loggorilla.error(APIADDR, f"Username and Password is Incorrect" )

48
pages/getting-starter.md Normal file
View File

@ -0,0 +1,48 @@
# Getting Starter
## Requirement
You need a Python and this libraries to use CostaPy:
- cherrypy
- cherrypy-cors
- mako
- mysql-connector
- bcrypt
- pyjwt[crypto]
## Download
Download from repository
git clone https://github.com/ditaAjiPratama/costapy
## Installation
You can install it with run this command
sh install.sh
Here is the completed command
sudo apt-get install -y python3-pip
pip install --upgrade pip
pip install cherrypy
pip install cherrypy-cors
pip install mako
pip install mysql-connector
pip install bcrypt
pip install pyjwt[crypto]
## Usage
Use this command to start the web service
python<ver> costa.py <ip_address> <port> <service_name>
For an example like this
python3 costa.py localhost 80 My_Service
You can use nohup too and running it in the background like this
nohup python3 costa.py localhost 80 My_Service &

167
pages/main-process.md Normal file
View File

@ -0,0 +1,167 @@
# CostaPy Modules - Main Process
## Giving the response
Main response have 3 options:
- status
- desc
- data
The response status only have 2 options:
- success
- failed
Example for the success response:
response["status" ] = "success"
response["desc" ] = "Product list collected"
response["data" ] = {
"product" : product_list,
"other" : "Some random text"
}
Example for the failed response:
response["status" ] = "failed"
response["desc" ] = "Something went wrong"
response["data" ] = {
"exception" : str(e)
}
## Fetching data
### fetchone
cursor.execute(f"SELECT * FROM tablename WHERE fieldname = {id} ")
row = cursor.fetchone()
token = row['token' ].decode()
id = row['id' ]
name = row['name' ]
email = row['email' ]
### fetchall
cursor.execute(f"SELECT * FROM product ")
product_list = cursor.fetchall()
### Nested fetchall
Variables:
- `l1` is mean `List` level `1`
- `c2` is mean `Count` level `2`
- `d3` is mean `Data` level `3`
- etc
Sample:
texture_list = []
cursor.execute(f"SELECT * FROM tableone WHERE fieldname = '{key}' ")
l1 = cursor.fetchall()
c1 = 0
for d1 in l1:
texture_list.append({
"id" :d1["id" ],
"name" :d1["name" ],
"desc" :d1["desc" ]
})
cursor.execute(f"SELECT * FROM tabletwo WHERE keyfield = '{d1['id']}' ")
l2 = cursor.fetchone()
texture_list[c1]["owner"] = l2
cursor.execute(f"SELECT * FROM tablethree WHERE keyfield = '{d1['id']}' ")
l2 = cursor.fetchall()
texture_list[c1]["file"] = []
c2 = 0
for d2 in l2:
texture_list[c1]["file"].append({
"id" :d2["id" ],
"filedir" :d2["filedir" ],
"filetype" :d2["filetype" ]
})
c2 += 1
c1 += 1
## Get the last row ID from insert query
cursor.execute(f"INSERT INTO `product_files` VALUES (DEFAULT, '{webdir}', '{filename}' ) ")
product_files_lastrowid = cursor.lastrowid
## Begin, Rollback, and Commit
Begin, rollback, and commit can be useful if you use more than 1 process that cannot be separate. For example: more than 1 table insertion query, inserting query while upload success, etc.
The pattern:
cursor.execute("BEGIN;")
try:
# Process and response
except Exception as e:
cursor.execute("ROLLBACK;")
# Process and response when failed
cursor.execute("COMMIT;")
Sample:
cursor.execute("BEGIN;")
try:
cursor.execute(f"INSERT INTO `files` VALUES (DEFAULT, '{webdir}', '{filename}' ) ")
files_lastrowid = cursor.lastrowid
cursor.execute(f"INSERT INTO `thumbnail` VALUES (DEFAULT, '{image}', '{files_lastrowid}' ) ")
response["status" ] = "success"
response["desc" ] = "insert success"
loggorilla.prcss(APIADDR, f"insert success")
except Exception as e:
cursor.execute("ROLLBACK;")
response["status" ] = "failed"
response["desc" ] = "There is error when processing try. See the exception for the clue."
response["data" ] = {
"exception" : str(e)
}
loggorilla.error(APIADDR, f"{str(e)}")
cursor.execute("COMMIT;")
## File management
### Uploading
mediafile = params["mediafile"]
name = "helloworld"
ext = pathlib.Path(mediafile.filename).suffix
dir = f"/srv/media/material/texture"
uploading.main(mediafile, name+ext, dir)
It will be overwrite if the file already on there.
### Removing
import pathlib
import glob
import os
dir = pathlib.Path(f"/srv/media/material/texture")
name = "helloworld.png"
for row in glob.iglob(os.path.join(dir, name)):
os.remove(row)
It can combining with `*`, for example:
dir = pathlib.Path(f"/srv/media/product/*/file")
name = "*.zip"

17
pages/structure.md Normal file
View File

@ -0,0 +1,17 @@
# Known the structure
![CostaPy structure](../images/structure.png "CostaPy have a 4 type of file")
It have a 4 type of file:
- Main
- Core
- Configuration
- Content
`Main` is the file that you will run with the Python.
`Core` is the place to put the extension script.
`Configuration` is for configuration.
and `Content` is the place for you to create a content.