Skip to content

Commit

Permalink
💥🔧🏗️
Browse files Browse the repository at this point in the history
├── 🏗️💥 major changes to midleware main processing
└── 🔧 rework of settings
  • Loading branch information
lstuma committed Nov 2, 2023
1 parent 0463f6d commit 2457704
Show file tree
Hide file tree
Showing 30 changed files with 400 additions and 249 deletions.
6 changes: 3 additions & 3 deletions demo/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@

# ADDRESS AND PORT
ADDRESS = ''
PORT = 81
PORT = 8080

# VIEWS
urls = {
VIEWS = {
'/welcome': views.welcome,
'/': views.counter,
}
errors = {
ERRORS = {
404: errors.not_found,
}

Expand Down
15 changes: 12 additions & 3 deletions demo/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
from pigeon.shortcuts import JSONResponse, render
from pigeon.shortcuts import HTTPResponse, JSONResponse, render
from pigeon.decorators import content_type


@content_type('application/json')
def welcome(request):
return JSONResponse(data={'welcome':'Hello World!'})
return JSONResponse(data={'welcome': 'Hello World!'})


@content_type('text/plain')
def welcome(request):
return HTTPResponse(data='Welcome! Hello World!', headers={'Content-Type': 'text/plain'})


def counter(request):
return render('counter.html', context={'request': request})
return render('counter.html', context={'request': request})
1 change: 1 addition & 0 deletions src/pigeon/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
import pigeon.http
import pigeon.templating
import pigeon.utils
import pigeon.middleware
5 changes: 3 additions & 2 deletions src/pigeon/conf/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from pigeon.conf.settings import Settings
settings = Settings()
import pigeon.conf.middleware as middleware
import pigeon.conf.settings as settings
import pigeon.conf.manager as manager
76 changes: 76 additions & 0 deletions src/pigeon/conf/manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from pathlib import Path
import pigeon.conf.settings as settings
import pigeon.conf.registry as registry


def setup():
"""
Configures any settings that need to be computed at runtime (e.g. typed views).
"""
# configure typed views
configure_typed_views()


def override(new_settings):
"""
Overrides current settings with new settings provided.
"""
# get all non-standard attributes as dict:
# attributes = {<attribute_name>:<attribute_value>}
attributes = {attr: getattr(new_settings, attr) for attr in dir(new_settings) if not attr.startswith('__')}

# override any attributes that also exist in settings
for attribute, value in attributes.items():
if hasattr(settings, attribute):
old = getattr(settings, attribute)
if isinstance(old, dict):
# if attribute is a dict only change values set in new_settings.attribute
old.update(value)
else:
setattr(settings, attribute, value)

# try to convert attributes containing filepaths to pathlib.Path if they are set
path_attributes = ['STATIC_FILES_DIR', 'MEDIA_FILES_DIR', 'TEMPLATES_DIR', 'CERTIFICATE_PATH', 'PRIVATE_KEY_PATH']
for attribute in path_attributes:
if value := getattr(settings, attribute):
setattr(settings, attribute, Path(value))


def configure_typed_views():
"""
Builds typed views used by middleware in content-negotiation.
"""
# reverse and restructure views dictionary like {(<func.__name__>,<func.__module__>):url, ...} for easier processing
# of views in next step.
reversed_views = dict()
for url, func in settings.VIEWS.items():
key = (func.__name__, func.__module__)
if reversed_views.get(key):
reversed_views[key].append(url)
else:
reversed_views[key] = [url]

# add typed funcs to views
print(reversed_views)
print(registry.TYPED_VIEWS)
for func, content_type in registry.TYPED_VIEWS:
# determine in which view the current function is listed in and then add it to it
key = (func.__name__, func.__module__)
if reversed_views.get(key):
for url in reversed_views[key]:
if settings.TYPED_VIEWS.get(url):
settings.TYPED_VIEWS[url][content_type] = func
else:
settings.TYPED_VIEWS[url] = {content_type: func}


print(settings.TYPED_VIEWS)
# add untyped funcs to views as type */*
for url, func in settings.VIEWS.items():
if settings.TYPED_VIEWS.get(url):
if func not in settings.TYPED_VIEWS[url].values():
settings.TYPED_VIEWS[url]['*/*'] = func
else:
settings.TYPED_VIEWS[url] = {'*/*': func}

print(settings.TYPED_VIEWS)
18 changes: 0 additions & 18 deletions src/pigeon/conf/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,3 @@
'1.1': Owl,
'2.0': Raven,
}

# PREPROCESSING COMPONENTS (COMPONENTS USED BY PREPROCESSOR)
PREPROCESSING_COMPONENTS = [
comp.host.HostComponent,
comp.cors.CORSComponent,
comp.method.MethodComponent,
comp.connection.ConnectionComponent,
comp.connection.CacheControlComponent,
]
# POSTPROCESSING COMPONENTS (COMPONENTS USED BY POSTPROCESSOR)
POSTPROCESSING_COMPONENTS = [
comp.server.ServerComponent,
comp.cors.CORSComponent,
comp.connection.ConnectionComponent,
comp.connection.CacheControlComponent,
]

#
7 changes: 7 additions & 0 deletions src/pigeon/conf/registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#
# Used to strore decentralized data in a central space (e.g. functions for typed views)
#


# list of views with content-type specified - will be filled through content_type decorator
TYPED_VIEWS = []
144 changes: 56 additions & 88 deletions src/pigeon/conf/settings.py
Original file line number Diff line number Diff line change
@@ -1,88 +1,56 @@
from pathlib import Path


class Settings:
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_allowed_headers = cors[2]
self.cors_allowed_methods = cors[3]
self.cors_max_age = cors[4]

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

# static
self.static_url_base = static[0]
self.static_files_dir = Path(static[1]) if static[1] else None

# media
self.media_url_base = media[0]
self.media_files_dir = Path(media[1]) if media[1] else None

# templates
self.templates_dir = Path(templates_dir) if templates_dir else None

# https
self.use_https = https[0]
self.https_cert_path = https[1]
self.https_privkey_path = https[2]
self.https_privkey_passwd = https[3]

# mime
self.supported_mimetypes = mime

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_methods)
self.views = local.urls
self.errors = {**self.errors, **check( 'errors', dict())}

# 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

# 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

# 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)
import pigeon.http.parsing.mime
import pigeon.default.errors

# VERBOSITY
VERBOSITY = 2

# ADDRESS
ADDRESS = ''
PORT = 8080

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

# VIEWS
VIEWS = {}
# TYPED VIEWS (INCLUDE CONTENT TYPE)
TYPED_VIEWS = {}
# ERRORS (VIEWS BUT FOR ERRORS)
ERRORS = {
000: pigeon.default.errors.fallback,
}

# ACCESS-CONTROL
CORS_ALLOWED_ORIGINS = []
CORS_ALLOW_CRED = False
CORS_ALLOWED_HEADERS = ['Content-Type']
CORS_ALLOWED_METHODS = ['POST', 'GET', 'HEAD', 'POST', 'PUT', 'OPTIONS']
CORS_MAX_AGE = 1200

# STATICFILES
STATIC_URL_BASE = None
STATIC_FILES_DIR = None

# MEDIAFILES
MEDIA_URL_BASE = None
MEDIA_FILES_DIR = None

# TEMPLATING
TEMPLATES_DIR = None

# HTTPS
USE_HTTPS = False
CERTIFICATE_PATH = None
PRIVATE_KEY_PATH = None
PRIVATE_KEY_PASSWD = None

# MIME
SUPPORTED_MIMETYPES = {
'application/json': pigeon.http.parsing.mime.JSONParser,
'application/x-www-form-urlencoded': pigeon.http.parsing.mime.UrlencodedFormParser,
'multipart/form-data': pigeon.http.parsing.mime.MultiPartFormParser,
}

MIDDLEWARE = None
21 changes: 11 additions & 10 deletions src/pigeon/core/server.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import socket
from pigeon.conf import settings
import pigeon.conf as conf
import pigeon.default.settings as default
import pigeon.utils.logger as logger
from pigeon.utils.logger import create_log
Expand All @@ -15,21 +16,21 @@

def start(settings_used):
# configure settings
settings.override(default)
settings.override(settings_used)
conf.manager.override(settings_used)
conf.manager.setup()


# set verbosity for logger
logger.VERBOSITY = settings.verbosity
logger.VERBOSITY = settings.VERBOSITY

log(2, 'STARTING SERVER...')

# load static files into memory
if settings.static_files_dir:
if settings.STATIC_FILES_DIR:
log(2, 'LOADING STATIC FILES')
static.load()

if settings.templates_dir:
if settings.TEMPLATES_DIR:
# create jinja2 template environment
log(2, 'LOADING TEMPLATES')
templater.load()
Expand All @@ -38,18 +39,18 @@ def start(settings_used):


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

# open socket
sock = socket.socket(socket.AF_INET)
sock.setblocking(False)
sock.bind(settings.address)
sock.bind((settings.ADDRESS, settings.PORT))

# configure https if specified in settings
if settings.use_https:
if settings.USE_HTTPS:
log(3, 'USING HTTPS')
secure_sock = secure.make_secure(sock, settings.https_cert_path, settings.https_privkey_path, settings.https_privkey_passwd)
secure_sock = secure.make_secure(sock, settings.CERTIFICATE_PATH, settings.PRIVATE_KEY_PATH, settings.PRIVATE_KEY_PASSWD)
# securing socket failed
if not secure_sock:
log(0, 'HTTPS FAILED')
Expand Down
13 changes: 13 additions & 0 deletions src/pigeon/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import pigeon.conf.registry as registry


def content_type(accept):
"""
Allows overloading functions by giving them a different content_type.
The decorator is used when defining views to allow developers to give multiple options for content-negotiation.
The standard middleware will choose one of the views dependent on content-negotiation.
"""
def _content_type(func):
registry.TYPED_VIEWS.append((func, accept.lower()))
return func
return _content_type
2 changes: 1 addition & 1 deletion src/pigeon/default/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@

def fallback(request: HTTPRequest | None, code: int):
"""
Fallback for when no
Fallback for when no specific error view is provided for status code
"""
return JSONResponse(data={'error': f'error{": " + request.path if request else ""} {code}'}, status=code)
Loading

1 comment on commit 2457704

@lstuma
Copy link
Member Author

@lstuma lstuma commented on 2457704 Nov 2, 2023

Choose a reason for hiding this comment

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

fix #19

Please sign in to comment.