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