diff --git a/example.py b/example.py new file mode 100644 index 0000000..760a433 --- /dev/null +++ b/example.py @@ -0,0 +1,23 @@ +from spiderweb import WebServer +from spiderweb.response import HttpResponse, JsonResponse, TemplateResponse, RedirectResponse + +app = WebServer(templates_dirs=["templates"]) + + +@app.route("/") +def index(request): + return TemplateResponse(request, "test.html", context={"value": "TEST!"}) + + +@app.route("/redirect") +def redirect(request): + return RedirectResponse("/") + + +if __name__ == "__main__": + # app.add_route("/", index) + try: + app.start() + print("Currently serving on", app.uri()) + except KeyboardInterrupt: + app.stop() diff --git a/poetry.lock b/poetry.lock index 51cdfb4..93162f3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,63 @@ # This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +[[package]] +name = "black" +version = "24.8.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +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"}, + {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, + {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, + {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, + {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, + {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, + {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, + {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, + {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, + {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, + {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, + {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, + {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, + {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, + {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, + {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, + {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, + {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, + {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, + {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, + {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" @@ -22,6 +80,103 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "jinja2" +version = "3.1.4" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +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"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + +[[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" +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"}, +] + [[package]] name = "packaging" version = "24.1" @@ -33,6 +188,17 @@ files = [ {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + [[package]] name = "peewee" version = "3.17.6" @@ -43,6 +209,22 @@ files = [ {file = "peewee-3.17.6.tar.gz", hash = "sha256:cea5592c6f4da1592b7cff8eaf655be6648a1f5857469e30037bf920c03fb8fb"}, ] +[[package]] +name = "platformdirs" +version = "4.2.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] + [[package]] name = "pluggy" version = "1.5.0" @@ -108,4 +290,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "8c6e729478f063b2dfe51e54aae69b0577ea66599cf77201928dbdcc5fc3f688" +content-hash = "f7533dcd984fcec36a80d3523af159bc6ebe71edb3c315d8bfd01690d910fb76" diff --git a/pyproject.toml b/pyproject.toml index 2b7a5f8..04c4e42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,10 +8,12 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.11" peewee = "^3.17.6" +jinja2 = "^3.1.4" [tool.poetry.group.dev.dependencies] ruff = "^0.5.5" pytest = "^8.3.2" +black = "^24.8.0" [build-system] requires = ["poetry-core"] diff --git a/spiderweb/__init__.py b/spiderweb/__init__.py index e69de29..1efac13 100644 --- a/spiderweb/__init__.py +++ b/spiderweb/__init__.py @@ -0,0 +1 @@ +from spiderweb.main import route, WebServer # noqa: F401 diff --git a/spiderweb/converters.py b/spiderweb/converters.py index c346ef0..2b3cb92 100644 --- a/spiderweb/converters.py +++ b/spiderweb/converters.py @@ -28,4 +28,4 @@ class FloatConverter: return float(value) def to_url(self, value): - return str(value) \ No newline at end of file + return str(value) diff --git a/spiderweb/default_responses.py b/spiderweb/default_responses.py index b0ee819..5ab8466 100644 --- a/spiderweb/default_responses.py +++ b/spiderweb/default_responses.py @@ -6,7 +6,9 @@ def http403(request): def http404(request): - return JsonResponse(data={"error": f"Route {request.url} not found"}, status_code=404) + return JsonResponse( + data={"error": f"Route {request.url} not found"}, status_code=404 + ) def http500(request): diff --git a/spiderweb/exceptions.py b/spiderweb/exceptions.py index 90150d1..f1252eb 100644 --- a/spiderweb/exceptions.py +++ b/spiderweb/exceptions.py @@ -5,6 +5,7 @@ class SpiderwebException(Exception): class SpiderwebNetworkException(SpiderwebException): """Something has gone wrong with the network stack.""" + def __init__(self, code, msg=None, desc=None): self.code = code self.msg = msg diff --git a/spiderweb/main.py b/spiderweb/main.py index 296049d..34ae7a4 100644 --- a/spiderweb/main.py +++ b/spiderweb/main.py @@ -5,27 +5,37 @@ import json import re +import traceback from http.server import BaseHTTPRequestHandler, HTTPServer import urllib.parse as urlparse import threading import logging from typing import Callable, Any +from jinja2 import Environment, FileSystemLoader + from spiderweb.converters import * # noqa: F403 -from spiderweb.default_responses import http403, http404, http500 -from spiderweb.exceptions import APIError, ConfigError, ParseError, GeneralException, NoResponseError +from spiderweb.default_responses import http403, http404, http500 # noqa: F401 +from spiderweb.exceptions import ( + APIError, + ConfigError, + ParseError, + GeneralException, + NoResponseError, +) from spiderweb.request import Request -from spiderweb.response import HttpResponse, JsonResponse +from spiderweb.response import HttpResponse, JsonResponse, TemplateResponse log = logging.getLogger(__name__) -def api_route(path): +def route(path): def outer(func): if not hasattr(func, "_routes"): setattr(func, "_routes", []) func._routes += [path] return func + return outer @@ -64,24 +74,30 @@ def convert_match_to_dict(match: dict): class WebServer(HTTPServer): - def __init__(self, addr: str, port: int, custom_handler: Callable = None): + def __init__( + self, + addr: str = None, + port: int = None, + custom_handler: Callable = None, + templates_dirs: list[str] = None, + middleware: list[str] = None, + ): """ Create a new server on address, port. Port can be zero. - > from simple_rpc_server import WebServer, APIError, api_route + > from simple_rpc_server import WebServer, APIError, route Create your handlers by inheriting from WebServer and tagging them with - @api_route("/path"). Alternately, you can use the WebServer() directly + @route("/path"). Alternately, you can use the WebServer() directly by calling `add_handler("path", function)`. - - Raise network errors by raising `APIError(code, message, description=None)`. - - Return responses by simply returning a dict() or str() object. - - Parameter to handlers is a dict(). - - Query arguments are shoved into the dict via urllib.parse_qs. """ + addr = addr if addr else "localhost" + port = port if port else 7777 + self.templates_dirs = templates_dirs + if self.templates_dirs: + self.env = Environment(loader=FileSystemLoader(self.templates_dirs)) + else: + self.env = None server_address = (addr, port) self.__addr = addr @@ -90,21 +106,54 @@ class WebServer(HTTPServer): pass self.handler_class = custom_handler if custom_handler else HandlerClass + self.handler_class.env = self.env # routed methods map into handler for method in type(self).__dict__.values(): if hasattr(method, "_routes"): for route in method._routes: self.add_route(route, method) - try: super().__init__(server_address, self.handler_class) except OSError: raise GeneralException("Port already in use.") + def check_for_route_duplicates(self, path: str): + if convert_path(path) in self.handler_class._routes: + raise ConfigError(f"Route '{path}' already exists.") + def add_route(self, path: str, method: Callable): + """Add a route to the server.""" + if not hasattr(self.handler_class, "_routes"): + setattr(self.handler_class, "_routes", []) + self.check_for_route_duplicates(path) self.handler_class._routes[convert_path(path)] = method + def route(self, path) -> Callable: + """ + Decorator for adding a route to a view. + + Usage: + + app = WebServer() + + @app.route("/hello") + def index(request): + return HttpResponse(content="Hello, world!") + + :param path: str + :return: Callable + """ + + def outer(func): + if not hasattr(self.handler_class, "_routes"): + setattr(self.handler_class, "_routes", []) + self.check_for_route_duplicates(path) + self.handler_class._routes[convert_path(path)] = func + return func + + return outer + def port(self): """Return current port.""" return self.socket.getsockname()[1] @@ -113,9 +162,10 @@ class WebServer(HTTPServer): """Return current IP address.""" return self.socket.getsockname()[0] - def uri(self, path): + def uri(self, path=None): """Make a URI pointing at myself.""" - if path[0] == "/": + path = path if path else "" + if path.startswith("/"): path = path[1:] return "http://" + self.__addr + ":" + str(self.port()) + "/" + path @@ -130,7 +180,7 @@ class WebServer(HTTPServer): print("Stopping server!") return - def shutdown(self): + def stop(self): super().shutdown() self.socket.close() @@ -146,7 +196,7 @@ class RequestHandler(BaseHTTPRequestHandler): body="", method=self.command, headers=self.headers, - path=self.path + path=self.path, ) def do_GET(self): @@ -212,13 +262,17 @@ class RequestHandler(BaseHTTPRequestHandler): raise NoResponseError(f"View {handler} returned None.") if isinstance(resp, dict): self.fire_response(JsonResponse(data=resp)) + if isinstance(resp, TemplateResponse): + if hasattr(self, "env"): # injected from above + resp.set_template_loader(self.env) + self.fire_response(resp) except APIError: raise except ConnectionAbortedError as e: log.error(f"GET {self.path} : {e}") - except Exception as e: - log.error(e.__traceback__) - self.fire_response(self.get_error_route(500)(self, request)) + except Exception: + log.error(traceback.format_exc()) + self.fire_response(self.get_error_route(500)(request)) else: raise APIError(404) diff --git a/spiderweb/middleware.py b/spiderweb/middleware.py index 68a4586..97b4bbc 100644 --- a/spiderweb/middleware.py +++ b/spiderweb/middleware.py @@ -18,12 +18,15 @@ class SpiderwebMiddleware: and the response will be returned immediately. `process_response` will not be called. """ + def process_request(self, request: Request) -> HttpResponse | None: # example of a middleware that sets a flag on the request request.spiderweb = True - def process_response(self, request: Request, response: HttpResponse) -> HttpResponse | None: + def process_response( + self, request: Request, response: HttpResponse + ) -> HttpResponse | None: # example of a middleware that sets a header on the resp - if hasattr(request, 'spiderweb'): - response.headers['X-Spiderweb'] = 'true' + if hasattr(request, "spiderweb"): + response.headers["X-Spiderweb"] = "true" return response diff --git a/spiderweb/request.py b/spiderweb/request.py index 48390f9..15806f5 100644 --- a/spiderweb/request.py +++ b/spiderweb/request.py @@ -2,7 +2,16 @@ import json class Request: - def __init__(self, content=None, body=None, method=None, headers=None, path=None, url=None, query_params=None): + def __init__( + self, + content=None, + body=None, + method=None, + headers=None, + path=None, + url=None, + query_params=None, + ): self.content: str = content self.body: str = body self.method: str = method @@ -13,5 +22,3 @@ class Request: def json(self): return json.loads(self.content) - - diff --git a/spiderweb/response.py b/spiderweb/response.py index 1b36b6d..e4e701e 100644 --- a/spiderweb/response.py +++ b/spiderweb/response.py @@ -1,25 +1,36 @@ +import datetime import json from typing import Any +from spiderweb.exceptions import GeneralException +from spiderweb.request import Request + class HttpResponse: def __init__( - self, - content: str = None, - data: dict[str, Any] = None, - status_code: int = 200, - headers=None, + self, + body: str = None, + data: dict[str, Any] = None, + context: dict[str, Any] = None, + status_code: int = 200, + headers=None, ): - self.content = content + self.body = body self.data = data + self.context = context if context else {} self.status_code = status_code self.headers = headers if headers else {} + self.headers["Content-Type"] = "text/html; charset=utf-8" + self.headers["Server"] = "Spiderweb" + self.headers["Date"] = datetime.datetime.now(tz=datetime.UTC).strftime( + "%a, %d %b %Y %H:%M:%S GMT" + ) def __str__(self): - return self.content + return self.body def render(self) -> str: - raise NotImplemented + return str(self.body) class JsonResponse(HttpResponse): @@ -32,8 +43,27 @@ class JsonResponse(HttpResponse): class RedirectResponse(HttpResponse): - ... + def __init__(self, location: str, *args, **kwargs): + super().__init__(*args, **kwargs) + self.status_code = 302 + self.headers["Location"] = location class TemplateResponse(HttpResponse): - ... + def __init__(self, request: Request, template=None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.context["request"] = request + self.template = template + self.loader = None + self._template = None + if not template: + raise GeneralException("TemplateResponse requires a template.") + + def render(self) -> str: + if self.loader is None: + raise GeneralException("TemplateResponse requires a template loader.") + self._template = self.loader.get_template(self.template) + return self._template.render(**self.context) + + def set_template_loader(self, env): + self.loader = env diff --git a/spiderweb/tests/test_spiderweb.py b/spiderweb/tests/test_spiderweb.py new file mode 100644 index 0000000..e69de29 diff --git a/templates/test.html b/templates/test.html new file mode 100644 index 0000000..a27453b --- /dev/null +++ b/templates/test.html @@ -0,0 +1,5 @@ +
+ This is a test of the {{ value }} template. +
\ No newline at end of file