Skip to content

Commit

Permalink
Merge pull request #2 from modularizer/micro
Browse files Browse the repository at this point in the history
Micro independence
  • Loading branch information
modularizer committed May 18, 2024
2 parents 41eecb7 + 4d38d9a commit cfc2db0
Show file tree
Hide file tree
Showing 26 changed files with 1,364 additions and 269 deletions.
58 changes: 55 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# socketwrench
A webserver based on `socket.socket` with no dependencies other than the standard library.
A webserver based on `socket.socket` with no dependencies whatsoever other **socket** (or a substitute you pass in ) and **optional** standard library dependencies which improve features. See [dependencies](#dependencies) for more info.
Provides a lightweight quickstart to make an API which supports OpenAPI, Swagger, and more.

## NOTE:
Expand Down Expand Up @@ -29,7 +29,7 @@ class MyServer:
return "world"

if __name__ == '__main__':
serve(MyServer)
serve(MyServer, thread=True)
# OR
# m = MyServer()
# serve(m)
Expand Down Expand Up @@ -111,8 +111,51 @@ No need to use our favicon! pass a `str | Path` `.ico` filepath to `favicon` arg
### fallback handler
Add a custom function to handle any requests that don't match any other routes.

# Dependencies
Default behavior is to use the standard library only. However, if you do not have the full standard library, socketwrench _should_ still work.
This is a work in progress as I am attempting to support micropython, circuitpython, etc. but I have not tested on these environments yet.

### How it works:
* `socket.socket` is the only required dependency, BUT you can pass in a substitute socket object if you want.
* The following standard library modules are used, BUT if import fails for any of them we fall back on dump fake versions I made in [src/socketwrench/fake_imports](.src/socketwrench/fake_imports) which attempt to approximate the same functionality
```python
import socket

import builtins # very niche case use for if a function is typehinted to accept a type, e.g. `def f(x: type):` and you pass in the type name via a string query e.g. `?x=int`
import inspect # used often for getting function signatures, autofilling parameters, etc., spoof version uses `__annotations__` and `__defaults__` of functions
from sys import argv # only used in commandline mode
from argparse import ArgumentParser # only used in commandline mode
from tempfile import TemporaryFile # only used if you attempt to return a folder using a StaticFileHandler
from zipfile import ZipFile # only used if you attempt to return a folder using a StaticFileHandler
from functools import wraps, partial # used regularly but easily replaced
import dataclasses # only used if your python function returns a dataclass which we try to coerce to json
from datetime import datetime # used for Last-Modified header of File responses
from pathlib import Path # used for file responses and static file serving, spoof version works okay
from json import dumps, loads # used for json responses, spoof version works okay
import logging # used for logging, spoof version works okay
from time import sleep # only used if pause_sleep > 0 or accept_sleep > 0, spoof version does not sleep at all
from threading import Event, Thread # only used if you `thread=True` in `serve` function (defaults to False)
from traceback import format_exception # only used if error_mode="traceback"
import importlib # only used if you pass a string into the serve module as the item to be served, e.g. in commandline mode
from sys import modules # only used if you pass a string into the serve module as the item to be served, e.g. in commandline mode
```

### sample
```python
from socketwrench import serve
import socket

class Sample:
def hello(self):
return "world"

if __name__ == '__main__':
serve(Sample, spoof_modules="all", thread=True, socket=socket, port=8123)
```

# Planned Features
* [ ] Implement nesting / recursion to serve deeper routes and/or multiple classes
* [x] Implement nesting / recursion to serve deeper routes and/or multiple classes
* [x] support default navigation pages to help show links to available routes
* [ ] Enforce OpenAPI spec with better error responses
* [x] Serve static folders
* [x] Make a better playground for testing endpoints
Expand All @@ -127,7 +170,16 @@ Add a custom function to handle any requests that don't match any other routes.
* [ ] document fallback handler
* [ ] document regexp / match routes
* [ ] Make a client-side python proxy object to make API requests from python

### Environment Support
* [x] Remove `|` typehints to allow for older python versions :/ (this makes me sad)
* [x] Remove standard library dependencies which microcontrollers may not have
* [x] Allow passing in a socket object
* [ ] test and support different kinds of sockets and objects pretending to be sockets
* [ ] Test on ESP32 and other microcontrollers
* [ ] Test in browser-based python environments using pyodide

### Other
* [ ] Ideas? Let me know!

# Other Usage Modes
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "socketwrench"
version = "1.9.3"
version = "2.0.0"
description = "A simple Python library for creating web servers and APIs using sockets, supporting openapi and swagger."
readme = "README.md"
authors = [{ name = "Torin Halsted", email = "modularizer@gmail.com" }]
Expand Down
13 changes: 9 additions & 4 deletions src/simplesocketwrench.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import socket
import logging

try:
from logging import Logger
except ImportError:
class Logger:
def info(self, *args):
print(*args)


class Server:
def __init__(self, port: int = 8080, host: str = '', logger: logging.Logger | str | None = None):
def __init__(self, port: int = 8080, host: str = '', logger = None):
self.host = host
self.port = port
self._log = logger if isinstance(logger, logging.Logger) else logging.getLogger(logger if logger else self.__class__.__name__)
self._log = logger or Logger()

self.server_socket = self.create_socket()

Expand Down Expand Up @@ -52,5 +58,4 @@ def send_response(self, client_connection: socket.socket, response: str):


if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG)
Server().serve()
72 changes: 34 additions & 38 deletions src/socketwrench/__init__.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,34 @@
from .server import Server
from .handlers import RouteHandler, StaticFileHandler, MatchableHandlerABC
from .types import (
Request,
Response,
HTTPStatusCode,
HTMLResponse,
JSONResponse,
ErrorResponse,
FileResponse,
FileTypeResponse,
RedirectResponse,
TemporaryRedirect,
PermanentRedirect,
RequestBody,
Query,
Body,
Route,
FullPath,
Method,
File,
ClientAddr,
Headers,
set_default_error_mode,
url_encode,
url_decode
)
from .tags import (
tag,
methods,
get,
post,
put,
patch,
delete
)

serve = Server.serve

def _spoof_modules(which="all"):
from socketwrench.settings import config
config["spoof_modules"] = which

def set_socket_module(module):
from socketwrench.settings import config
if not hasattr(module, "socket"):
raise ValueError("socket module must have a 'socket' attribute. expecting a module like that from 'import socket'")
config["socket_module"] = module


class _unspecified:
pass


def serve(*args, spoof_modules=_unspecified, socket=_unspecified, log_level=None, **kwargs):
if spoof_modules is not _unspecified:
_spoof_modules(spoof_modules)
if socket is not _unspecified:
set_socket_module(socket)
import socketwrench.public
if log_level is not None:
from socketwrench.standardlib_dependencies import logging
logging.basicConfig(level=log_level)
return socketwrench.public.serve(*args, **kwargs)


def __getattr__(name):
# import from public
if name == "_spoof_modules":
return _spoof_modules
import socketwrench.public
return getattr(socketwrench.public, name)
12 changes: 6 additions & 6 deletions src/socketwrench/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@


def main():
import argparse
parser = argparse.ArgumentParser(description="Serve a module or class.")
from socketwrench.standardlib_dependencies import ArgumentParser
parser = ArgumentParser(description="Serve a module or class.")
parser.add_argument("module_or_class", help="The module or class to serve.")
# add help text for the other arguments
parser.add_argument("--host", help="The host to bind to.", default="*", type=str)
Expand All @@ -15,13 +15,13 @@ def main():


if __name__ == '__main__':
import sys
if len(sys.argv) != 2:
from socketwrench.standardlib_dependencies import argv
if len(argv) != 2:
print("Usage: python -m socketwrench <import.path.of.module.or.class>")
print("Example: python -m socketwrench socketwrench.samples.sample.Sample")
print(f"Sample Shortcut: python -m socketwrench sample")
sys.exit(1)
m = sys.argv[1]
exit(1)
m = argv[1]
if m == "sample":
m = "socketwrench.samples.sample.Sample"
Server.serve(m)
16 changes: 12 additions & 4 deletions src/socketwrench/connection.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import socket
import logging
from socketwrench.standardlib_dependencies import (
logging,
socket,
)

from socketwrench.types import Request, Response

Expand All @@ -15,12 +17,14 @@ def __init__(self,
connection_socket: socket.socket,
client_address: tuple,
cleanup_event,
chunk_size: int = default_chunk_size):
chunk_size: int = default_chunk_size,
origin: str = ""):
self.socket = connection_socket
self.client_addr = client_address
self.chunk_size = chunk_size
self.cleanup_event = cleanup_event
self.handler = handler
self.origin = origin

self._rep = None

Expand All @@ -29,12 +33,15 @@ def handle(self):
request = self.receive_request(self.socket)
if self.check_cleanup():
return request, None, False
logger.debug(str(request))
response = self.handler(request)
logger.log(9, f"\t\t{response}")
if self.check_cleanup():
return request, response, False
self.send_response(self.socket, response)
return request, response, True
except Exception as e:
logger.error(f"Error handling request: {e}")
self.close()
raise e

Expand Down Expand Up @@ -66,7 +73,7 @@ def receive_request(self, connection_socket: socket.socket, chunk_size: int = No
else:
body = b''

r = Request.from_components(pre_body_bytes, body, self.client_addr, self.socket)
r = Request.from_components(pre_body_bytes, body, self.client_addr, self.socket, origin=self.origin)
return r

def send_response(self, connection_socket: socket.socket, response: Response):
Expand All @@ -92,3 +99,4 @@ def __repr__(self):

self._rep = f'<{self.__class__.__name__}({self.socket}, {self.client_addr}, {self.cleanup_event}{r})>'
return self._rep

Loading

0 comments on commit cfc2db0

Please sign in to comment.