Compare commits
6 commits
main
...
file-uploa
Author | SHA1 | Date | |
---|---|---|---|
b1c2dc9ce0 | |||
87b9d6048d | |||
e2ebdd45ba | |||
98e1ecdf13 | |||
b5acd932b7 | |||
cf1a248538 |
13 changed files with 241 additions and 36 deletions
21
example.py
21
example.py
|
@ -29,6 +29,7 @@ app = SpiderwebRouter(
|
|||
append_slash=False, # default
|
||||
cors_allow_all_origins=True,
|
||||
static_url="static_stuff",
|
||||
media_dir="media",
|
||||
debug=True,
|
||||
case_transform_middleware_type="spongebob",
|
||||
)
|
||||
|
@ -71,6 +72,26 @@ def example(request, 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)
|
||||
def http405(request) -> HttpResponse:
|
||||
return HttpResponse(body="Method not allowed", status_code=405)
|
||||
|
|
74
poetry.lock
generated
74
poetry.lock
generated
|
@ -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]]
|
||||
name = "annotated-types"
|
||||
|
@ -6,6 +6,7 @@ version = "0.7.0"
|
|||
description = "Reusable constraint types to use with typing.Annotated"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
|
||||
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
|
||||
|
@ -17,18 +18,19 @@ version = "24.2.0"
|
|||
description = "Classes Without Boilerplate"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"},
|
||||
{file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "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]"]
|
||||
dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "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 ; 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 ; 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)"]
|
||||
tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
||||
tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
|
||||
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) ; 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]]
|
||||
name = "black"
|
||||
|
@ -36,6 +38,7 @@ version = "24.8.0"
|
|||
description = "The uncompromising code formatter."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
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_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"},
|
||||
|
@ -70,7 +73,7 @@ platformdirs = ">=2"
|
|||
|
||||
[package.extras]
|
||||
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)"]
|
||||
uvloop = ["uvloop (>=0.15.2)"]
|
||||
|
||||
|
@ -80,6 +83,8 @@ version = "1.17.1"
|
|||
description = "Foreign Function Interface for Python calling C code."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "platform_python_implementation != \"PyPy\""
|
||||
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_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
|
||||
|
@ -159,6 +164,7 @@ version = "8.1.7"
|
|||
description = "Composable command line interface toolkit"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
|
||||
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
|
||||
|
@ -173,6 +179,8 @@ version = "0.4.6"
|
|||
description = "Cross-platform colored terminal text."
|
||||
optional = false
|
||||
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 = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
|
@ -184,6 +192,7 @@ version = "7.6.1"
|
|||
description = "Code coverage measurement for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
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_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"},
|
||||
|
@ -260,7 +269,7 @@ files = [
|
|||
]
|
||||
|
||||
[package.extras]
|
||||
toml = ["tomli"]
|
||||
toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
|
@ -268,6 +277,7 @@ version = "43.0.1"
|
|||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{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"},
|
||||
|
@ -317,6 +327,7 @@ version = "2.6.1"
|
|||
description = "DNS toolkit"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"},
|
||||
{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."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"},
|
||||
{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"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"},
|
||||
{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"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "hypothesis-6.112.1-py3-none-any.whl", hash = "sha256:93631b1498b20d2c205ed304cbd41d50e9c069d78a9c773c1324ca094c5e30ce"},
|
||||
{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"
|
||||
|
||||
[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)"]
|
||||
codemods = ["libcst (>=0.3.16)"]
|
||||
crosshair = ["crosshair-tool (>=0.0.70)", "hypothesis-crosshair (>=0.0.13)"]
|
||||
|
@ -397,7 +411,7 @@ pandas = ["pandas (>=1.1)"]
|
|||
pytest = ["pytest (>=4.6)"]
|
||||
pytz = ["pytz (>=2014.1)"]
|
||||
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]]
|
||||
name = "idna"
|
||||
|
@ -405,6 +419,7 @@ version = "3.10"
|
|||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
|
||||
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
|
||||
|
@ -419,6 +434,7 @@ version = "2.0.0"
|
|||
description = "brain-dead simple config-ini parsing"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||
{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."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
|
||||
{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."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
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_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"},
|
||||
|
@ -510,12 +528,29 @@ files = [
|
|||
{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]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.0.0"
|
||||
description = "Type system extensions for programs checked with the mypy type checker."
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
||||
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
||||
|
@ -527,6 +562,7 @@ version = "24.1"
|
|||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
|
||||
{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."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
|
||||
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
|
||||
|
@ -549,6 +586,7 @@ version = "3.17.6"
|
|||
description = "a little orm"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{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`."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
|
||||
{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"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
|
||||
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
|
||||
|
@ -590,6 +630,8 @@ version = "2.22"
|
|||
description = "C parser in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "platform_python_implementation != \"PyPy\""
|
||||
files = [
|
||||
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
|
||||
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
|
||||
|
@ -601,6 +643,7 @@ version = "2.9.2"
|
|||
description = "Data validation using Python type hints"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"},
|
||||
{file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"},
|
||||
|
@ -616,7 +659,7 @@ typing-extensions = [
|
|||
|
||||
[package.extras]
|
||||
email = ["email-validator (>=2.0.0)"]
|
||||
timezone = ["tzdata"]
|
||||
timezone = ["tzdata ; python_version >= \"3.9\" and sys_platform == \"win32\""]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
|
@ -624,6 +667,7 @@ version = "2.23.4"
|
|||
description = "Core functionality for Pydantic validation and serialization"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
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_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"},
|
||||
|
@ -725,6 +769,7 @@ version = "8.3.3"
|
|||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"},
|
||||
{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."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{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"},
|
||||
|
@ -772,6 +818,7 @@ version = "2.4.0"
|
|||
description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"},
|
||||
{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+"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
|
||||
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
||||
]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "17f5dc4b157da57ad75a6f6aa3feb7adfa07500b805d5e79d1f09d640964949f"
|
||||
content-hash = "81a82cbbeda308234dd260a0d9edfd0e4e6264e2f40eb908049e2461c1438eaa"
|
||||
|
|
|
@ -35,6 +35,7 @@ jinja2 = "^3.1.4"
|
|||
cryptography = "^43.0.0"
|
||||
email-validator = "^2.2.0"
|
||||
pydantic = "^2.8.2"
|
||||
multipart = "^1.2.1"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "^0.5.5"
|
||||
|
|
49
spiderweb/files.py
Normal file
49
spiderweb/files.py
Normal 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)
|
|
@ -3,8 +3,8 @@ import logging
|
|||
import pathlib
|
||||
import re
|
||||
import traceback
|
||||
import urllib.parse as urlparse
|
||||
from logging import Logger
|
||||
from pathlib import Path
|
||||
from threading import Thread
|
||||
from typing import Optional, Callable, Sequence, Literal
|
||||
from wsgiref.simple_server import WSGIServer
|
||||
|
@ -74,8 +74,10 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi
|
|||
templates_dirs: Sequence[str] = None,
|
||||
middleware: Sequence[str] = None,
|
||||
append_slash: bool = False,
|
||||
staticfiles_dirs: Sequence[str] = None,
|
||||
staticfiles_dirs: Sequence[str | Path] = None,
|
||||
static_url: str = "static",
|
||||
media_dir: str | Path = None,
|
||||
media_url: str = "media",
|
||||
routes: Sequence[tuple[str, Callable] | tuple[str, Callable, dict]] = None,
|
||||
error_routes: dict[int, Callable] = None,
|
||||
secret_key: str = None,
|
||||
|
@ -96,9 +98,12 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi
|
|||
self.port = port if port else 8000
|
||||
self.server_address = (self.addr, self.port)
|
||||
self.append_slash = append_slash
|
||||
self.fix_route_starting_slash = True
|
||||
self.templates_dirs = templates_dirs
|
||||
self.staticfiles_dirs = staticfiles_dirs
|
||||
self.media_dir = media_dir
|
||||
self.static_url = static_url
|
||||
self.media_url = media_url
|
||||
self._middleware: list[str] = middleware or []
|
||||
self.middleware: list[Callable] = []
|
||||
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."
|
||||
)
|
||||
|
||||
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.
|
||||
self.log.info("Run startup checks...")
|
||||
self.run_middleware_checks()
|
||||
|
@ -283,6 +303,7 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi
|
|||
return resp
|
||||
except ConnectionAbortedError as 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]:
|
||||
try:
|
||||
|
@ -303,7 +324,7 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi
|
|||
|
||||
except Exception:
|
||||
self.log.error(traceback.format_exc())
|
||||
self.fire_response(
|
||||
return self.fire_response(
|
||||
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):
|
||||
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:
|
||||
if handler:
|
||||
abort_view = self.process_request_middleware(request)
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
import json
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
from spiderweb.constants import DEFAULT_ENCODING
|
||||
from spiderweb.files import MediaFile
|
||||
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:
|
||||
def __init__(
|
||||
|
@ -24,8 +32,9 @@ class Request:
|
|||
self.query_params = []
|
||||
self.server = server
|
||||
self.handler = handler # the view function that will be called
|
||||
self.GET = {}
|
||||
self.POST = {}
|
||||
self.GET = MultiDict()
|
||||
self.POST = MultiDict()
|
||||
self.FILES = MultiDict()
|
||||
self.META = {}
|
||||
self.COOKIES = {}
|
||||
# only used for the session middleware
|
||||
|
@ -39,10 +48,21 @@ class Request:
|
|||
self.populate_cookies()
|
||||
|
||||
content_length = int(self.headers.get("content_length") or 0)
|
||||
if content_length:
|
||||
self.content = (
|
||||
self.environ["wsgi.input"].read(content_length).decode(DEFAULT_ENCODING)
|
||||
)
|
||||
if self.is_form_request():
|
||||
if self.method == "POST":
|
||||
# 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:
|
||||
data = self.headers
|
||||
|
@ -90,7 +110,4 @@ class Request:
|
|||
return json.loads(self.content)
|
||||
|
||||
def is_form_request(self) -> bool:
|
||||
return (
|
||||
"content_type" in self.headers
|
||||
and self.headers["content_type"] == "application/x-www-form-urlencoded"
|
||||
)
|
||||
return m_is_form_request(self.environ)
|
||||
|
|
|
@ -12,6 +12,8 @@ from spiderweb.exceptions import GeneralException
|
|||
from spiderweb.request import Request
|
||||
from spiderweb.utils import Headers
|
||||
|
||||
from multipart import MultiDict
|
||||
|
||||
|
||||
mimetypes.init()
|
||||
|
||||
|
@ -122,6 +124,8 @@ class JsonResponse(HttpResponse):
|
|||
self.headers["content-type"] = "application/json"
|
||||
|
||||
def render(self) -> str:
|
||||
if isinstance(self.data, MultiDict):
|
||||
self.data = self.data.dict
|
||||
return json.dumps(self.data)
|
||||
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ class RoutesMixin:
|
|||
_error_routes: dict
|
||||
error_routes: dict[int, Callable]
|
||||
append_slash: bool
|
||||
fix_route_starting_slash: bool
|
||||
|
||||
def route(self, path, allowed_methods=None, name=None) -> Callable:
|
||||
"""
|
||||
|
@ -134,7 +135,8 @@ class RoutesMixin:
|
|||
or 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
|
||||
|
||||
def get_packet(func):
|
||||
|
|
|
@ -3,6 +3,7 @@ from wsgiref.util import setup_testing_defaults
|
|||
from peewee import SqliteDatabase
|
||||
|
||||
from spiderweb import SpiderwebRouter
|
||||
from spiderweb.request import Request
|
||||
|
||||
|
||||
class StartResponse:
|
||||
|
@ -28,3 +29,34 @@ def setup(**kwargs):
|
|||
environ,
|
||||
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,
|
||||
)
|
||||
|
|
3
spiderweb/tests/test_files.py
Normal file
3
spiderweb/tests/test_files.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from io import BytesIO
|
||||
|
||||
...
|
|
@ -300,7 +300,7 @@ def test_csrf_trusted_origins():
|
|||
|
||||
environ["HTTP_ORIGIN"] = "example.com"
|
||||
resp2 = app(environ, start_response)[0].decode(DEFAULT_ENCODING)
|
||||
assert resp2 == '{"name": "bob"}'
|
||||
assert resp2 == '{"name": ["bob"]}'
|
||||
|
||||
|
||||
def test_post_process_middleware():
|
||||
|
|
|
@ -36,6 +36,7 @@ def get_http_status_by_code(code: int) -> Optional[str]:
|
|||
resp = HTTPStatus(code)
|
||||
if resp:
|
||||
return f"{resp.value} {resp.phrase}"
|
||||
return None
|
||||
|
||||
|
||||
def is_form_request(request: "Request") -> bool:
|
||||
|
|
13
templates/file_upload.html
Normal file
13
templates/file_upload.html
Normal 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 %}
|
Loading…
Add table
Reference in a new issue