Skip to content

Commit

Permalink
v0.1
Browse files Browse the repository at this point in the history
  • Loading branch information
nan-dan-unni committed Jan 31, 2021
1 parent aff956f commit 0c806b7
Show file tree
Hide file tree
Showing 21 changed files with 367 additions and 0 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
<br />

### To run example app
```bash
$ pip3 install -r requirements.txt

$ gunicorn main:api
```

## Documentation
Refer the example app. Working on a better documentation
14 changes: 14 additions & 0 deletions example/controllers.py
Original file line number Diff line number Diff line change
@@ -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"})
6 changes: 6 additions & 0 deletions example/models.py
Original file line number Diff line number Diff line change
@@ -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)
7 changes: 7 additions & 0 deletions example/routes.py
Original file line number Diff line number Diff line change
@@ -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)
5 changes: 5 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from nano.app import Nano

api = Nano()

import example.routes
37 changes: 37 additions & 0 deletions nano/app.py
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions nano/core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .errors import ResponseTypeError, DBError
8 changes: 8 additions & 0 deletions nano/core/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class _BaseError(Exception):
pass

class ResponseTypeError(_BaseError):
pass

class DBError(_BaseError):
pass
1 change: 1 addition & 0 deletions nano/db/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .db import DB
35 changes: 35 additions & 0 deletions nano/db/db.py
Original file line number Diff line number Diff line change
@@ -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()
49 changes: 49 additions & 0 deletions nano/db/model.py
Original file line number Diff line number Diff line change
@@ -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("<classmethod")
or str(self.__dict__[field]).startswith("<function")):
pass
else:
self.fields.append(self.__dict__[field].command)
print(self.fields)

30 changes: 30 additions & 0 deletions nano/db/prop_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
class _BaseType():
def __init__(self, model_type, name, unique, required, default):
self.name = str(name).lower()
self.unique = 'UNIQUE ' if unique else ''
self.required = 'NOT NULL ' if required else '' # Default : False
self.base_cmd = f"{self.name} {model_type} {self.required}{self.unique}"


class IntegerType(_BaseType):
def __init__(self, name="", max=10000000, min=-10000000, unique=False, required=False, default=None):
super().__init__('INTEGER', name, unique, required, default)
self.check = f"CHECK ({self.name} < {max} AND {self.name} > {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

3 changes: 3 additions & 0 deletions nano/handler/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .router import Router
from .request import Request
from .response import Response
12 changes: 12 additions & 0 deletions nano/handler/request.py
Original file line number Diff line number Diff line change
@@ -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)
21 changes: 21 additions & 0 deletions nano/handler/response.py
Original file line number Diff line number Diff line change
@@ -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)
16 changes: 16 additions & 0 deletions nano/handler/router.py
Original file line number Diff line number Diff line change
@@ -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')
82 changes: 82 additions & 0 deletions nano/handler/status.py
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions nano/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .logger import log, msg, warn, err
Empty file added nano/utils/datastructures.py
Empty file.
21 changes: 21 additions & 0 deletions nano/utils/logger.py
Original file line number Diff line number Diff line change
@@ -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"))
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
colorama==0.4.4
gunicorn==20.0.4

0 comments on commit 0c806b7

Please sign in to comment.