commit 0e5052676bea840f5c849f8819d10033ae601668 Author: Dita Aji Pratama Date: Sun Aug 13 21:55:18 2023 +0700 First commit diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..03e3adf --- /dev/null +++ b/LICENSE.md @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6540165 --- /dev/null +++ b/README.md @@ -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/. diff --git a/_sidebar.md b/_sidebar.md new file mode 100644 index 0000000..3200247 --- /dev/null +++ b/_sidebar.md @@ -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 diff --git a/images/structure.png b/images/structure.png new file mode 100644 index 0000000..195db2a Binary files /dev/null and b/images/structure.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..212b185 --- /dev/null +++ b/index.html @@ -0,0 +1,24 @@ + + + + + CostaPy + + + + + + +
+ + + + + + diff --git a/pages/configuration/database.md b/pages/configuration/database.md new file mode 100644 index 0000000..32dbfe5 --- /dev/null +++ b/pages/configuration/database.md @@ -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, + } diff --git a/pages/configuration/directory.md b/pages/configuration/directory.md new file mode 100644 index 0000000..a405e03 --- /dev/null +++ b/pages/configuration/directory.md @@ -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' , + }, + } diff --git a/pages/configuration/globalvar.md b/pages/configuration/globalvar.md new file mode 100644 index 0000000..65769da --- /dev/null +++ b/pages/configuration/globalvar.md @@ -0,0 +1,11 @@ +# Global Variable (config/globalvar.py) + +`globalvar.py` is the place for storing your Global Variable. + +GV_base_url
+Is the variable for your base URL (without `/` in the end). + +GV_title
+Is the variable for your web title. + +You can put anything in here. like a variable or def. diff --git a/pages/configuration/server.md b/pages/configuration/server.md new file mode 100644 index 0000000..fad65c1 --- /dev/null +++ b/pages/configuration/server.md @@ -0,0 +1,27 @@ +# Server (config/server.py) + +`server` is the place to configure the server things. + +`tools.sessions.on`
+**Default:** `True`
+**Description:** Enable a sessions + +`engine.autoreload.on`
+**Default:** `False`
+**Description:** Auto Reload when source code change. Don't use it in production. + +`request.show_tracebacks`
+**Default:** `False`
+**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), + ... + } diff --git a/pages/configuration/template.md b/pages/configuration/template.md new file mode 100644 index 0000000..8ee2eef --- /dev/null +++ b/pages/configuration/template.md @@ -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 diff --git a/pages/content/handler.md b/pages/content/handler.md new file mode 100644 index 0000000..333b5fc --- /dev/null +++ b/pages/content/handler.md @@ -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 (self, **kwargs): + ... + .exposed = True + +The `index` on there is `yourdomain.com/`. + +let say the `` 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 (self, **kwargs): + + + + 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() + ... + + + + ... + except Exception as e: + + return + .exposed = True + +### Request with POST + +The handler with HTML request pattern look more simple than JSON request one. It look like this: + + def (self, **kwargs): + + + + + + return + .exposed = True + +You can make it simple the pattern to look like this: + + def (self, **kwargs): + + + + return + .exposed = True + +### Explanation + +The `` is where you name the page. + +`` is the optional. It looks like this: + + authentication.token_check(f"{globalvar.GV_base_url}/?message=forbidden") + +`` 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") + +`` is the optional one. It is for safely giving return. + +When you want to use a template, we can use `` 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. + +``, ``, and `` 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() diff --git a/pages/content/modules.md b/pages/content/modules.md new file mode 100644 index 0000000..ad88f6d --- /dev/null +++ b/pages/content/modules.md @@ -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 diff --git a/pages/core/loggorilla.md b/pages/core/loggorilla.md new file mode 100644 index 0000000..82e932f --- /dev/null +++ b/pages/core/loggorilla.md @@ -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" ) diff --git a/pages/getting-starter.md b/pages/getting-starter.md new file mode 100644 index 0000000..285cd0c --- /dev/null +++ b/pages/getting-starter.md @@ -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 costa.py + +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 & diff --git a/pages/main-process.md b/pages/main-process.md new file mode 100644 index 0000000..28c4dfa --- /dev/null +++ b/pages/main-process.md @@ -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" diff --git a/pages/structure.md b/pages/structure.md new file mode 100644 index 0000000..808cdc4 --- /dev/null +++ b/pages/structure.md @@ -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.