diff --git a/README.md b/README.md new file mode 100644 index 0000000..4447057 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# NANO : A Nano Web Framework + +Nano is an Express inspired nano web framework which is under development. Currently, it has features of adding routes, controllers and sending API responses (Refer the [example directory](example/) for reference). Defining models with custom field types is also available but is under development. + +**Status** : Under developement +
+ +### To run example app +```bash +$ pip3 install -r requirements.txt + +$ gunicorn main:api +``` + +## Documentation +Refer the example app. Working on a better documentation diff --git a/example/controllers.py b/example/controllers.py new file mode 100644 index 0000000..2f0a4ed --- /dev/null +++ b/example/controllers.py @@ -0,0 +1,14 @@ +from nano.handler import Response + +def index(req): + return Response(status=200, data={"message": "Welocome to your first Nano app"}) + +def create_user(req): + print(req.data) + return Response(status=201, data=req.data) + +def get_user(req): + return Response(status=200, data={"user": "User found"}) + +def delete_user(req): + return Response(status=200, data={"message": "Deleted user"}) diff --git a/example/models.py b/example/models.py new file mode 100644 index 0000000..cddbee6 --- /dev/null +++ b/example/models.py @@ -0,0 +1,6 @@ +from nano.db import model + +class User(model.Model): + username = model.StringType('username', max_length=50, min_length=4, unique=True, required=True) + ph_no = model.IntegerType('ph_no', max=9999999999, min=9000000000) + is_admin = model.BooleanType('is_admin', default=False) diff --git a/example/routes.py b/example/routes.py new file mode 100644 index 0000000..a55f347 --- /dev/null +++ b/example/routes.py @@ -0,0 +1,7 @@ +from main import api +from .controllers import index, create_user, get_user, delete_user + +api.router.get("/", index) +api.router.get("/user", get_user) +api.router.post("/user/create", create_user) +api.router.get("/user/delete", delete_user) diff --git a/main.py b/main.py new file mode 100644 index 0000000..c672bdb --- /dev/null +++ b/main.py @@ -0,0 +1,5 @@ +from nano.app import Nano + +api = Nano() + +import example.routes diff --git a/nano/app.py b/nano/app.py new file mode 100644 index 0000000..8eeec5b --- /dev/null +++ b/nano/app.py @@ -0,0 +1,37 @@ +from nano.utils import msg, warn, err +from nano.handler import Router, Request, Response +import os + +class Nano: + def __init__(self): + print(msg("RUN", "")) + self.router = Router() + self.APP_DIR = os.path.abspath(os.getcwd()) + + def __call__(self, environ, start_response): + response = self.request_handler(environ) + start_response(response.status_msg, response.headers) + return [bytes(response.data, 'utf-8')] + + def __del__(self): + print(msg("END", "")) + + def request_handler(self, request_environ): + request = Request(request_environ) + response = Response() + found_url = False + print(request) + for i in range(len(self.router.urls)): + if request.path == self.router.urls[i] and request.method == self.router.methods[i]: + found_url = True + response = self.router.controllers[i](request) + if not found_url: + response = Response(status=404) + print(err("REQUEST", f"URL '{request.path}' not found in server router")) + if not isinstance(response, Response): + response = Response(status=500) + print(err("RESPONSE", "The controller must return a Response object")) + print(response) + raise TypeError(warn("The controller must return a HTTP Response object")) + print(response) + return response diff --git a/nano/core/__init__.py b/nano/core/__init__.py new file mode 100644 index 0000000..1c68039 --- /dev/null +++ b/nano/core/__init__.py @@ -0,0 +1 @@ +from .errors import ResponseTypeError, DBError diff --git a/nano/core/errors.py b/nano/core/errors.py new file mode 100644 index 0000000..98a2614 --- /dev/null +++ b/nano/core/errors.py @@ -0,0 +1,8 @@ +class _BaseError(Exception): + pass + +class ResponseTypeError(_BaseError): + pass + +class DBError(_BaseError): + pass diff --git a/nano/db/__init__.py b/nano/db/__init__.py new file mode 100644 index 0000000..8d77dbe --- /dev/null +++ b/nano/db/__init__.py @@ -0,0 +1 @@ +from .db import DB \ No newline at end of file diff --git a/nano/db/db.py b/nano/db/db.py new file mode 100644 index 0000000..a1427d4 --- /dev/null +++ b/nano/db/db.py @@ -0,0 +1,35 @@ +import sqlite3 +from nano.utils import err + +class DB: + def __init__(self, models): + self.server = sqlite3.connect('database.db') + self.admin = self.server.cursor() + self.models = models + for model in self.models: + model.table_name = model.__name__.lower() + + def is_booted(self): + is_booted = False + for model in self.models: + self.admin.execute( f"SELECT count(name) FROM sqlite_master WHERE type='table' AND name='{model.__name__.lower()}'") + if self.admin.fetchone()[0]: + is_booted = True + return is_booted + + def boot(self): + fields = "id INT PRIMARY KEY NOT NULL" + for model in self.models: + for field in model.get_fields(): + fields = fields + ", " + field + try: + self.admin.execute( + f"CREATE TABLE {model.__name__.lower()} ({fields})") + except sqlite3.OperationalError: + print(err("MODEL", f"Model {model.__name__} already booted")) + fields = "id INT PRIMARY KEY NOT NULL" + self.server.commit() + + def __del__(self): + self.admin.close() + self.server.close() \ No newline at end of file diff --git a/nano/db/model.py b/nano/db/model.py new file mode 100644 index 0000000..4019d1d --- /dev/null +++ b/nano/db/model.py @@ -0,0 +1,49 @@ +import sqlite3 +from .prop_types import IntegerType, StringType, BooleanType + +class Model: + + table_name = str() + fields = list() + db_server = sqlite3.connect('database.db') + db_admin = db_server.cursor() + id = str() + + def db_handler(self, command): + self.db_admin.execute(command) + self.db_admin.commit() + return self.db_admin.fetchone() + + def save(self): + return self.db_handler(f"UPDATE {self.table_name} SET {self} WHERE id={self.id}") + + @classmethod + def create(self, data): + return self.db_handler(f"INSERT INTO {self.table_name} VALUES ({data})") + + @classmethod + def find(self, id): + return self.db_handler(f"SELECT * FROM {self.table_name} WHERE id={id}") + + @classmethod + def update(self, id, data): + return self.db_handler(f"UPDATE {self.table_name} SET {data} WHERE id={id}") + + @classmethod + def delete(self, id): + self.db_handler(f"DELETE FROM {self.table_name} WHERE id={id}") + + def test(self): + print(self.table_name) + + @classmethod + def get_fields(self): + for field in self.__dict__: + if ((field.startswith("__") and field.endswith("__")) + or str(self.__dict__[field]).startswith(" {min})" + self.default = f"DEFAULT {default if default else 'NULL'} " + self.command = self.base_cmd + self.default + self.check + + +class StringType(_BaseType): + def __init__(self, name="", max_length=None, min_length=0, unique=False, required=False, default=None): + super().__init__('TEXT', name, unique, required, default) + self.default = f"DEFAULT {default if default else 'NULL'} " + self.check = f"CHECK (LEN({self.name}) < {max_length} AND LEN({self.name}) > {min_length})" + self.command = self.base_cmd + self.check + + +class BooleanType(_BaseType): + def __init__(self, name="", unique=False, required=False, default=None): + super().__init__('INTEGER', name, unique, required, default) + self.default = f"DEFAULT {1 if default else 0} " + self.command = self.base_cmd + self.default + diff --git a/nano/handler/__init__.py b/nano/handler/__init__.py new file mode 100644 index 0000000..821573b --- /dev/null +++ b/nano/handler/__init__.py @@ -0,0 +1,3 @@ +from .router import Router +from .request import Request +from .response import Response diff --git a/nano/handler/request.py b/nano/handler/request.py new file mode 100644 index 0000000..524d942 --- /dev/null +++ b/nano/handler/request.py @@ -0,0 +1,12 @@ +from nano.utils import log + +class Request: + + def __init__(self, environ): + self.path = environ['PATH_INFO'] + self.method = environ['REQUEST_METHOD'] + self.content_length = int(environ.get('CONTENT_LENGTH', 0)) + self.data = environ['wsgi.input'].read(self.content_length).decode('utf-8') + + def __str__(self): + return log("REQ", self.method, self.path) diff --git a/nano/handler/response.py b/nano/handler/response.py new file mode 100644 index 0000000..78e0660 --- /dev/null +++ b/nano/handler/response.py @@ -0,0 +1,21 @@ +from nano.utils import log +from .status import get_status_text + +class Response: + + def __init__(self, status=200, data={}): + self.status_code = int(status) + self.status_text = str(get_status_text[self.status_code]) + self.status_msg = str(self.status_code) + " " + self.status_text + self.data = str(data) + self.headers = [ + ('Content-Type', 'text/plain'), + ('Content-Length', str(len(self.data))) + ] + + def __str__(self): + return log("RES", self.status_code, self.status_text) + +if __name__ == '__main__': + test = Response(status=200) + print(test) diff --git a/nano/handler/router.py b/nano/handler/router.py new file mode 100644 index 0000000..a7192e2 --- /dev/null +++ b/nano/handler/router.py @@ -0,0 +1,16 @@ +class Router: + + def __init__(self): + self.urls = [] + self.controllers = [] + self.methods = [] + + def get(self, url: str, controller): + self.urls.append(url) + self.controllers.append(controller) + self.methods.append('GET') + + def post(self, url: str, controller): + self.urls.append(url) + self.controllers.append(controller) + self.methods.append('POST') \ No newline at end of file diff --git a/nano/handler/status.py b/nano/handler/status.py new file mode 100644 index 0000000..2c7c3ba --- /dev/null +++ b/nano/handler/status.py @@ -0,0 +1,82 @@ +get_status_text = { + 100: "CONTINUE", + 101: "SWITCHING PROTOCOLS", + 200: "OK", + 201: "CREATED", + 202: "ACCEPTED", + 203: "NON AUTHORITATIVE INFORMATION", + 204: "NO CONTENT", + 205: "RESET CONTENT", + 206: "PARTIAL CONTENT", + 207: "MULTI STATUS", + 208: "ALREADY REPORTED", + 226: "IM USED", + 300: "MULTIPLE CHOICES", + 301: "MOVED PERMANENTLY", + 302: "FOUND", + 303: "SEE OTHER", + 304: "NOT MODIFIED", + 305: "USE PROXY", + 306: "RESERVED", + 307: "TEMPORARY REDIRECT", + 308: "PERMANENT REDIRECT", + 400: "BAD REQUEST", + 401: "UNAUTHORIZED", + 402: "PAYMENT REQUIRED", + 403: "FORBIDDEN", + 404: "NOT FOUND", + 405: "METHOD NOT ALLOWED", + 406: "NOT ACCEPTABLE", + 407: "PROXY AUTHENTICATION REQUIRED", + 408: "REQUEST TIMEOUT", + 409: "CONFLICT", + 410: "GONE", + 411: "LENGTH REQUIRED", + 412: "PRECONDITION FAILED", + 413: "REQUEST ENTITY TOO LARGE", + 414: "REQUEST URI TOO LONG", + 415: "UNSUPPORTED MEDIA TYPE", + 416: "REQUESTED RANGE NOT SATISFIABLE", + 417: "EXPECTATION FAILED", + 418: "IM A TEAPOT", + 422: "UNPROCESSABLE ENTITY", + 423: "LOCKED", + 424: "FAILED DEPENDENCY", + 426: "UPGRADE REQUIRED", + 428: "PRECONDITION REQUIRED", + 429: "TOO MANY REQUESTS", + 431: "REQUEST HEADER FIELDS TOO LARGE", + 451: "UNAVAILABLE FOR LEGAL REASONS", + 500: "INTERNAL SERVER ERROR", + 501: "NOT IMPLEMENTED", + 502: "BAD GATEWAY", + 503: "SERVICE UNAVAILABLE", + 504: "GATEWAY TIMEOUT", + 505: "ION NOT SUPPORTED", + 506: "VARIANT ALSO NEGOTIATES", + 507: "INSUFFICIENT STORAGE", + 508: "LOOP DETECTED", + 509: "BANDWIDTH LIMIT EXCEEDED", + 510: "NOT EXTENDED", + 511: "NETWORK AUTHENTICATION REQUIRED" +} + + +def is_informational(code): + return 100 <= code <= 199 + + +def is_success(code): + return 200 <= code <= 299 + + +def is_redirect(code): + return 300 <= code <= 399 + + +def is_client_error(code): + return 400 <= code <= 499 + + +def is_server_error(code): + return 500 <= code <= 599 \ No newline at end of file diff --git a/nano/utils/__init__.py b/nano/utils/__init__.py new file mode 100644 index 0000000..3a42694 --- /dev/null +++ b/nano/utils/__init__.py @@ -0,0 +1 @@ +from .logger import log, msg, warn, err \ No newline at end of file diff --git a/nano/utils/datastructures.py b/nano/utils/datastructures.py new file mode 100644 index 0000000..e69de29 diff --git a/nano/utils/logger.py b/nano/utils/logger.py new file mode 100644 index 0000000..9ba87f8 --- /dev/null +++ b/nano/utils/logger.py @@ -0,0 +1,21 @@ +from colorama import Fore, Style + +def err(field, err): + return Fore.RED + Style.BRIGHT + "[ERR] " + Style.RESET_ALL + Fore.YELLOW + f"{field}: " + Style.RESET_ALL + f"{err}" + +def warn(msg): + return Fore.YELLOW + msg + Style.RESET_ALL + +def msg(field, msg): + return Fore.YELLOW + Style.BRIGHT + f"[{field}] " + Style.RESET_ALL + f"{msg}" + +def log(field, head, body): + clr = Fore.GREEN if field == 'REQ' else Fore.BLUE + return clr + Style.BRIGHT + f"[{field}] " + Style.RESET_ALL + Fore.YELLOW + f"{head}: " + Style.RESET_ALL + body + +if __name__ == '__main__': + print(warn("Testing logging system")) + print(log("REQ", "POST", "/test/url")) + print(log("RES", 200, "OK")) + print(err("TEST", "Testing logging system")) + print(msg("Testing logging system")) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5408c38 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +colorama==0.4.4 +gunicorn==20.0.4