├── LumosWeb
├── __init__.py
├── static
│ └── styles.css
├── response.py
├── middleware.py
├── cli.py
├── api.py
└── orm.py
├── test.db
├── .gitignore
├── static
└── main.css
├── templates
├── index.html
├── error.html
└── index.md
├── .devcontainer
├── Dockerfile
├── docker-compose.yml
└── devcontainer.json
├── conftest.py
├── setup.py
├── app.py
├── test_orm.py
├── README.md
└── test_lumos.py
/LumosWeb/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sddilora/LumosWeb/HEAD/test.db
--------------------------------------------------------------------------------
/LumosWeb/static/styles.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sddilora/LumosWeb/HEAD/LumosWeb/static/styles.css
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/lumos_venv
2 | Notes.md
3 |
4 | .vscode
5 |
6 | **/__pycache__/
7 | **/.pytest_cache/
8 | **/.coverage
9 | **/.coveragerc
10 |
11 | LumosWeb.egg-info/
12 | build/
13 | dist/
--------------------------------------------------------------------------------
/static/main.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: Arial, sans-serif;
3 | background-color: #f2f2f2;
4 | margin: 0;
5 | padding: 0;
6 | }
7 |
8 | .container {
9 | max-width: 600px;
10 | margin: 0 auto;
11 | padding: 20px;
12 | background-color: #fff;
13 | border-radius: 8px;
14 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
15 | }
16 |
17 | h1 {
18 | color: #20786f;
19 | text-align: center;
20 | }
21 |
22 | p {
23 | color: #666;
24 | text-align: center;
25 | }
26 |
--------------------------------------------------------------------------------
/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ title }}
5 |
6 |
7 |
8 |
9 |
10 |
Welcome to {{ name }} !
11 |
This is the example page of LumosWeb, a fantastic web framework.
12 |
You can create your templates in templates/ directory.
13 |
Like This one :)
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.devcontainer/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/devcontainers/python:0-3.11
2 |
3 | ENV PYTHONUNBUFFERED 1
4 |
5 | # [Optional] If your requirements rarely change, uncomment this section to add them to the image.
6 | # COPY requirements.txt /tmp/pip-tmp/
7 | # RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \
8 | # && rm -rf /tmp/pip-tmp
9 |
10 | # [Optional] Uncomment this section to install additional OS packages.
11 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
12 | # && apt-get -y install --no-install-recommends
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import os
3 |
4 | from LumosWeb.api import API
5 | from LumosWeb.orm import Database, Table, Column, ForeignKey
6 |
7 | @pytest.fixture
8 | def api():
9 | return API()
10 |
11 |
12 | @pytest.fixture
13 | def client(api):
14 | return api.test_session()
15 |
16 | @pytest.fixture
17 | def db():
18 | DB_PATH = "./test.db"
19 | if os.path.exists(DB_PATH):
20 | os.remove(DB_PATH)
21 | db = Database(DB_PATH)
22 | return db
23 |
24 |
25 | @pytest.fixture
26 | def Author():
27 | class Author(Table):
28 | name = Column(str)
29 | age = Column(int)
30 |
31 | return Author
32 |
33 |
34 | @pytest.fixture
35 | def Book(Author):
36 | class Book(Table):
37 | title = Column(str)
38 | published = Column(bool)
39 | author = ForeignKey(Author)
40 |
41 | return Book
--------------------------------------------------------------------------------
/templates/error.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{title}}
5 |
45 |
46 |
47 |
48 |
Hey, who turned off the lights? :(
49 |
We are encountering an error:
50 |
{{name}}
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/LumosWeb/response.py:
--------------------------------------------------------------------------------
1 | import json
2 | from webob import Response as WebObResponse
3 |
4 | class Response:
5 | def __init__(self):
6 | self.text = None
7 | self.json = None
8 | self.html = None
9 | self.status_code = 200
10 | self.body = b''
11 | self.content_type = None
12 |
13 | def __call__(self, environ, start_response):
14 | self.set_body_and_content_type()
15 |
16 | response = WebObResponse(
17 | body = self.body, content_type=self.content_type, status=self.status_code
18 | )
19 | return response(environ, start_response)
20 |
21 | def set_body_and_content_type(self):
22 | if self.json is not None:
23 | self.body = json.dumps(self.json).encode("utf-8")
24 | self.content_type = "application/json"
25 |
26 | if self.text is not None:
27 | self.body = self.text
28 | self.content_type = "text/plain"
29 |
30 | if self.html is not None:
31 | self.body = self.html.encode()
32 | self.content_type = "text/html"
33 |
--------------------------------------------------------------------------------
/LumosWeb/middleware.py:
--------------------------------------------------------------------------------
1 | from webob import Request
2 | class Middleware:
3 | def __init__(self, app):
4 | self.app = app
5 |
6 | # Since middlewares are the first entrypoint to the app, they are now called by a web server
7 | # instead of the app itself. So, we need to add a __call__ method to the Middleware class.
8 | def __call__(self, environ, start_response):
9 | req = Request(environ)
10 | resp = self.handle_request(req)
11 | return resp(environ, start_response)
12 |
13 | # we wrapped the given middleware class around the current app.
14 | def add(self, middleware_cls):
15 | self.app = middleware_cls(self.app)
16 |
17 | # we added a process_request and process_response method to the Middleware class.
18 | def process_request(self, req):
19 | pass
20 |
21 | def process_response(self, req, resp):
22 | pass
23 |
24 | # The method that handles incoming requests
25 | def handle_request(self, req):
26 | self.process_request(req)
27 | resp = self.app.handle_request(req)
28 | self.process_response(req, resp)
29 | return resp
--------------------------------------------------------------------------------
/.devcontainer/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 | app:
5 | build:
6 | context: ..
7 | dockerfile: .devcontainer/Dockerfile
8 |
9 | volumes:
10 | - ../..:/workspaces:cached
11 |
12 | # Overrides default command so things don't shut down after the process ends.
13 | command: sleep infinity
14 |
15 | # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
16 | network_mode: service:db
17 |
18 | # Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
19 | # (Adding the "ports" property to this file will not forward from a Codespace.)
20 |
21 | db:
22 | image: postgres:latest
23 | restart: unless-stopped
24 | volumes:
25 | - postgres-data:/var/lib/postgresql/data
26 | environment:
27 | POSTGRES_USER: postgres
28 | POSTGRES_DB: postgres
29 | POSTGRES_PASSWORD: postgres
30 |
31 | # Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally.
32 | # (Adding the "ports" property to this file will not forward from a Codespace.)
33 |
34 | volumes:
35 | postgres-data:
36 |
--------------------------------------------------------------------------------
/LumosWeb/cli.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import os
3 | from LumosWeb.api import API # Import the API class
4 |
5 |
6 | def main():
7 | if len(sys.argv) < 4 or sys.argv[1] != "--app":
8 | print("Usage: Lumosweb --app run")
9 | return
10 |
11 | app_module = sys.argv[2]
12 | app_path = os.path.abspath(os.path.join(os.getcwd(), app_module + ".py"))
13 | app_directory = os.path.dirname(app_path)
14 |
15 | if os.path.exists(app_path):
16 | sys.path.append(app_directory) # Add app directory to the system path
17 |
18 | with open(app_path, "r") as file:
19 | code = compile(file.read(), app_path, "exec")
20 | namespace = {}
21 | exec(code, namespace)
22 | app = None
23 | for obj in namespace.values():
24 | if isinstance(obj, API):
25 | app = obj
26 | break
27 | if app is not None:
28 | app.run()
29 | else:
30 | raise AttributeError(f"No instance of 'API' found in module: {app_module}")
31 | else:
32 | raise ImportError(f"Failed to import app module: {app_module}")
33 |
34 | if __name__ == "__main__":
35 | main()
36 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the
2 | // README at: https://github.com/devcontainers/templates/tree/main/src/postgres
3 | {
4 | "name": "Python 3 & PostgreSQL",
5 | "dockerComposeFile": "docker-compose.yml",
6 | "service": "app",
7 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
8 | "customizations": {
9 | "vscode": {
10 | "extensions": [
11 | "eamodio.gitlens",
12 | "esbenp.prettier-vscode"
13 | ]
14 | }
15 | }
16 |
17 | // Features to add to the dev container. More info: https://containers.dev/features.
18 | // "features": {},
19 |
20 | // Use 'forwardPorts' to make a list of ports inside the container available locally.
21 | // This can be used to network with other containers or the host.
22 | // "forwardPorts": [5000, 5432],
23 |
24 | // Use 'postCreateCommand' to run commands after the container is created.
25 | // "postCreateCommand": "pip install --user -r requirements.txt",
26 |
27 | // Configure tool-specific properties.
28 | // "customizations": {},
29 |
30 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
31 | // "remoteUser": "root"
32 | }
33 |
--------------------------------------------------------------------------------
/templates/index.md:
--------------------------------------------------------------------------------
1 | # Example Requests
2 |
3 | ```http
4 | POST /topics HTTP/1.1
5 | Host: localhost:8080
6 | Content-Type: application/json
7 | ```
8 |
9 | | Parameter | Type | Value |
10 | | :--------: | :-------: | :-------------------------: |
11 | | `title` | `string` | "Example Topic" |
12 | | `title` | `string` | "Example Topic2" |
13 |
14 | ```go
15 | package main
16 | import "fmt"
17 | func main() {
18 | fmt.Println("hello world")
19 | }
20 | ```
21 |
22 | ```json
23 | {
24 | "title": "Example Topic"
25 | }
26 | ```
27 |
28 | ```python
29 | print("Hello World")
30 | a = 4
31 | ```
32 |
33 | Left Header | Center Header | Right Header
34 | :----------- | :-------------: | ------------:
35 | Conent Cell | Content Cell | Content Cell
36 |
37 | ```python
38 | print('hellow world')
39 | ```
40 |
41 | ```js
42 | console.log('hello world')
43 | var str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
44 | ```
45 |
46 | ~~~
47 | a one-line code block
48 | ~~~
49 |
50 | [gooogle](www.google.com)
51 |
52 | > Lorem ipsum dolor sit amet
53 |
54 | * A list item.
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | import io
5 | import os
6 |
7 | from setuptools import find_packages, setup
8 |
9 | # Package meta-data.
10 | NAME = "LumosWeb"
11 | DESCRIPTION = "LumosWeb is web framework, simple and effective usage"
12 | EMAIL = "sumeyyedilaradogan@gmail.com"
13 | AUTHOR = "Sddilora"
14 | REQUIRES_PYTHON = ">=3.6.0"
15 | VERSION = "1.2.0"
16 |
17 | # Which packages are required for this module to be executed?
18 | REQUIRED = [
19 | "Jinja2==3.1.2",
20 | "parse==1.19.0",
21 | "requests==2.31.0",
22 | "requests-wsgi-adapter==0.4.1",
23 | "WebOb==1.8.7",
24 | "whitenoise==6.4.0",
25 | "Markdown==3.4.3",
26 | "Pygments==2.15.1"
27 | ]
28 |
29 | here = os.path.abspath(os.path.dirname(__file__))
30 |
31 | # Import the README and use it as the long-description.
32 | # Note: this will only work if 'README.md' is present in your MANIFEST.in file!
33 | try:
34 | with io.open(os.path.join(here, "README.md"), encoding="utf-8") as f:
35 | long_description = "\n" + f.read()
36 | except FileNotFoundError:
37 | long_description = DESCRIPTION
38 |
39 | # Load the package's __version__.py module as a dictionary.
40 | about = {}
41 | if not VERSION:
42 | project_slug = NAME.lower().replace("-", "_").replace(" ", "_")
43 | with open(os.path.join(here, project_slug, "__version__.py")) as f:
44 | exec(f.read(), about)
45 | else:
46 | about["__version__"] = VERSION
47 |
48 |
49 | # Where the magic happens:
50 | setup(
51 | name=NAME,
52 | version=about["__version__"],
53 | description=DESCRIPTION,
54 | long_description=long_description,
55 | long_description_content_type="text/markdown",
56 | author=AUTHOR,
57 | author_email=EMAIL,
58 | python_requires=REQUIRES_PYTHON,
59 | packages=find_packages(exclude=["test_*"]),
60 | install_requires=REQUIRED,
61 | include_package_data=True,
62 | license="MIT",
63 | classifiers=[
64 | "Programming Language :: Python :: 3.6",
65 | ],
66 | setup_requires=["wheel"],
67 | entry_points={
68 | 'console_scripts': [
69 | 'Lumosweb = LumosWeb.cli:main',
70 | ]},
71 | )
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 | from LumosWeb.api import API
2 | from LumosWeb.middleware import Middleware
3 |
4 | app = API()
5 |
6 | @app.route("/", allowed_methods=["GET", "post"])
7 | def about(request, response):
8 | response.text = "Lights are on!"
9 |
10 | @app.route("/home", allowed_methods=["get"])
11 | def home(request, response):
12 | if request.method == "get" or "GET": ### This is a bug, as we are not checking for the request method TODO: Fix this
13 | response.text = "Hello from the HOME page"
14 | else:
15 | raise AttributeError("Method not allowed.",request.method)
16 |
17 | @app.route("/hello/{name}", allowed_methods=["get", "post"])
18 | def greeting(request, response, name):
19 | response.text = f"What are you doing here, {name}"
20 |
21 | @app.route("/book/{title}/page/{page:d}", allowed_methods=["get", "post"])
22 | def book(request, response, title, page):
23 | response.text = f"You are reading the Book: {title}, and you were on Page: {page}"
24 |
25 | @app.route("/sum/{num_1:d}/{num_2:d}", allowed_methods=["get", "post"])
26 | def sum(request, response, num_1, num_2):
27 | total = int(num_1) + int(num_2)
28 | response.text = f"{num_1} + {num_2} = {total}"
29 |
30 | @app.route("/book", allowed_methods=["get", "post"])
31 | class BooksResource:
32 | def get(self, req, resp):
33 | resp.text = "Books Page"
34 |
35 | def post(self, req, resp):
36 | resp.text = "Endpoint to create a book"
37 |
38 | # @app.route("/template")
39 | # def template_handler(req, resp):
40 | # resp.body = app.template("index.html", context={"name": "LumosWeb", "title":"Lights are on!"}).encode() # Now we dont need to encode the body, as we are doing it in the Response class
41 |
42 | ## Adding a route without a decorator
43 | def handler(req, resp):
44 | resp.text = "We don't have to use decorators!"
45 |
46 | app.add_route("/sample", handler, allowed_methods=["get", "post"])
47 |
48 | # To handle exceptions
49 | def custom_exception_handler(request, response, exception_cls):
50 | response.body = app.template("error.html", context={"name": exception_cls, "title":"Lights cannot be turned on!"}).encode()
51 |
52 |
53 | app.add_exception_handler(custom_exception_handler)
54 |
55 | @app.route("/exception", allowed_methods=["get", "post"])
56 | def exception_throwing_handler(request, response):
57 | raise AssertionError("Sorry, This handler should not be used")
58 |
59 | # custom middleware
60 | class SimpleCustomMiddleware(Middleware):
61 | def process_request(self, req):
62 | print("Processing request", req.url)
63 |
64 | def process_response(self, req, resp):
65 | print("Processing response", req.url)
66 |
67 | app.add_middleware(SimpleCustomMiddleware)
68 |
69 | @app.route("/template", allowed_methods=["get", "post"])
70 | def template_handler(req, resp):
71 | resp.html = app.template("index.html", context={"name": "LumosWeb", "title":"Lights are on!"})
72 |
73 | @app.route("/json", allowed_methods=["get", "post"])
74 | def json_handler(req, resp):
75 | resp.json = {"name": "LumosData", "type":"JSON"}
76 |
77 | @app.route("/text", allowed_methods=["get", "post"])
78 | def text_handler(req, resp):
79 | resp.text = "This is a plain text"
80 |
81 | @app.route("/serve/md-files", allowed_methods=["get", "post"])
82 | def about(request, response):
83 | response.html = app.template("index.md")
84 |
85 | app.run()
--------------------------------------------------------------------------------
/test_orm.py:
--------------------------------------------------------------------------------
1 | import sqlite3
2 |
3 | import pytest
4 |
5 | def test_create_db(db):
6 | assert isinstance(db.conn, sqlite3.Connection)
7 | assert db.tables == []
8 |
9 | def test_define_tables(Author, Book):
10 | assert Author.name.type == str
11 | assert Book.author.table == Author
12 |
13 | assert Author.name.sql_type == "TEXT"
14 | assert Author.age.sql_type == "INTEGER"
15 |
16 | def test_create_tables(db, Author, Book):
17 | db.create(Author)
18 | db.create(Book)
19 |
20 | assert Author._get_create_sql() == "CREATE TABLE IF NOT EXISTS author (id INTEGER PRIMARY KEY AUTOINCREMENT, age INTEGER, name TEXT);"
21 | assert Book._get_create_sql() == "CREATE TABLE IF NOT EXISTS book (id INTEGER PRIMARY KEY AUTOINCREMENT, author_id INTEGER, published INTEGER, title TEXT);"
22 |
23 | for table in ("author", "book"):
24 | assert table in db.tables
25 |
26 | def test_create_author_instance(db, Author):
27 | db.create(Author)
28 | author = Author(name="J. K. Rowling", age=54)
29 | assert author.name == "J. K. Rowling"
30 | assert author.age == 54
31 | assert author.id is None
32 |
33 | def test_save_author_instance(db, Author):
34 | db.create(Author)
35 |
36 | rowling = Author(name="J. K. Rowling", age=54) # instance of Author class
37 | db.save(rowling)
38 |
39 | assert rowling._get_insert_sql() == (
40 | "INSERT INTO author (age, name) VALUES (?, ?);",
41 | [54, "J. K. Rowling"]
42 | )
43 | assert rowling.id == 1
44 |
45 | man = Author(name="Man Harsh", age=20)
46 | db.save(man)
47 | assert man.id == 2
48 |
49 | vik = Author(name="Vik Star", age=43)
50 | db.save(vik)
51 | assert vik.id == 3
52 |
53 | jack = Author(name="Jack Sparrow", age=34)
54 | db.save(jack)
55 | assert jack.id == 4
56 |
57 | def test_query_all_authors(db, Author):
58 | db.create(Author)
59 | rowling = Author(name="J. K. Rowling", age=54)
60 | vik = Author(name="Vik Star", age=43)
61 | db.save(rowling)
62 | db.save(vik)
63 |
64 | authors = db.all(Author)
65 |
66 | assert Author._get_select_sql() == (
67 | "SELECT id, age, name FROM author;",
68 | ["id", "age", "name"]
69 | )
70 | assert len(authors) == 2
71 | assert type(authors[0]) == Author
72 | assert {a.age for a in authors} == {54, 43}
73 | assert {a.name for a in authors} == {"J. K. Rowling", "Vik Star"}
74 |
75 | def test_get_author(db, Author):
76 | db.create(Author)
77 |
78 | novel = Author(name="J. K. Rowling", age=54)
79 | db.save(novel)
80 |
81 | rowling_from_db = db.get(Author, id=1)
82 |
83 | assert Author._get_select_where_sql(id=1) == (
84 | "SELECT id, age, name FROM author WHERE id = ?;",
85 | ["id", "age", "name"],
86 | [1]
87 | )
88 | assert rowling_from_db.name == "J. K. Rowling"
89 | assert rowling_from_db.age == 54
90 | assert rowling_from_db.id == 1
91 | assert type(rowling_from_db) == Author
92 |
93 | def test_get_book(db, Author, Book):
94 | db.create(Author)
95 | db.create(Book)
96 |
97 | rowling = Author(name="J. K. Rowling", age=54)
98 | db.save(rowling)
99 |
100 | harry_potter = Book(title="Harry Potter", published=True, author=rowling)
101 | db.save(harry_potter)
102 |
103 | book_from_db = db.get(Book, 1)
104 |
105 | assert book_from_db.title == "Harry Potter"
106 | assert book_from_db.published == True
107 | assert book_from_db.author.name == "J. K. Rowling"
108 | assert book_from_db.author.age == 54
109 | assert book_from_db.author.id == 1
110 | assert type(book_from_db) == Book
111 |
112 | def test_query_all_books(db, Author, Book):
113 | db.create(Author)
114 | db.create(Book)
115 |
116 | rowling = Author(name="J. K. Rowling", age=54)
117 | db.save(rowling)
118 |
119 | harry_potter = Book(title="Harry Potter", published=True, author=rowling)
120 | db.save(harry_potter)
121 |
122 | books = db.all(Book)
123 |
124 | assert len(books) == 1
125 | assert books[0].title == "Harry Potter"
126 | assert books[0].published == True
127 | assert books[0].author.name == "J. K. Rowling"
128 | assert books[0].author.age == 54
129 | assert books[0].author.id == 1
130 | assert type(books[0]) == Book
131 |
132 | def test_update_author(db, Author):
133 | db.create(Author)
134 |
135 | rowling = Author(name="J. K. Rowling", age=54)
136 | db.save(rowling)
137 |
138 | rowling.name = "J. K. Rowling (Joanne)"
139 | rowling.age = 55
140 | db.update(rowling)
141 |
142 | rowling_from_db = db.get(Author, id = rowling.id)
143 |
144 | assert rowling_from_db.name == "J. K. Rowling (Joanne)"
145 | assert rowling_from_db.age == 55
146 |
147 | def test_delete_author(db, Author):
148 | db.create(Author)
149 |
150 | rowling = Author(name="J. K. Rowling", age=54)
151 | db.save(rowling)
152 |
153 | db.delete(Author, id=1)
154 |
155 | with pytest.raises(Exception):
156 | db.get(Author, 1)
157 |
--------------------------------------------------------------------------------
/LumosWeb/api.py:
--------------------------------------------------------------------------------
1 | import select
2 | import socket
3 | from webob import Request
4 | from parse import parse
5 | import inspect
6 | from requests import Session as RequestsSession
7 | from wsgiadapter import WSGIAdapter as RequestsWSGIAdapter
8 | from wsgiref.simple_server import make_server
9 | import os
10 | from jinja2 import Environment, FileSystemLoader
11 | from whitenoise import WhiteNoise
12 | from .middleware import Middleware
13 | from .response import Response
14 | import markdown
15 |
16 | class API:
17 | def __init__(self, templates_dir="templates", static_dir="static"):
18 | self.routes = {} # dictionary of routes and handlers, path as keys and handlers as values
19 |
20 | self.templates_env = Environment(
21 | loader = FileSystemLoader(os.path.abspath(templates_dir))
22 | )
23 |
24 | self.exception_handler = None
25 |
26 | self.whitenoise = WhiteNoise(self.wsgi_app, root=static_dir)
27 |
28 | self.middleware = Middleware(self)
29 |
30 | self._server = None
31 |
32 | def __call__(self, environ, start_response):
33 | path_info = environ["PATH_INFO"]
34 |
35 | if path_info.startswith("/static"):
36 | environ["PATH_INFO"] = path_info[len("/static"):]
37 | return self.whitenoise(environ, start_response)
38 |
39 | return self.middleware(environ, start_response)
40 |
41 | def wsgi_app(self, environ, start_response):
42 | request = Request(environ)
43 |
44 | response = self.handle_request(request)
45 |
46 | return response(environ, start_response)
47 |
48 | def add_route(self, path, handler, allowed_methods=None):
49 | assert path not in self.routes, "You have already used this route, please choose another route :)"
50 |
51 | self.routes[path] = {"handler":handler, "allowed_methods": allowed_methods} # path as an argument.
52 |
53 | def route(self, path, allowed_methods=None):
54 | def wrapper(handler):
55 | self.add_route(path, handler, allowed_methods)
56 | return handler
57 | return wrapper
58 |
59 | def default_response(self, response):
60 | response.status_code = 404
61 | response.text = "Not found. :("
62 |
63 | def find_handler(self, request_path):
64 | for path, handler in self.routes.items():
65 | parse_result = parse(path, request_path)
66 |
67 | if parse_result is not None:
68 | return handler, parse_result.named
69 | return None, None
70 |
71 | def handle_request(self, request):
72 | response = Response()
73 |
74 | handler_data, kwargs = self.find_handler(request_path=request.path)
75 | try:
76 | if handler_data is not None:
77 | handler = handler_data["handler"]
78 | allowed_methods = handler_data["allowed_methods"]
79 | if inspect.isclass(handler): # To check if the handler is a class
80 | handler = getattr(handler(), request.method.lower(), None) # To get the method of the class, if handler() doesn't has the method attribute,getattr() returns None
81 | if handler is None:
82 | raise AttributeError("Method not allowed", request.method)
83 | else:
84 | if request.method.lower() not in allowed_methods:
85 | raise AttributeError("Method not allowed", request.method)
86 | handler(request, response, **kwargs) # **kwargs is used to unpack the dictionary
87 | else:
88 | self.default_response(response)
89 | except Exception as e:
90 | if self.exception_handler is None:
91 | raise e
92 | else:
93 | self.exception_handler(request, response, e)
94 |
95 | return response
96 |
97 | # To create a test client for the API
98 | def test_session(self, base_url="http://testserver"):
99 | session = RequestsSession()
100 | session.mount(prefix=base_url, adapter=RequestsWSGIAdapter(self))
101 | return session
102 |
103 | def template(self, template_name, context=None):
104 | if context is None:
105 | context = {}
106 |
107 | template = self.templates_env.get_template(template_name)
108 | rendered_template = template.render(**context)
109 |
110 | # Check if the file ends with .md extension
111 | if template_name.endswith('.md'):
112 | # Convert the rendered template to HTML using Markdown
113 | converted_html = markdown.markdown(rendered_template, extensions=['fenced_code', 'codehilite', 'tables'])
114 | css_path = os.path.join(os.path.dirname(__file__), 'static/styles.css')
115 | with open(css_path, encoding="utf16") as css_file:
116 | css_content = css_file.read()
117 | rendered_template = f"{converted_html}"
118 |
119 | return rendered_template
120 |
121 | def add_exception_handler(self, exception_handler):
122 | self.exception_handler = exception_handler
123 |
124 | def add_middleware(self, middleware_cls):
125 | self.middleware.add(middleware_cls)
126 |
127 | def is_running(self):
128 | return self._server is not None and not self._server._BaseServer__is_shut_down.is_set()
129 |
130 | def run(self, host="localhost", port=8080, timeout=None):
131 | attempts = 0
132 | while True:
133 | try:
134 | # Check if the port is available
135 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
136 | sock.bind((host, port))
137 | sock.close()
138 | break # Exit the loop if the port is available
139 | except OSError:
140 | print(f"Port {port} is not available, trying the next port")
141 | attempts += 1
142 | port += 1
143 | if attempts > 10:
144 | raise Exception("No ports available to run the API")
145 |
146 | server = make_server(host, port, self)
147 | self._server = server
148 | actual_port = server.server_port
149 | print(f"Starting Lumos server on {host}:{actual_port}")
150 |
151 | if timeout is None:
152 | server.serve_forever()
153 | else:
154 | while True:
155 | r, _, _ = select.select([server], [], [], timeout)
156 | if r:
157 | server.handle_request()
158 | else:
159 | break
160 |
--------------------------------------------------------------------------------
/LumosWeb/orm.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | import sqlite3
3 | from typing import Any
4 |
5 | class Database:
6 | def __init__(self, path):
7 | self.conn = sqlite3.Connection(path)
8 |
9 | @property
10 | def tables(self):
11 | SELECT_TABLES_SQL = "SELECT name FROM sqlite_master WHERE type='table';"
12 | return [x[0] for x in self.conn.execute(SELECT_TABLES_SQL).fetchall()]
13 |
14 | def create(self, table):
15 | self.conn.execute(table._get_create_sql())
16 |
17 | def save(self, instance):
18 | sql, values = instance._get_insert_sql()
19 | cursor = self.conn.execute(sql, values)
20 | instance._data["id"] = cursor.lastrowid
21 | self.conn.commit()
22 |
23 | def all(self, table):
24 | sql, fields = table._get_select_sql()
25 |
26 | result = []
27 | for row in self.conn.execute(sql).fetchall():
28 | instance = table()
29 | for field, value in zip(fields, row):
30 | if field.endswith("_id"):
31 | field = field[:-3]
32 | fk = getattr(table, field)
33 | value = self.get(fk.table, value)
34 | setattr(instance, field, value)
35 | result.append(instance)
36 |
37 | return result
38 |
39 | def get(self, table, id):
40 | sql, fields, params = table._get_select_where_sql(id = id)
41 |
42 | row = self.conn.execute(sql, params).fetchone()
43 | if row is None:
44 | raise Exception(f"{table.__name__} instance with id {id} does not exist")
45 |
46 | instance = table()
47 | for field, value in zip(fields, row):
48 | if field.endswith("_id"):
49 | field = field[:-3]
50 | fk = getattr(table, field)
51 | value = self.get(fk.table, value)
52 | setattr(instance, field, value)
53 |
54 | return instance
55 |
56 | def update(self, instance):
57 | sql, values = instance._get_update_sql()
58 | self.conn.execute(sql, values)
59 | self.conn.commit()
60 |
61 | def delete(self, table, id):
62 | sql, params = table._get_delete_sql(id)
63 | self.conn.execute(sql, params)
64 | self.conn.commit()
65 |
66 | class Table:
67 | def __init__(self, **kwargs):
68 | self._data= {
69 | "id": None
70 | }
71 |
72 | for key, value in kwargs.items():
73 | self._data[key] = value
74 |
75 | @classmethod
76 | def _get_create_sql(cls):
77 | CREATE_TABLE_SQL = "CREATE TABLE IF NOT EXISTS {name} ({fields});"
78 | fields = [
79 | "id INTEGER PRIMARY KEY AUTOINCREMENT"
80 | ]
81 |
82 | for name, field in inspect.getmembers(cls):
83 | if isinstance(field, Column):
84 | fields.append(f"{name} {field.sql_type}")
85 | elif isinstance(field, ForeignKey):
86 | fields.append(f"{name}_id INTEGER")
87 |
88 | fields = ", ".join(fields)
89 | name = cls.__name__.lower()
90 | return CREATE_TABLE_SQL.format(name=name, fields=fields)
91 |
92 | def __getattribute__(self, key):
93 | _data = super().__getattribute__("_data")
94 | if key in _data:
95 | return _data[key]
96 | return super().__getattribute__(key)
97 |
98 | def _get_insert_sql(self):
99 | INSERT_SQL = "INSERT INTO {name} ({fields}) VALUES ({placeholders});"
100 | cls = self.__class__
101 | fields = []
102 | placeholders = []
103 | values = []
104 |
105 | for name, field in inspect.getmembers(cls):
106 | if isinstance(field, Column):
107 | fields.append(name)
108 | values.append(getattr(self, name))
109 | placeholders.append("?")
110 | elif isinstance(field, ForeignKey):
111 | fields.append(name + "_id")
112 | values.append(getattr(self, name).id)
113 | placeholders.append("?")
114 |
115 | fields = ", ".join(fields)
116 | placeholders = ", ".join(placeholders)
117 |
118 | sql = INSERT_SQL.format(name=cls.__name__.lower(), fields=fields, placeholders=placeholders)
119 |
120 | return sql, values
121 |
122 | @classmethod
123 | def _get_select_sql(cls):
124 | SELECT_ALL_SQL = "SELECT {fields} FROM {name};"
125 |
126 | fields = ["id"]
127 | for name, field in inspect.getmembers(cls):
128 | if isinstance(field, Column):
129 | fields.append(name)
130 | if isinstance(field, ForeignKey):
131 | fields.append(name + "_id")
132 |
133 | sql = SELECT_ALL_SQL.format(name=cls.__name__.lower(), fields=", ".join(fields))
134 |
135 | return sql, fields
136 |
137 | @classmethod
138 | def _get_select_where_sql(cls, id):
139 | SELECT_WHERE_SQL = "SELECT {fields} FROM {name} WHERE id = ?;"
140 | fields = ["id"]
141 | for name, field in inspect.getmembers(cls):
142 | if isinstance(field, Column):
143 | fields.append(name)
144 | if isinstance(field, ForeignKey):
145 | fields.append(name + "_id")
146 |
147 | sql = SELECT_WHERE_SQL.format(name=cls.__name__.lower(), fields=", ".join(fields))
148 | params = [id]
149 |
150 | return sql, fields, params
151 |
152 | def __setattr__(self, key, value):
153 | super().__setattr__(key, value)
154 | if key in self._data:
155 | self._data[key] = value
156 |
157 | def _get_update_sql(self):
158 | UPDATE_SQL = 'UPDATE {name} SET {fields} WHERE id = ?'
159 | cls = self.__class__
160 | fields = []
161 | values = []
162 |
163 | for name, field in inspect.getmembers(cls):
164 | if isinstance(field, Column):
165 | fields.append(name)
166 | values.append(getattr(self, name))
167 | elif isinstance(field, ForeignKey):
168 | fields.append(name + "_id")
169 | values.append(getattr(self, name).id)
170 |
171 | values.append(getattr(self, "id"))
172 |
173 | sql = UPDATE_SQL.format(
174 | name=cls.__name__.lower(),
175 | fields=", ".join([f"{field} = ?" for field in fields])
176 | )
177 |
178 | return sql, values
179 |
180 | @classmethod
181 | def _get_delete_sql(cls, id):
182 | DELETE_SQL = "DELETE FROM {name} WHERE id = ?"
183 | sql = DELETE_SQL.format(name=cls.__name__.lower())
184 |
185 | return sql, [id]
186 |
187 |
188 | class Column:
189 | def __init__(self, column_type):
190 | self.type = column_type
191 |
192 | @property
193 | def sql_type(self):
194 | SQLITE_TYPE_MAP = {
195 | int: "INTEGER",
196 | float: "REAL",
197 | str: "TEXT",
198 | bytes: "BLOB",
199 | bool: "INTEGER", # 0 or 1
200 | }
201 | return SQLITE_TYPE_MAP[self.type]
202 |
203 | class ForeignKey:
204 | def __init__(self, table):
205 | self.table = table
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LumosWeb 
2 |
3 | - To ensure compatibility and access the latest features and improvements, it is highly recommended to use version 1.0.0 or higher of the package.
4 | - LumosWeb is web framework written in python
5 | - It's a WSGI framework and can be used with any WSGI application server such as Gunicorn.
6 | - [PyPI Release](https://pypi.org/project/LumosWeb/)
7 | - [Sample App](https://github.com/Sddilora/LumosWeb-SampleApp)
8 |
9 |
10 |
11 | ## Installation
12 | ```shell
13 | pip install LumosWeb==
14 | e.g. pip install LumosWeb==1.0.0
15 | ```
16 |
17 | ## Getting Started
18 |
19 | ## Basic usage
20 |
21 | ### Define App
22 |
23 | ```python
24 | from LumosWeb.api import API()
25 | app = API() # We created our api instance
26 | ```
27 |
28 | ```python
29 | @app.route("/home", allowed_methods=["get", "post", "put", "delete"])
30 | def home(request, response):
31 | if request.method == "get":
32 | response.text = "Hello from the HOME page"
33 | else:
34 | raise AttributeError("Method not allowed.")
35 |
36 | # Parameterized routes
37 | @app.route("/book/{title}/page/{page:d}", allowed_methods=["get", "post"])
38 | def book(request, response, title, page):
39 | response.text = f"You are reading the Book: {title}, and you were on Page: {page}"
40 |
41 | ## Adding a route without a decorator
42 | def handler(req, resp):
43 | resp.text = "We don't have to use decorators!"
44 |
45 | app.add_route("/sample", handler, allowed_methods=["get", "post"])
46 |
47 |
48 | ```
49 | ### Run Server
50 | Navigate to the directory in the Terminal where the file of your API instance is located
51 | > Lumosweb --app run
52 |
53 | And lights are on!
54 |
55 | ### Unit Test
56 |
57 | The recommended way of writing unit tests is with [pytest](https://docs.pytest.org/en/latest/). There are two built in fixtures
58 | that you may want to use when writing unit tests with LumosWeb. The first one is `app` which is an instance of the main `API` class:
59 | ```python
60 | def test_basic_route_adding(api):
61 | @api.route("/home", allowed_methods=["get", "post"])
62 | def home(req, resp):
63 | resp.text = "Lumos is on!"
64 | with pytest.raises(AssertionError):
65 | @api.route("/home", allowed_methods=["get", "post"])
66 | def home2(req, resp):
67 | resp.text = "Lumos is off!"
68 | ```
69 | The other one is `client` that you can use to send HTTP requests to your handlers. It is based on the famous [requests](https://requests.readthedocs.io/) and it should feel very familiar:
70 | ```python
71 | def test_lumos_test_client_can_send_requests(api, client):
72 | RESPONSE_TEXT = "Yes it can :)!"
73 |
74 | @api.route("/lumos", allowed_methods=["get", "post"])
75 | def lumos(req, resp):
76 | resp.text = RESPONSE_TEXT
77 |
78 | assert client.get("http://testserver/lumos").text == RESPONSE_TEXT
79 |
80 | ```
81 |
82 | ## Templates
83 | The default folder for templates is `templates`. You can change it when initializing the main `API()` class:
84 | ```python
85 | app = API(templates_dir="templates_dir_name")
86 | ```
87 | Then you can use HTML or Markdown files in that folder like so in a handler:
88 |
89 | ```python
90 | @app.route("/show/template")
91 | def handler_with_template(req, resp):
92 | resp.html = app.template(
93 | "example.html", context={"title": "Awesome Framework", "body": "welcome to the future!"})
94 |
95 | @app.route("/md-files", allowed_methods=["get"])
96 | def index(req, resp):
97 | resp.html = app.template("index.md")
98 | ```
99 |
100 | ## Static Files
101 |
102 | Just like templates, the default folder for static files is `static` and you can override it:
103 | ```python
104 | app = API(static_dir="static_dir_name")
105 | ```
106 | Then you can use the files inside this folder in HTML files:
107 | ```html
108 |
109 |
110 |
111 |
112 |
113 | {{title}}
114 |
115 |
116 |
117 |
118 |
119 | {{body}}
120 | This is a paragraph
121 |
122 |
123 | ```
124 |
125 | ### Middleware
126 | You can create custom middleware classes by inheriting from the `LumosWeb.middleware.Middleware` class and overriding its two methods
127 | that are called before and after each request:
128 |
129 | ```python
130 | from LumosWeb.api import API
131 | from LumosWeb.middleware import Middleware
132 |
133 | app = API()
134 |
135 | class SimpleCustomMiddleware(Middleware):
136 | def process_request(self, req):
137 | print("Before dispatch", req.url)
138 |
139 | def process_response(self, req, res):
140 | print("After dispatch", req.url)
141 |
142 |
143 | app.add_middleware(SimpleCustomMiddleware)
144 | ```
145 |
146 | ### Database
147 | You can create custom middleware classes by inheriting from the `LumosWeb.orm.Database` class
148 | First create models file and create a class for each table in the database
149 |
150 | ```python
151 | # models.py
152 |
153 | from LumosWeb.orm import Table, Column
154 |
155 | class Book(Table):
156 | author = Column(str)
157 | name = Column(str)
158 | ```
159 | Then create a storage file and import the models
160 |
161 | ```python
162 | # storage.py
163 |
164 | from models import Book
165 |
166 | class BookStorage:
167 | _id = 0
168 |
169 | def __init__(self):
170 | self._books = []
171 |
172 | def all(self):
173 | return [book._asdict() for book in self._books]
174 |
175 | def get(self, id: int):
176 | for book in self._books:
177 | if book.id == id:
178 | return book
179 |
180 | return None
181 |
182 | def create(self, **kwargs):
183 | self._id += 1
184 | kwargs["id"] = self._id
185 | book = Book(**kwargs)
186 | self._books.append(book)
187 | return book
188 |
189 | def delete(self, id):
190 | for ind, book in enumerate(self._books):
191 | if book.id == id:
192 | del self._books[ind]
193 | ```
194 | Now you can use them
195 |
196 | ```python
197 | # app.py
198 |
199 | from LumosWeb.orm import Database
200 |
201 | db = Database("./lumos.db") # lumos.db is the name of the database file
202 | # which will be created in the current directory (if it doesn't exist already)
203 | db.create(Book)
204 |
205 | @app.route("/", allowed_methods=["get"])
206 | def index(req, resp):
207 | books = db.all(Book)
208 | resp.html = app.template("index.html", context={"books": books})
209 |
210 | @app.route("/books", allowed_methods=["post"])
211 | def create_book(req, resp):
212 | book = Book(**req.POST) # Creates a Book instance with the given data in the request.
213 | db.save(book)
214 |
215 | resp.status_code = 201 # Created
216 | resp.json = {"name": book.name, "author": book.author}
217 |
218 | @app.route("/books/{id:d}", allowed_methods=["delete"])
219 | def delete_book(req, resp, id):
220 | db.delete(Book, id=id)
221 | resp.status_code = 204 # No content (resource has successfully been deleted.)
222 |
223 | ```
224 |
--------------------------------------------------------------------------------
/test_lumos.py:
--------------------------------------------------------------------------------
1 | import socket
2 | import pytest
3 |
4 | from LumosWeb.api import API
5 | from LumosWeb.middleware import Middleware
6 |
7 | FILE_DIR ="css"
8 | FILE_NAME = "main.css"
9 | FILE_CONTENTS = "body {background-color: #d0e4fe}"
10 |
11 |
12 | # helpers
13 | # A helper method is created that creates a static file under the given folder.
14 | def _create_static(static_dir):
15 | asset = static_dir.mkdir(FILE_DIR).join(FILE_NAME)
16 | asset.write(FILE_CONTENTS)
17 |
18 | return asset
19 |
20 | # tests
21 |
22 | def test_basic_route_adding(api):
23 | @api.route("/home", allowed_methods=["get", "post"])
24 | def home(req, resp):
25 | resp.text = "Lumos is on!"
26 | with pytest.raises(AssertionError):
27 | @api.route("/home", allowed_methods=["get", "post"])
28 | def home2(req, resp):
29 | resp.text = "Lumos is off!"
30 |
31 | def test_lumos_test_client_can_send_requests(api, client):
32 | RESPONSE_TEXT = "Yes it can :)!"
33 |
34 | @api.route("/lumos", allowed_methods=["get", "post"])
35 | def lumos(req, resp):
36 | resp.text = RESPONSE_TEXT
37 |
38 | assert client.get("http://testserver/lumos").text == RESPONSE_TEXT
39 |
40 | def test_parametrized_route(api, client):
41 | @api.route("/{name}", allowed_methods=["get"])
42 | def hello(req, resp, name):
43 | resp.text = f"Hey {name}"
44 |
45 | assert client.get("http://testserver/sdd").text == "Hey sdd"
46 | assert client.get("http://testserver/123").text == "Hey 123"
47 |
48 | def test_params_are_passed_correctly(api, client):
49 | @api.route("/sum/{num_1:d}/{num_2:d}", allowed_methods=["get", "post"])
50 | def sum(req, resp, num_1, num_2):
51 | resp.text = f"{num_1} + {num_2} = {num_1 + num_2}"
52 |
53 | assert client.get("http://testserver/sum/12/13").text == "12 + 13 = 25"
54 | assert client.get("http://testserver/sum/12/13/14").status_code == 404
55 | assert client.get("http://testserver/sum/12/hello").status_code == 404
56 |
57 | def test_default_404_response(client):
58 | response = client.get("http://testserver/doesnotexist")
59 | assert response.status_code == 404
60 | assert response.text == "Not found. :("
61 |
62 | def test_class_based_handler_get(api, client):
63 | RESPONSE_TEXT = "This is a GET request"
64 |
65 | @api.route("/book", allowed_methods=["get", "post"])
66 | class BookResource:
67 | def get(self, req, resp):
68 | resp.text = RESPONSE_TEXT
69 |
70 | assert client.get("http://testserver/book").text == RESPONSE_TEXT
71 |
72 | def test_class_based_handler_post(api, client):
73 | RESPONSE_TEXT = "This is a POST request"
74 |
75 | @api.route("/book", allowed_methods=["get", "post"])
76 | class BookResource:
77 | def post(self, req, resp):
78 | resp.text = RESPONSE_TEXT
79 |
80 | assert client.post("http://testserver/book").text == RESPONSE_TEXT
81 |
82 | def test_class_based_handler_not_allowed_method(api, client):
83 | @api.route("/book", allowed_methods=["get", "post"])
84 | class BookResource:
85 | def post(self, req, resp):
86 | resp.text = "Lumos!"
87 |
88 | with pytest.raises(AttributeError):
89 | client.get("http://testserver/book")
90 |
91 | def test_alternative_route(api, client):
92 | RESPONSE_TEXT = "Alternative way to add a route"
93 |
94 | def home(req, resp):
95 | resp.text = RESPONSE_TEXT
96 |
97 | api.add_route("/alternative", home, allowed_methods=["get", "post"])
98 |
99 | assert client.get("http://testserver/alternative").text == RESPONSE_TEXT
100 |
101 | def test_template(api, client):
102 | @api.route("/html", allowed_methods=["get", "post"])
103 | def html_handler(req, resp):
104 | resp.body = api.template(
105 | "index.html", context={"title": "Some Title", "name": "Some Name"}
106 | ).encode()
107 |
108 | response = client.get("http://testserver/html")
109 | assert "text/html" in response.headers["Content-Type"]
110 | assert "Some Title" in response.text
111 | assert "Some Name" in response.text
112 |
113 | def test_custom_exception_handler(api, client):
114 | def on_exception(req, resp, exc):
115 | resp.text = "AttributeErrorHappened"
116 |
117 | api.add_exception_handler(on_exception)
118 |
119 | @api.route("/", allowed_methods=["get"])
120 | def index(req, resp):
121 | raise AttributeError()
122 |
123 | response = client.get("http://testserver/")
124 | assert response.text == "AttributeErrorHappened"
125 |
126 | # This one tests if a 404 (Not Found) response is returned if a request is sent for a nonexistent static file.
127 | def test_404_is_returned_for_nonexistent_static_file(client):
128 | assert client.get(f"http://testserver/static/main.css)").status_code == 404
129 |
130 | def test_assets_are_served(tmpdir_factory):
131 | static_dir = tmpdir_factory.mktemp("static")
132 | _create_static(static_dir)
133 |
134 | api = API(static_dir=str(static_dir))
135 | client = api.test_session()
136 |
137 | response = client.get(f"http://testserver/static/{FILE_DIR}/{FILE_NAME}")
138 | assert response.status_code == 200
139 | assert response.text == FILE_CONTENTS
140 |
141 | def test_middleware_methods_are_called(api, client):
142 | process_request_called = False
143 | process_response_called = False
144 |
145 | class CalledMiddleware(Middleware):
146 | def __init__(self, app):
147 | super().__init__(app)
148 |
149 | def process_request(self, req):
150 | nonlocal process_request_called
151 | process_request_called = True
152 |
153 | def process_response(self, req, resp):
154 | nonlocal process_response_called
155 | process_response_called = True
156 |
157 | api.add_middleware(CalledMiddleware)
158 |
159 | @api.route("/", allowed_methods=["get"])
160 | def index(req, resp):
161 | resp.text = "Hello Middleware!"
162 |
163 | client.get("http://testserver/")
164 |
165 | assert process_request_called is True
166 | assert process_response_called is True
167 |
168 | def test_allowed_methods_for_function_based_handlers(api, client):
169 | @api.route("/home", allowed_methods=["post"])
170 | def home(req, resp):
171 | resp.text = "Hello"
172 |
173 | with pytest.raises(AttributeError):
174 | client.get("http://testserver/home")
175 |
176 | assert client.post("http://testserver/home").text == "Hello"
177 |
178 | def test_json_response_helper(api, client):
179 | @api.route("/json", allowed_methods=["get", "post"])
180 | def json_handler(req, resp):
181 | resp.json = {"name": "Lumos"}
182 |
183 | response = client.get("http://testserver/json")
184 | json_body = response.json()
185 | assert response.headers["Content-Type"] == "application/json"
186 | assert json_body["name"] == "Lumos"
187 |
188 | def test_html_response_helper(api, client):
189 | @api.route("/html", allowed_methods=["get", "post"])
190 | def html_handler(req, resp):
191 | resp.html = api.template(
192 | "index.html", context={"title": "Some Title", "name": "Some Name"}
193 | )
194 |
195 | response = client.get("http://testserver/html")
196 | assert "text/html" in response.headers["Content-Type"]
197 | assert "Some Title" in response.text
198 | assert "Some Name" in response.text
199 |
200 | def test_text_response_helper(api, client):
201 | response_text = "Plain text response from LumosWeb"
202 |
203 | @api.route("/text", allowed_methods=["get", "post"])
204 | def text_handler(req, resp):
205 | resp.text = response_text
206 |
207 | response = client.get("http://testserver/text")
208 |
209 | assert "text/plain" in response.headers["Content-Type"]
210 | assert response.text == response_text
211 |
212 | def test_manually_setting_body(api, client):
213 | @api.route("/body", allowed_methods=["get", "post"])
214 | def text_handler(req, resp):
215 | resp.body = b"Byte body"
216 | resp.content_type = "text/plain"
217 |
218 | response = client.get("http://testserver/body")
219 |
220 | assert "text/plain" in response.headers["Content-Type"]
221 | assert response.text == "Byte body"
222 |
223 | def test_run_success(api):
224 | host = "localhost"
225 | port = 8080
226 |
227 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
228 | sock.bind((host, port))
229 |
230 | try:
231 | api.run(host=host, port=port, timeout=1) # Set a short timeout for testing purposes
232 | assert api.is_running()
233 | finally:
234 | sock.close()
235 |
236 | def test_run_alternative_port(api):
237 | host = "localhost"
238 | port = 8080
239 |
240 | # Create a socket and bind it to port 8080 to simulate a busy port
241 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
242 | sock.bind((host, port))
243 |
244 | try:
245 | api.run(host=host, port=port, timeout=1) # Set a short timeout for testing purposes
246 | assert api.is_running() # Check if the API is running (on another port, api.run changes the port if default port is not available) even though the default port is busy
247 | except Exception as exc:
248 | assert str(exc) == "No ports available to run the API"
249 | finally:
250 | sock.close()
251 |
252 | def test_run_exception_other_error(api):
253 | host = "localhost"
254 | port = 8080
255 |
256 | try:
257 | with pytest.raises(Exception):
258 | # Simulate a different exception by passing an invalid value for `host`
259 | api.run(host=None, port=port, timeout=1) # Set a short timeout for testing purposes
260 | finally:
261 | assert not api.is_running()
262 |
263 |
--------------------------------------------------------------------------------