Compare commits

...
Sign in to create a new pull request.

6 commits

13 changed files with 241 additions and 36 deletions

View file

@ -29,6 +29,7 @@ app = SpiderwebRouter(
append_slash=False, # default append_slash=False, # default
cors_allow_all_origins=True, cors_allow_all_origins=True,
static_url="static_stuff", static_url="static_stuff",
media_dir="media",
debug=True, debug=True,
case_transform_middleware_type="spongebob", case_transform_middleware_type="spongebob",
) )
@ -71,6 +72,26 @@ def example(request, id):
return HttpResponse(body=f"Example with id {id}") return HttpResponse(body=f"Example with id {id}")
@app.route("file_upload/")
def file_upload(request):
if request.method == "POST":
if "file" not in request.FILES:
return HttpResponse(body="No file uploaded", status_code=400)
file = request.FILES["file"]
content = file.read()
filepath = file.save() # Save the file to the media directory
try:
return HttpResponse(body=f"File content: {content.decode('utf-8')}")
except UnicodeDecodeError:
return HttpResponse(
body=f"The file has been uploaded, but it is not a text file."
f" Saved to {filepath}",
status_code=400,
)
else:
return TemplateResponse(request, "file_upload.html")
@app.error(405) @app.error(405)
def http405(request) -> HttpResponse: def http405(request) -> HttpResponse:
return HttpResponse(body="Method not allowed", status_code=405) return HttpResponse(body="Method not allowed", status_code=405)

74
poetry.lock generated
View file

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. # This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
[[package]] [[package]]
name = "annotated-types" name = "annotated-types"
@ -6,6 +6,7 @@ version = "0.7.0"
description = "Reusable constraint types to use with typing.Annotated" description = "Reusable constraint types to use with typing.Annotated"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["main"]
files = [ files = [
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
@ -17,18 +18,19 @@ version = "24.2.0"
description = "Classes Without Boilerplate" description = "Classes Without Boilerplate"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
groups = ["dev"]
files = [ files = [
{file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"},
{file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"},
] ]
[package.extras] [package.extras]
benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\"", "pytest-xdist[psutil]"]
cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\"", "pytest-xdist[psutil]"]
dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\"", "pytest-xdist[psutil]"]
docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\"", "pytest-xdist[psutil]"]
tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.9\" and python_version < \"3.13\""]
[[package]] [[package]]
name = "black" name = "black"
@ -36,6 +38,7 @@ version = "24.8.0"
description = "The uncompromising code formatter." description = "The uncompromising code formatter."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["dev"]
files = [ files = [
{file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"},
{file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"},
@ -70,7 +73,7 @@ platformdirs = ">=2"
[package.extras] [package.extras]
colorama = ["colorama (>=0.4.3)"] colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] d = ["aiohttp (>=3.7.4) ; sys_platform != \"win32\" or implementation_name != \"pypy\"", "aiohttp (>=3.7.4,!=3.9.0) ; sys_platform == \"win32\" and implementation_name == \"pypy\""]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"] uvloop = ["uvloop (>=0.15.2)"]
@ -80,6 +83,8 @@ version = "1.17.1"
description = "Foreign Function Interface for Python calling C code." description = "Foreign Function Interface for Python calling C code."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["main"]
markers = "platform_python_implementation != \"PyPy\""
files = [ files = [
{file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
{file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
@ -159,6 +164,7 @@ version = "8.1.7"
description = "Composable command line interface toolkit" description = "Composable command line interface toolkit"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
groups = ["dev"]
files = [ files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
@ -173,6 +179,8 @@ version = "0.4.6"
description = "Cross-platform colored terminal text." description = "Cross-platform colored terminal text."
optional = false optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["dev"]
markers = "sys_platform == \"win32\" or platform_system == \"Windows\""
files = [ files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
@ -184,6 +192,7 @@ version = "7.6.1"
description = "Code coverage measurement for Python" description = "Code coverage measurement for Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["dev"]
files = [ files = [
{file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"},
{file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"},
@ -260,7 +269,7 @@ files = [
] ]
[package.extras] [package.extras]
toml = ["tomli"] toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
[[package]] [[package]]
name = "cryptography" name = "cryptography"
@ -268,6 +277,7 @@ version = "43.0.1"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
groups = ["main"]
files = [ files = [
{file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"}, {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"},
{file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"}, {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"},
@ -317,6 +327,7 @@ version = "2.6.1"
description = "DNS toolkit" description = "DNS toolkit"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["main"]
files = [ files = [
{file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"},
{file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"},
@ -337,6 +348,7 @@ version = "2.2.0"
description = "A robust email address syntax and deliverability validation library." description = "A robust email address syntax and deliverability validation library."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["main"]
files = [ files = [
{file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"},
{file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"},
@ -352,6 +364,7 @@ version = "23.0.0"
description = "WSGI HTTP Server for UNIX" description = "WSGI HTTP Server for UNIX"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
groups = ["dev"]
files = [ files = [
{file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"},
{file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"},
@ -373,6 +386,7 @@ version = "6.112.1"
description = "A library for property-based testing" description = "A library for property-based testing"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["dev"]
files = [ files = [
{file = "hypothesis-6.112.1-py3-none-any.whl", hash = "sha256:93631b1498b20d2c205ed304cbd41d50e9c069d78a9c773c1324ca094c5e30ce"}, {file = "hypothesis-6.112.1-py3-none-any.whl", hash = "sha256:93631b1498b20d2c205ed304cbd41d50e9c069d78a9c773c1324ca094c5e30ce"},
{file = "hypothesis-6.112.1.tar.gz", hash = "sha256:b070d7a1bb9bd84706c31885c9aeddc138e2b36a9c112a91984f49501c567856"}, {file = "hypothesis-6.112.1.tar.gz", hash = "sha256:b070d7a1bb9bd84706c31885c9aeddc138e2b36a9c112a91984f49501c567856"},
@ -383,7 +397,7 @@ attrs = ">=22.2.0"
sortedcontainers = ">=2.1.0,<3.0.0" sortedcontainers = ">=2.1.0,<3.0.0"
[package.extras] [package.extras]
all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.70)", "django (>=3.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.13)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.17.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2024.1)"] all = ["backports.zoneinfo (>=0.2.1) ; python_version < \"3.9\"", "black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.70)", "django (>=3.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.13)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.17.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2024.1) ; sys_platform == \"win32\" or sys_platform == \"emscripten\""]
cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"] cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"]
codemods = ["libcst (>=0.3.16)"] codemods = ["libcst (>=0.3.16)"]
crosshair = ["crosshair-tool (>=0.0.70)", "hypothesis-crosshair (>=0.0.13)"] crosshair = ["crosshair-tool (>=0.0.70)", "hypothesis-crosshair (>=0.0.13)"]
@ -397,7 +411,7 @@ pandas = ["pandas (>=1.1)"]
pytest = ["pytest (>=4.6)"] pytest = ["pytest (>=4.6)"]
pytz = ["pytz (>=2014.1)"] pytz = ["pytz (>=2014.1)"]
redis = ["redis (>=3.0.0)"] redis = ["redis (>=3.0.0)"]
zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2024.1)"] zoneinfo = ["backports.zoneinfo (>=0.2.1) ; python_version < \"3.9\"", "tzdata (>=2024.1) ; sys_platform == \"win32\" or sys_platform == \"emscripten\""]
[[package]] [[package]]
name = "idna" name = "idna"
@ -405,6 +419,7 @@ version = "3.10"
description = "Internationalized Domain Names in Applications (IDNA)" description = "Internationalized Domain Names in Applications (IDNA)"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
groups = ["main"]
files = [ files = [
{file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
@ -419,6 +434,7 @@ version = "2.0.0"
description = "brain-dead simple config-ini parsing" description = "brain-dead simple config-ini parsing"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
groups = ["dev"]
files = [ files = [
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
@ -430,6 +446,7 @@ version = "3.1.4"
description = "A very fast and expressive template engine." description = "A very fast and expressive template engine."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
groups = ["main"]
files = [ files = [
{file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
{file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
@ -447,6 +464,7 @@ version = "2.1.5"
description = "Safely add untrusted strings to HTML/XML markup." description = "Safely add untrusted strings to HTML/XML markup."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
groups = ["main"]
files = [ files = [
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"},
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"},
@ -510,12 +528,29 @@ files = [
{file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"},
] ]
[[package]]
name = "multipart"
version = "1.2.1"
description = "Parser for multipart/form-data"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "multipart-1.2.1-py3-none-any.whl", hash = "sha256:c03dc203bc2e67f6b46a599467ae0d87cf71d7530504b2c1ff4a9ea21d8b8c8c"},
{file = "multipart-1.2.1.tar.gz", hash = "sha256:829b909b67bc1ad1c6d4488fcdc6391c2847842b08323addf5200db88dbe9480"},
]
[package.extras]
dev = ["build", "pytest", "pytest-cov", "twine"]
docs = ["sphinx (>=8,<9)", "sphinx-autobuild"]
[[package]] [[package]]
name = "mypy-extensions" name = "mypy-extensions"
version = "1.0.0" version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker." description = "Type system extensions for programs checked with the mypy type checker."
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
groups = ["dev"]
files = [ files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
@ -527,6 +562,7 @@ version = "24.1"
description = "Core utilities for Python packages" description = "Core utilities for Python packages"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["dev"]
files = [ files = [
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
@ -538,6 +574,7 @@ version = "0.12.1"
description = "Utility library for gitignore style pattern matching of file paths." description = "Utility library for gitignore style pattern matching of file paths."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["dev"]
files = [ files = [
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
@ -549,6 +586,7 @@ version = "3.17.6"
description = "a little orm" description = "a little orm"
optional = false optional = false
python-versions = "*" python-versions = "*"
groups = ["main"]
files = [ files = [
{file = "peewee-3.17.6.tar.gz", hash = "sha256:cea5592c6f4da1592b7cff8eaf655be6648a1f5857469e30037bf920c03fb8fb"}, {file = "peewee-3.17.6.tar.gz", hash = "sha256:cea5592c6f4da1592b7cff8eaf655be6648a1f5857469e30037bf920c03fb8fb"},
] ]
@ -559,6 +597,7 @@ version = "4.3.6"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["dev"]
files = [ files = [
{file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
{file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
@ -575,6 +614,7 @@ version = "1.5.0"
description = "plugin and hook calling mechanisms for python" description = "plugin and hook calling mechanisms for python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["dev"]
files = [ files = [
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
@ -590,6 +630,8 @@ version = "2.22"
description = "C parser in Python" description = "C parser in Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["main"]
markers = "platform_python_implementation != \"PyPy\""
files = [ files = [
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
@ -601,6 +643,7 @@ version = "2.9.2"
description = "Data validation using Python type hints" description = "Data validation using Python type hints"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["main"]
files = [ files = [
{file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"},
{file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"},
@ -616,7 +659,7 @@ typing-extensions = [
[package.extras] [package.extras]
email = ["email-validator (>=2.0.0)"] email = ["email-validator (>=2.0.0)"]
timezone = ["tzdata"] timezone = ["tzdata ; python_version >= \"3.9\" and sys_platform == \"win32\""]
[[package]] [[package]]
name = "pydantic-core" name = "pydantic-core"
@ -624,6 +667,7 @@ version = "2.23.4"
description = "Core functionality for Pydantic validation and serialization" description = "Core functionality for Pydantic validation and serialization"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["main"]
files = [ files = [
{file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"},
{file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"},
@ -725,6 +769,7 @@ version = "8.3.3"
description = "pytest: simple powerful testing with Python" description = "pytest: simple powerful testing with Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["dev"]
files = [ files = [
{file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"},
{file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"},
@ -745,6 +790,7 @@ version = "0.5.7"
description = "An extremely fast Python linter and code formatter, written in Rust." description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
groups = ["dev"]
files = [ files = [
{file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"}, {file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"},
{file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"}, {file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"},
@ -772,6 +818,7 @@ version = "2.4.0"
description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
optional = false optional = false
python-versions = "*" python-versions = "*"
groups = ["dev"]
files = [ files = [
{file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"},
{file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"},
@ -783,12 +830,13 @@ version = "4.12.2"
description = "Backported and Experimental Type Hints for Python 3.8+" description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["main"]
files = [ files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
] ]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.1"
python-versions = "^3.11" python-versions = "^3.11"
content-hash = "17f5dc4b157da57ad75a6f6aa3feb7adfa07500b805d5e79d1f09d640964949f" content-hash = "81a82cbbeda308234dd260a0d9edfd0e4e6264e2f40eb908049e2461c1438eaa"

View file

@ -35,6 +35,7 @@ jinja2 = "^3.1.4"
cryptography = "^43.0.0" cryptography = "^43.0.0"
email-validator = "^2.2.0" email-validator = "^2.2.0"
pydantic = "^2.8.2" pydantic = "^2.8.2"
multipart = "^1.2.1"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
ruff = "^0.5.5" ruff = "^0.5.5"

49
spiderweb/files.py Normal file
View file

@ -0,0 +1,49 @@
import random
import string
from multipart import MultipartPart
class MediaFile:
# This class acts as a sort of container for uploaded files.
# Rather than trying to subclass the MultipartPart class and deal with
# the complexities of multipart parsing, we just use this class to
# add the save functionality for the media folder. Also makes the most
# common attributes available directly on the instance.
def __init__(self, server, multipart_part: MultipartPart):
self._file: MultipartPart = multipart_part
self.filename: str = self._file.filename
self.content_type: str = self._file.content_type
self.server = server
#: Part size in bytes.
self.size = self._file.size
#: Part name.
self.name = self._file.name
#: Charset as defined in the part header, or the parser default charset.
self.charset = self._file.charset
#: All part headers as a list of (name, value) pairs.
self.headerlist = self._file.headerlist
self.memfile_limit = self._file.memfile_limit
self.buffer_size = self._file.buffer_size
def get_random_suffix(self) -> str:
"""Generate a random 6 character suffix."""
return "".join(random.choices(string.ascii_letters, k=6))
def save(self):
file_path = self.server.BASE_DIR / self.server.media_dir / self._file.filename
if file_path.exists():
# If the file already exists, append a random suffix to the filename
suffix = self.get_random_suffix()
file_path = file_path.with_name(
f"{file_path.stem}_[{suffix}]{file_path.suffix}"
)
self._file.save_as(file_path)
return file_path
def read(self):
return self._file.file.read()
def seek(self, offset: int, whence: int = 0):
return self._file.file.seek(offset, whence)

View file

@ -3,8 +3,8 @@ import logging
import pathlib import pathlib
import re import re
import traceback import traceback
import urllib.parse as urlparse
from logging import Logger from logging import Logger
from pathlib import Path
from threading import Thread from threading import Thread
from typing import Optional, Callable, Sequence, Literal from typing import Optional, Callable, Sequence, Literal
from wsgiref.simple_server import WSGIServer from wsgiref.simple_server import WSGIServer
@ -74,8 +74,10 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi
templates_dirs: Sequence[str] = None, templates_dirs: Sequence[str] = None,
middleware: Sequence[str] = None, middleware: Sequence[str] = None,
append_slash: bool = False, append_slash: bool = False,
staticfiles_dirs: Sequence[str] = None, staticfiles_dirs: Sequence[str | Path] = None,
static_url: str = "static", static_url: str = "static",
media_dir: str | Path = None,
media_url: str = "media",
routes: Sequence[tuple[str, Callable] | tuple[str, Callable, dict]] = None, routes: Sequence[tuple[str, Callable] | tuple[str, Callable, dict]] = None,
error_routes: dict[int, Callable] = None, error_routes: dict[int, Callable] = None,
secret_key: str = None, secret_key: str = None,
@ -96,9 +98,12 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi
self.port = port if port else 8000 self.port = port if port else 8000
self.server_address = (self.addr, self.port) self.server_address = (self.addr, self.port)
self.append_slash = append_slash self.append_slash = append_slash
self.fix_route_starting_slash = True
self.templates_dirs = templates_dirs self.templates_dirs = templates_dirs
self.staticfiles_dirs = staticfiles_dirs self.staticfiles_dirs = staticfiles_dirs
self.media_dir = media_dir
self.static_url = static_url self.static_url = static_url
self.media_url = media_url
self._middleware: list[str] = middleware or [] self._middleware: list[str] = middleware or []
self.middleware: list[Callable] = [] self.middleware: list[Callable] = []
self.secret_key = secret_key if secret_key else self.generate_key() self.secret_key = secret_key if secret_key else self.generate_key()
@ -200,6 +205,21 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi
" files will not be served." " files will not be served."
) )
if self.media_dir:
self.media_dir = pathlib.Path(self.media_dir)
if not pathlib.Path(self.BASE_DIR / self.media_dir).exists():
self.log.error(
f"Media directory '{str(self.media_dir)}' does not exist."
)
raise ConfigError
if self.debug:
self.add_route(rf"/{self.media_url}/<path:filename>", send_file)
else:
self.log.warning(
"`media_dir` is set, but `debug` is set to FALSE."
" Media files will not be served."
)
# finally, run the startup checks to verify everything is correct and happy. # finally, run the startup checks to verify everything is correct and happy.
self.log.info("Run startup checks...") self.log.info("Run startup checks...")
self.run_middleware_checks() self.run_middleware_checks()
@ -283,6 +303,7 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi
return resp return resp
except ConnectionAbortedError as e: except ConnectionAbortedError as e:
self.log.error(f"{request.method} {request.path} : {e}") self.log.error(f"{request.method} {request.path} : {e}")
return HttpResponse(status_code=500)
def prepare_and_fire_response(self, start_response, request, resp) -> list[bytes]: def prepare_and_fire_response(self, start_response, request, resp) -> list[bytes]:
try: try:
@ -303,7 +324,7 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi
except Exception: except Exception:
self.log.error(traceback.format_exc()) self.log.error(traceback.format_exc())
self.fire_response( return self.fire_response(
start_response, request, self.get_error_route(500)(request) start_response, request, self.get_error_route(500)(request)
) )
@ -334,13 +355,6 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi
if not self.check_valid_host(request): if not self.check_valid_host(request):
handler = self.get_error_route(403) handler = self.get_error_route(403)
if request.is_form_request():
form_data = urlparse.parse_qs(request.content)
for key, value in form_data.items():
if len(value) == 1:
form_data[key] = value[0]
setattr(request, request.method, form_data)
try: try:
if handler: if handler:
abort_view = self.process_request_middleware(request) abort_view = self.process_request_middleware(request)

View file

@ -1,9 +1,17 @@
import json import json
from urllib.parse import urlparse from urllib.parse import urlparse, parse_qs
from spiderweb.constants import DEFAULT_ENCODING from spiderweb.constants import DEFAULT_ENCODING
from spiderweb.files import MediaFile
from spiderweb.utils import get_client_address, Headers from spiderweb.utils import get_client_address, Headers
from multipart import (
parse_form_data,
is_form_request as m_is_form_request,
MultiDict,
MultipartPart,
)
class Request: class Request:
def __init__( def __init__(
@ -24,8 +32,9 @@ class Request:
self.query_params = [] self.query_params = []
self.server = server self.server = server
self.handler = handler # the view function that will be called self.handler = handler # the view function that will be called
self.GET = {} self.GET = MultiDict()
self.POST = {} self.POST = MultiDict()
self.FILES = MultiDict()
self.META = {} self.META = {}
self.COOKIES = {} self.COOKIES = {}
# only used for the session middleware # only used for the session middleware
@ -39,10 +48,21 @@ class Request:
self.populate_cookies() self.populate_cookies()
content_length = int(self.headers.get("content_length") or 0) content_length = int(self.headers.get("content_length") or 0)
if content_length: if self.is_form_request():
self.content = ( if self.method == "POST":
self.environ["wsgi.input"].read(content_length).decode(DEFAULT_ENCODING) # this pulls from wsgi.input, so we don't have to do it ourselves
) self.POST, self.FILES = parse_form_data(self.environ)
for key, value in self.FILES.items():
if isinstance(value, MultipartPart):
self.FILES[key] = MediaFile(self.server, value)
else:
if content_length:
self.content = (
self.environ["wsgi.input"]
.read(content_length)
.decode(DEFAULT_ENCODING)
)
self.GET.update(parse_qs(self.content))
def populate_headers(self) -> None: def populate_headers(self) -> None:
data = self.headers data = self.headers
@ -90,7 +110,4 @@ class Request:
return json.loads(self.content) return json.loads(self.content)
def is_form_request(self) -> bool: def is_form_request(self) -> bool:
return ( return m_is_form_request(self.environ)
"content_type" in self.headers
and self.headers["content_type"] == "application/x-www-form-urlencoded"
)

View file

@ -12,6 +12,8 @@ from spiderweb.exceptions import GeneralException
from spiderweb.request import Request from spiderweb.request import Request
from spiderweb.utils import Headers from spiderweb.utils import Headers
from multipart import MultiDict
mimetypes.init() mimetypes.init()
@ -122,6 +124,8 @@ class JsonResponse(HttpResponse):
self.headers["content-type"] = "application/json" self.headers["content-type"] = "application/json"
def render(self) -> str: def render(self) -> str:
if isinstance(self.data, MultiDict):
self.data = self.data.dict
return json.dumps(self.data) return json.dumps(self.data)

View file

@ -40,6 +40,7 @@ class RoutesMixin:
_error_routes: dict _error_routes: dict
error_routes: dict[int, Callable] error_routes: dict[int, Callable]
append_slash: bool append_slash: bool
fix_route_starting_slash: bool
def route(self, path, allowed_methods=None, name=None) -> Callable: def route(self, path, allowed_methods=None, name=None) -> Callable:
""" """
@ -134,7 +135,8 @@ class RoutesMixin:
or allowed_methods or allowed_methods
or DEFAULT_ALLOWED_METHODS or DEFAULT_ALLOWED_METHODS
) )
if not path.startswith("/") and self.fix_route_starting_slash:
path = "/" + path
reverse_path = re.sub(r"<(.*?):(.*?)>", r"{\2}", path) if "<" in path else path reverse_path = re.sub(r"<(.*?):(.*?)>", r"{\2}", path) if "<" in path else path
def get_packet(func): def get_packet(func):

View file

@ -3,6 +3,7 @@ from wsgiref.util import setup_testing_defaults
from peewee import SqliteDatabase from peewee import SqliteDatabase
from spiderweb import SpiderwebRouter from spiderweb import SpiderwebRouter
from spiderweb.request import Request
class StartResponse: class StartResponse:
@ -28,3 +29,34 @@ def setup(**kwargs):
environ, environ,
StartResponse(), StartResponse(),
) )
class TestClient:
def __init__(self, **kwargs):
self.app, self.environ, self.start_response = setup(**kwargs)
...
class RequestFactory:
@staticmethod
def create_request(
environ=None,
content=None,
headers=None,
path=None,
server=None,
handler=None,
):
if not environ:
environ = {}
setup_testing_defaults(environ)
environ["HTTP_USER_AGENT"] = "Mozilla/5.0 (testrequest)"
environ["REMOTE_ADDR"] = "1.1.1.1"
return Request(
environ=environ,
content=content,
headers=headers,
path=path,
server=server,
handler=handler,
)

View file

@ -0,0 +1,3 @@
from io import BytesIO
...

View file

@ -300,7 +300,7 @@ def test_csrf_trusted_origins():
environ["HTTP_ORIGIN"] = "example.com" environ["HTTP_ORIGIN"] = "example.com"
resp2 = app(environ, start_response)[0].decode(DEFAULT_ENCODING) resp2 = app(environ, start_response)[0].decode(DEFAULT_ENCODING)
assert resp2 == '{"name": "bob"}' assert resp2 == '{"name": ["bob"]}'
def test_post_process_middleware(): def test_post_process_middleware():

View file

@ -36,6 +36,7 @@ def get_http_status_by_code(code: int) -> Optional[str]:
resp = HTTPStatus(code) resp = HTTPStatus(code)
if resp: if resp:
return f"{resp.value} {resp.phrase}" return f"{resp.value} {resp.phrase}"
return None
def is_form_request(request: "Request") -> bool: def is_form_request(request: "Request") -> bool:

View file

@ -0,0 +1,13 @@
{% extends 'base.html' %}
{% block content %}
<form action="" method="post" enctype='multipart/form-data'>
<div class="mb-3">
<label for="formFile" class="form-label">Default file input example</label>
<input name="file" class="form-control" type="file" id="formFile">
</div>
{{ csrf_token }}
<button type="submit" class="btn btn-primary">Submit</button>
</form>
{% endblock %}