Skip to content

Commit

Permalink
🏗️⚡🔧 rewrite entire middleware
Browse files Browse the repository at this point in the history
 ├─ 🏗️ middleware is now structured into three parts
 │    ├─ preprocessing: parse request, evaluate and validate request
 │    ├─ proccessing: gather response from application logic
 │    └─ postprocessing: add server-side headers etc.
 └─ 🔧🎨 rewrote pigeon.conf.settings
       ├─  can now be imported using `from pigeon.conf import settings` and cannot be directly created from module
       └─ if one wishes to create settings from a module one must first create a pigeon.conf.settings.Settings object and then use Settings.override(module) to overwrite the settings using the module
  • Loading branch information
lstuma committed Nov 1, 2023
1 parent ee376aa commit c43c013
Show file tree
Hide file tree
Showing 20 changed files with 117 additions and 118 deletions.
5 changes: 2 additions & 3 deletions demo/app.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import pigeon.core.server as server
from pigeon.conf.settings import Settings
import settings as local
import settings as settings

def run():
server.start(
Settings.from_settings(local)
settings_used=settings
)


Expand Down
9 changes: 5 additions & 4 deletions demo/settings.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import pathlib
import views, errors
import views
import errors

BASE_DIR = pathlib.Path(__file__).parent.resolve()

# LOGGIN VERBOSITY
VERBOSITY = 2
VERBOSITY = 4

# ADDRESS AND PORT
ADDRESS = ''
PORT = 80
PORT = 81

# VIEWS
urls = {
Expand Down Expand Up @@ -37,7 +38,7 @@
MEDIA_FILES_DIR = BASE_DIR / 'media/'

# TEMPLATES
TEMPLATES_DIR = 'templates/'
TEMPLATES_DIR = BASE_DIR / 'templates/'

# HTTPS
USE_HTTPS = False
Expand Down
1 change: 1 addition & 0 deletions src/pigeon/conf/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from pigeon.conf.settings import Settings
settings = Settings()
89 changes: 42 additions & 47 deletions src/pigeon/conf/settings.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
from pathlib import Path
import pigeon.default.settings as default


class Settings:
def __init__(self, verbosity: int, address: str, port: int, allowed_hosts: list, urls: dict, errors: dict, cors: tuple,
static: tuple, media: tuple, templates_dir, https: tuple,
mime: dict):
def __init__(self, verbosity: int = None, address: str = None, port: int = None, allowed_hosts: list = None,
allowed_methods: list = None, urls: dict = None, errors: dict = None,
cors: tuple = (None, None, None, None, None), static: tuple = (None, None), media: tuple = (None, None),
templates_dir=None, https: tuple = (None, None, None, None), mime: dict = None):
# logging
self.verbosity = verbosity

# address and port
self.address = (address, port)

self.allowed_hosts = allowed_hosts
self.allowed_methods = allowed_methods

# cors
self.cors_allowed_origins = cors[0]
self.cors_allow_creds = cors[1]
self.cors_allow_headers = cors[2]
self.cors_allow_methods = cors[3]
self.cors_allowed_headers = cors[2]
self.cors_allowed_methods = cors[3]
self.cors_max_age = cors[4]

# views
self.views = urls
self.errors = errors
self.errors = errors or dict()

# static
self.static_url_base = static[0]
Expand All @@ -45,49 +46,43 @@ def __init__(self, verbosity: int, address: str, port: int, allowed_hosts: list,
# mime
self.supported_mimetypes = mime

@classmethod
def from_settings(cls, local):
return Settings(
verbosity=getattr(local, 'VERBOSITY', 2),
address=local.ADDRESS,
port=local.PORT,
allowed_hosts=local.ALLOWED_HOSTS,
urls=local.urls,
errors={**default.errors, **getattr(local, 'errors', dict())},
cors=(
getattr(local, 'CORS_ALLOWED_ORIGINS', []),
getattr(local, 'CORS_ALLOW_CREDENTIALS', False),
getattr(local, 'CORS_ALLOW_HEADERS', ['Content-Type']),
getattr(local, 'CORS_ALLOW_METHODS', ['POST', 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'OPTIONS']),
getattr(local, 'CORS_MAX_AGE', 1200)
),
static=(
getattr(local, 'STATIC_URL_BASE', None),
getattr(local, 'STATIC_FILES_DIR', None),
),
media=(
getattr(local, 'MEDIA_URL_BASE', None),
getattr(local, 'MEDIA_FILES_DIR', None),
),
templates_dir=getattr(local, 'TEMPLATES_DIR', None),
https=(
getattr(local, 'USE_HTTPS', False),
getattr(local, 'CERTIFICATE_PATH', ''),
getattr(local, 'PRIVATE_KEY_PATH', ''),
getattr(local, 'PRIVATE_KEY_PASSWD', ''),
),
mime=(getattr(local, 'SUPPORTED_MIMETYPES', default.SUPPORTED_MIMETYPES)),
)
def override(self, local):
check = lambda property_name, current_value: getattr(local, property_name, current_value)

self.verbosity = check('VERBOSITY', self.verbosity)
self.address = (local.ADDRESS, local.PORT)
self.allowed_hosts = local.ALLOWED_HOSTS
self.allowed_methods = check('ALLOWED_METHODS', self.allowed_hosts)
self.views = local.urls
self.errors = {**self.errors, **check( 'errors', dict())}

settings_used: Settings
# CORS
self.cors_allowed_origins = check('CORS_ALLOWED_ORIGINS', self.cors_allowed_origins)
self.cors_allow_creds = check('CORS_ALLOW_CREDENTIALS', self.cors_allow_creds)
self.cors_allowed_headers = check('CORS_ALLOWED_HEADERS', self.cors_allowed_headers)
self.cors_allowed_methods = check('CORS_ALLOWED_METHODS', self.cors_allowed_methods)
self.cors_max_age = check('CORS_MAX_AGE', self.cors_max_age)

# STATIC
self.static_url_base = check('STATIC_URL_BASE', self.static_url_base)
self.static_files_dir = check('STATIC_FILES_DIR', self.static_files_dir)
self.static_files_dir = Path(self.static_files_dir) if self.static_files_dir else None

def use(settings: Settings) -> None:
global settings_used
settings_used = settings
# MEDIA
self.media_url_base = check('MEDIA_URL_BASE', self.media_url_base)
self.media_files_dir = check('MEDIA_FILES_DIR', self.media_files_dir)
self.media_files_dir = Path(self.media_files_dir) if self.media_files_dir else None

# TEMPLATING
self.templates_dir = check('TEMPLATES_DIR', self.templates_dir)
self.templates_dir = Path(self.templates_dir) if self.templates_dir else None

def get() -> Settings:
global settings_used
return settings_used
# HTTPS
self.use_https = check('USE_HTTPS', self.use_https)
self.https_cert_path = check('CERTIFICATE_PATH', self.https_cert_path)
self.https_cert_path = Path(self.https_cert_path) if self.https_cert_path else None
self.https_privkey_path = check('PRIVATE_KEY_PATH', self.https_privkey_path)
self.https_privkey_path = Path(self.https_privkey_path) if self.https_privkey_path else None
self.https_privkey_passwd = check('PRIVATE_KEY_PASSWD', self.https_privkey_passwd)

self.supported_mimetypes = check('SUPPORTED_MIMETYPES', self.supported_mimetypes)
1 change: 0 additions & 1 deletion src/pigeon/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import pigeon.core.access_control as access_control
import pigeon.core.handler as handler
import pigeon.core.secure as secure
import pigeon.core.server as server
9 changes: 6 additions & 3 deletions src/pigeon/core/handler.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import socket
import pigeon.conf.settings as _settings
from pigeon.conf import settings
import pigeon.middleware as middleware
from pigeon.utils.logger import create_log
from pigeon.http import HTTPRequest, HTTPResponse
from pigeon.files.static import handle_static_request
from pigeon.files.media import handle_media_request
from pigeon.http.common import error

log = create_log('HANDLER', 'cyan')
settings = _settings.get()



def receive_data(client_sock: socket.socket, size:int = 4096):
Expand Down Expand Up @@ -54,7 +57,7 @@ def handle_connection(client_sock: socket.socket, client_address: tuple):
log(3, f'RESPONSE SENT')

# do not keep connection open on error
if response.is_error():
if response.is_error:
break

# client asks to terminate connection
Expand Down
18 changes: 9 additions & 9 deletions src/pigeon/core/server.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import socket
import pigeon.conf.settings as _settings
import pigeon.core.secure as secure
from pigeon.utils.logger import create_log
from pigeon.conf import settings
import pigeon.default.settings as default
import pigeon.utils.logger as logger
from pigeon.utils.logger import create_log
import pigeon.core.secure as secure
import pigeon.core.handler as handler
import pigeon.default.errors as default_errors
import pigeon.files.static as static
Expand All @@ -12,11 +13,10 @@
log = create_log('SERVER', 'white')


def start(settings_used: _settings.Settings):
def start(settings_used):
# configure settings
_settings.use(settings_used)

settings = settings_used
settings.override(default)
settings.override(settings_used)


# set verbosity for logger
Expand All @@ -34,10 +34,10 @@ def start(settings_used: _settings.Settings):
log(2, 'LOADING TEMPLATES')
templater.load()

serve(settings)
serve()


def serve(settings: _settings.Settings):
def serve():
log(2, f'ADDRESS: {settings.address[0] if settings.address[0] else "ANY"}')
log(2, f'PORT: {settings.address[1]}')

Expand Down
6 changes: 3 additions & 3 deletions src/pigeon/default/errors.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from pigeon.http import HTTPResponse, HTTPRequest, JSONResponse
from pigeon.http import HTTPResponse, HTTPRequest, JSONResponse


def fallback(request: HTTPRequest, code: int):
def fallback(request: HTTPRequest | None, code: int):
"""
Fallback for when no
"""
return JSONResponse(data={'error':f'invalid request: {request.path}'}, status=code)
return JSONResponse(data={'error': f'error{": " + request.path if request else ""} {code}'}, status=code)
10 changes: 5 additions & 5 deletions src/pigeon/default/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
# ALLOWED HOSTS
ALLOWED_HOSTS = None

# ALLOWED METHODS
ALLOWED_METHODS = ['POST', 'GET', 'HEAD', 'POST', 'PUT', 'OPTIONS']

# VIEWS
urls = {

Expand All @@ -20,14 +23,11 @@
000: fallback,
}

# ALLOWED METHODS
ALLOWED_METHODS = ['POST', 'GET', 'HEAD', 'POST', 'PUT', 'OPTIONS']

# CORS
CORS_ALLOWED_ORIGINS = []
CORS_ALLOW_CREDENTIALS = False
CORS_ALLOW_HEADERS = ['Content-Type']
CORS_ALLOW_METHODS = ['POST', 'GET', 'HEAD', 'POST', 'PUT', 'OPTIONS']
CORS_ALLOWED_HEADERS = ['Content-Type']
CORS_ALLOWED_METHODS = ['POST', 'GET', 'HEAD', 'POST', 'PUT', 'OPTIONS']
CORS_MAX_AGE = 1200

# STATIC
Expand Down
4 changes: 2 additions & 2 deletions src/pigeon/files/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
import pigeon.files.media as media
import pigeon.files.static as static
from pigeon.files.media import handle_media_request
from pigeon.files.static import handle_static_request
4 changes: 1 addition & 3 deletions src/pigeon/files/media.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import pigeon.conf.settings as _settings
from pigeon.conf import settings
from pigeon.http import HTTPRequest, HTTPResponse
from pigeon.http.common import error, status
from pathlib import Path
import mimetypes
import gzip
import os

settings = _settings.get()


def fetch_file(local_path: Path, encodings):
"""
Expand Down
4 changes: 2 additions & 2 deletions src/pigeon/files/static.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import pigeon.conf.settings as _settings
from pigeon.conf import settings
from pigeon.http import HTTPRequest, HTTPResponse
from pigeon.http.common import error
from pathlib import Path
Expand All @@ -7,7 +7,7 @@
import os

loaded_files = dict()
settings = _settings.get()



def load():
Expand Down
8 changes: 3 additions & 5 deletions src/pigeon/http/common.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import pigeon.conf.settings as _settings
from pigeon.conf import settings
from http import HTTPStatus

settings = _settings.get()


def error(code: int, request):
"""
Returns the HTTPResponse for the error code provided
"""
# if a specific error view for the error code exists
if code in settings.errors:
return settings.errors[code](request)
return settings.errors[code](request=request)
# otherwise just return a standard error page but with the code provided
else:
return settings.errors[000](request, code)
return settings.errors[000](request=request, code=code)


def status(code):
Expand Down
5 changes: 4 additions & 1 deletion src/pigeon/http/parsing/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@



def parse(request: str) -> HTTPRequest:
def parse(request: bytes) -> HTTPRequest:
"""
Parses a string representation of an http request and creates a valid HTTPRequest object from it.
"""

# decode request
request = str(request, 'ascii')

# split into request line and message
request_line, message_raw = (request.split('\r\n', 1)+[''])[:2]

Expand Down
11 changes: 7 additions & 4 deletions src/pigeon/middleware/components/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@ class ConnectionComponent(comp.MiddlewareComponent):
@classmethod
def postprocess(cls, response: HTTPResponse, request: HTTPRequest) -> HTTPResponse | int:
# do not close connection if client requests to keep it alive
if cls.is_keep_alive(request=request):
response.set_headers(headers={'Connection': 'keep-alive'})
response.set_headers(headers={'Connection': 'close'})
response.set_headers(headers={'Connection': 'keep-alive' if request.keep_alive else 'close'})
return response


@classmethod
def preprocess(cls, request: HTTPRequest) -> HTTPRequest | int:
# set keep-alive property for HTTPRequest object
request.keep_alive = cls.is_keep_alive(request=request)
return request

@classmethod
def is_keep_alive(cls, request: HTTPRequest) -> bool:
"""
Checks if the Host header in the request has a valid hostname.
"""
return request.headers('connection') == 'keep-alive'
return request.headers('connection') == 'keep-alive'
1 change: 0 additions & 1 deletion src/pigeon/middleware/components/method.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@




class MethodComponent(comp.MiddlewareComponent):
@classmethod
def preprocess(cls, request: HTTPRequest) -> HTTPRequest | int:
Expand Down
Loading

1 comment on commit c43c013

@lstuma
Copy link
Member Author

@lstuma lstuma commented on c43c013 Nov 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix #19
Fix #12
I need to get some sleep - damn

Please sign in to comment.