Compare commits

..

5 Commits

Author SHA1 Message Date
c27a7b28aa 📝 cover page 2024-08-26 01:57:03 -04:00
42552e2dbc 📝 first push of docs 2024-08-26 01:56:56 -04:00
3ab9e05442 add helpers 2024-08-26 01:56:31 -04:00
6c8e88b8d9 add test.html 2024-08-26 01:56:24 -04:00
6b44e34013 Testing! 2024-08-26 01:56:08 -04:00
34 changed files with 908 additions and 59 deletions

12
conftest.py Normal file
View File

@ -0,0 +1,12 @@
import os
from pytest import fixture
@fixture(autouse=True, scope="session")
def db():
if os.path.exists("spiderweb-tests.db"):
os.remove("spiderweb-tests.db")
yield
if os.path.exists("spiderweb-tests.db"):
os.remove("spiderweb-tests.db")

44
docs/README.md Normal file
View File

@ -0,0 +1,44 @@
# spiderweb
As a professional web developer focusing on arcane uses of Django for arcane purposes, it occurred to me a little while ago that I didn't actually know how a web framework _worked_.
> So I built one.
This is `spiderweb`, a web framework that's just big enough to hold a spider. When building it, my goals were simple:
- Learn a lot
- Create an unholy blend of Django and Flask
- Not look at any existing code. Go off of vibes alone and try to solve all the problems I could think of in my own way
> [!WARNING]
> This is a learning project. It should not be used for production without heavy auditing. It's not secure. It's not fast. It's not well-tested. It's not well-documented. It's not well-anything. It's a learning project.
>
> That being said, it's fun and it works, so I'm counting that as a win.
Here's a non-exhaustive list of things this can do:
* Function-based views
* Optional Flask-style URL routing
* Optional Django-style URL routing
* URLs with variables in them a lá Django
* Full middleware implementation
* Limit routes by HTTP verbs
* (Only GET and POST are implemented right now)
* Custom error routes
* Built-in dev server
* Gunicorn support
* HTML templates with Jinja2
* Static files support
* Cookies (reading and setting)
* Optional append_slash (with automatic redirects!)
* ~~CSRF middleware implementation~~ (it's there, but it's crappy and unsafe. This might be beyond my skillset.)
* Optional POST data validation middleware with Pydantic
* Database support (using Peewee, but the end user can use whatever they want as long as there's a Peewee driver for it)
* Session middleware with built-in session store
* Tests (currently a little over 80% coverage)
The TODO list:
* Fix CSRF middleware
* Add more HTTP verbs

9
docs/_coverpage.md Normal file
View File

@ -0,0 +1,9 @@
![logo](_media/spiderweb_logo.png)
> the web framework just big enough for a spider
[GitHub](https://github.com/itsthejoker/spiderweb/)
[Get Started](#spiderweb)
![color](#222)

View File

@ -0,0 +1 @@
https://www.hubspot.com/hubfs/brand-kit-generator/prototype/fonts/DMSans-Bold.ttf

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
docs/_media/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

6
docs/_sidebar.md Normal file
View File

@ -0,0 +1,6 @@
- [home](README.md)
- [quickstart](quickstart.md)
- [responses](responses.md)
- Middleware
- [middleware](middleware/test.md)
- [middleware2](middleware/test2.md)

12
docs/example.md Normal file
View File

@ -0,0 +1,12 @@
> [!ATTENTION]
> An alert of type 'attention' using global style 'callout'.
> [!TIP]
> An alert of type 'tip' using global style 'callout'.
> [!WARNING]
> An alert of type 'warning' using global style 'callout'.
> [!NOTE]
> An alert of type 'note' using global style 'callout'.

44
docs/index.html Normal file
View File

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="description" content="Description">
<link rel="icon" type="image/png" sizes="32x32" href="/_media/Favicon-32x32.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/vue.css">
</head>
<body>
<style>
@font-face {
font-family: "DMSans";
src: url('_media/DMSans-Medium.ttf') format('truetype');
}
body {
font-family: "DMSans", sans-serif;
}
</style>
<div id="app"></div>
<script>
window.$docsify = {
name: 'Spiderweb',
loadSidebar: true,
repo: 'https://github.com/itsthejoker/spiderweb',
maxLevel: 3,
coverpage: true,
'flexible-alerts': {
style: 'callout' // or 'flat'
},
auto2top: true,
}
</script>
<!-- Docsify v4 -->
<script src="//cdn.jsdelivr.net/npm/docsify@4"></script>
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-python.min.js"></script>
<!-- admonitions -->
<script src="https://unpkg.com/docsify-plugin-flexible-alerts"></script>
<script src="//unpkg.com/docsify-pagination/dist/docsify-pagination.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/docsify-tabs@1"></script>
</body>
</html>

1
docs/middleware/test.md Normal file
View File

@ -0,0 +1 @@
asdf

1
docs/middleware/test2.md Normal file
View File

@ -0,0 +1 @@
asdfawaasdf

93
docs/quickstart.md Normal file
View File

@ -0,0 +1,93 @@
# quickstart
Start by installing the package with your favorite package manager:
<!-- tabs:start -->
<!-- tab:poetry -->
```shell
poetry add spiderweb-framework
```
<!-- tab:pip -->
```shell
pip install spiderweb-framework
```
<!-- tab:pipenv -->
```shell
pipenv install spiderweb-framework
```
<!-- tabs:end -->
Then, create a new file and drop this in it:
```python
from spiderweb import SpiderwebRouter
from spiderweb.response import HttpResponse
app = SpiderwebRouter()
@app.route("/")
def index(request):
return HttpResponse("HELLO, WORLD!")
if __name__ == "__main__":
app.start()
```
Start the dev server by running `python {yourfile.py}` and navigating to `http://localhost:8000/` in your browser. You should see `HELLO, WORLD!` displayed on the page. Press `Ctrl+C` to stop the server.
That's it! You've got a working web app. Let's take a look at what these few lines of code are doing:
```python
from spiderweb import SpiderwebRouter
```
The `SpiderwebRouter` class is the main object that everything stems from in `spiderweb`. It's where you'll set your options, your routes, and more.
```python
from spiderweb.response import HttpResponse
```
Rather than trying to infer what you want, spiderweb wants you to be specific about what you want it to do. Part of that is the One Response Rule:
> Every view must return a Response, and each Response must be a specific type.
There are four different types of responses; if you want to skip ahead, hop over to [the responses page](responses.md) to learn more. For this example, we'll focus on `HttpResponse`, which is the base response.
```python
app = SpiderwebRouter()
```
This line creates a new instance of the `SpiderwebRouter` class and assigns it to the variable `app`. This is the object that will handle all of your requests and responses. If you need to pass any options into spiderweb, you'll do that here.
```python
@app.route("/")
def index(request):
return HttpResponse("HELLO, WORLD!")
```
This is an example view. There are a few things to note here:
- The `@app.route("/")` decorator tells spiderweb that this view should be called when the user navigates to the root of the site.
- The `def index(request):` function is the view itself. It takes a single argument, `request`, which is a `Request` object that contains all the information about the incoming request.
- The `return HttpResponse("HELLO, WORLD!")` line is the response. In this case, it's a simple `HttpResponse` object that contains the string `HELLO, WORLD!`. This will be sent back to the user's browser.
> [!TIP]
> Every view must accept a `request` object as its first argument. This object contains all the information about the incoming request, including headers, cookies, and more.
>
> There's more that we can pass in, but for now, we'll keep it simple.
```python
if __name__ == "__main__":
app.start()
```
Once you finish setting up your app, it's time to start it! You can start the dev server by just calling `app.start()` (and its counterpart `app.stop()` to stop it). This will start a simple server on `localhost:8000` that you can access in your browser. It's not a secure server; don't even think about using it in production. It's just good enough for development.
Now that your app is done, you can also run it with Gunicorn by running `gunicorn --workers=2 {yourfile}:app` in your terminal. This will start a Gunicorn server on `localhost:8000` that you can access in your browser and is a little more robust than the dev server.

3
docs/responses.md Normal file
View File

@ -0,0 +1,3 @@
# responses
...

View File

@ -55,15 +55,15 @@ app = SpiderwebRouter(
],
staticfiles_dirs=["static_files"],
routes=[
["/", index],
["/redirect", redirect],
["/json", json],
["/error", error],
["/middleware", middleware],
["/example/<int:id>", example],
["/form", form, {"allowed_methods": ["GET", "POST"], "csrf_exempt": True}],
("/", index),
("/redirect", redirect),
("/json", json),
("/error", error),
("/middleware", middleware),
("/example/<int:id>", example),
("/form", form, {"allowed_methods": ["GET", "POST"], "csrf_exempt": True}),
],
error_routes={"405": http405},
error_routes={405: http405},
)

148
poetry.lock generated
View File

@ -11,6 +11,25 @@ files = [
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
]
[[package]]
name = "attrs"
version = "24.2.0"
description = "Classes Without Boilerplate"
optional = false
python-versions = ">=3.7"
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]"]
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"]
[[package]]
name = "black"
version = "24.8.0"
@ -159,6 +178,90 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "coverage"
version = "7.6.1"
description = "Code coverage measurement for Python"
optional = false
python-versions = ">=3.8"
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"},
{file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"},
{file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"},
{file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"},
{file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"},
{file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"},
{file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"},
{file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"},
{file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"},
{file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"},
{file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"},
{file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"},
{file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"},
{file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"},
{file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"},
{file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"},
{file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"},
{file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"},
{file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"},
{file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"},
{file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"},
{file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"},
{file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"},
{file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"},
{file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"},
{file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"},
{file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"},
{file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"},
{file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"},
{file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"},
{file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"},
{file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"},
{file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"},
{file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"},
{file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"},
{file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"},
{file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"},
{file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"},
{file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"},
{file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"},
{file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"},
{file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"},
{file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"},
{file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"},
{file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"},
{file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"},
{file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"},
{file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"},
{file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"},
{file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"},
{file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"},
{file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"},
{file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"},
{file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"},
{file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"},
{file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"},
{file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"},
{file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"},
{file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"},
{file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"},
{file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"},
{file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"},
{file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"},
{file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"},
{file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"},
{file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"},
{file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"},
{file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"},
{file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"},
{file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"},
{file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"},
]
[package.extras]
toml = ["tomli"]
[[package]]
name = "cryptography"
version = "43.0.0"
@ -264,6 +367,38 @@ setproctitle = ["setproctitle"]
testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"]
tornado = ["tornado (>=0.2)"]
[[package]]
name = "hypothesis"
version = "6.111.2"
description = "A library for property-based testing"
optional = false
python-versions = ">=3.8"
files = [
{file = "hypothesis-6.111.2-py3-none-any.whl", hash = "sha256:055e8228958e22178d6077e455fd86a72044d02dac130dbf9c8b31e161b9809c"},
{file = "hypothesis-6.111.2.tar.gz", hash = "sha256:0496ad28c7240ee9ba89fcc7fb1dc74e89f3e40fbcbbb5f73c0091558dec8e6e"},
]
[package.dependencies]
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)"]
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)"]
dateutil = ["python-dateutil (>=1.4)"]
django = ["django (>=3.2)"]
dpcontracts = ["dpcontracts (>=0.4)"]
ghostwriter = ["black (>=19.10b0)"]
lark = ["lark (>=0.10.1)"]
numpy = ["numpy (>=1.17.3)"]
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)"]
[[package]]
name = "idna"
version = "3.7"
@ -627,6 +762,17 @@ files = [
{file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"},
]
[[package]]
name = "sortedcontainers"
version = "2.4.0"
description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
optional = false
python-versions = "*"
files = [
{file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"},
{file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"},
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
@ -641,4 +787,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "84633fc94c48c2a05b5ec77367ad29f327be1dc249a6e4cb76b50ebbe14739b5"
content-hash = "17f5dc4b157da57ad75a6f6aa3feb7adfa07500b805d5e79d1f09d640964949f"

View File

@ -41,6 +41,8 @@ ruff = "^0.5.5"
pytest = "^8.3.2"
black = "^24.8.0"
gunicorn = "^23.0.0"
hypothesis = "^6.111.2"
coverage = "^7.6.1"
[build-system]
requires = ["poetry-core"]
@ -53,3 +55,32 @@ Homepage = "https://github.com/itsthejoker/spiderweb"
Documentation = "https://github.com/itsthejoker/spiderweb"
Repository = "https://git.joekaufeld.com/jkaufeld/spiderweb"
"Bug Tracker" = "https://github.com/itsthejoker/spiderweb/issues"
[tool.pytest.ini_options]
minversion = "6.0"
addopts = ["--maxfail=2", "-rf"]
[tool.coverage.run]
branch = true
omit = ["conftest.py"]
[tool.coverage.report]
# Regexes for lines to exclude from consideration
exclude_also = [
# Don't complain about missing debug-only code:
"def __repr__",
"if self\\.debug",
# Don't complain if tests don't hit defensive assertion code:
"raise AssertionError",
"raise NotImplementedError",
# Don't complain if non-runnable code isn't run:
"if 0:",
"if __name__ == .__main__.:",
# Don't complain about abstract methods, they aren't run:
"@(abc\\.)?abstractmethod",
]
ignore_errors = true

View File

@ -1,7 +1,7 @@
from peewee import DatabaseProxy
DEFAULT_ALLOWED_METHODS = ["GET"]
DEFAULT_ENCODING = "ISO-8859-1"
DEFAULT_ENCODING = "UTF-8"
__version__ = "0.10.0"
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie

View File

@ -5,9 +5,6 @@ class IntConverter:
def to_python(self, value):
return int(value)
def to_url(self, value):
return str(value)
class StrConverter:
regex = r"[^/]+"
@ -16,9 +13,6 @@ class StrConverter:
def to_python(self, value):
return str(value)
def to_url(self, value):
return str(value)
class FloatConverter:
regex = r"\d+\.\d+"
@ -26,6 +20,3 @@ class FloatConverter:
def to_python(self, value):
return float(value)
def to_url(self, value):
return str(value)

View File

@ -11,7 +11,7 @@ def http403(request):
def http404(request):
return JsonResponse(
data={"error": f"Route {request.url} not found"}, status_code=404
data={"error": f"Route `{request.path}` not found"}, status_code=404
)

View File

@ -7,7 +7,7 @@ from threading import Thread
from typing import Optional, Callable
from wsgiref.simple_server import WSGIServer
from jinja2 import Environment, FileSystemLoader
from jinja2 import BaseLoader, Environment, FileSystemLoader
from peewee import Database, SqliteDatabase
from spiderweb.middleware import MiddlewareMixin
@ -46,8 +46,8 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi
middleware: list[str] = None,
append_slash: bool = False,
staticfiles_dirs: list[str] = None,
routes: list[list[str | Callable | dict]] = None,
error_routes: dict[str, Callable] = None,
routes: list[tuple[str, Callable] | tuple[str, Callable, dict]] = None,
error_routes: dict[int, Callable] = None,
secret_key: str = None,
session_max_age=60 * 60 * 24 * 14, # 2 weeks
session_cookie_name="swsession",
@ -100,10 +100,14 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi
if self.routes:
self.add_routes()
if self.error_routes:
self.add_error_routes()
if self.templates_dirs:
self.env = Environment(loader=FileSystemLoader(self.templates_dirs))
self.template_loader = Environment(loader=FileSystemLoader(self.templates_dirs))
else:
self.env = None
self.template_loader = None
self.string_loader = Environment(loader=BaseLoader())
if self.staticfiles_dirs:
for static_dir in self.staticfiles_dirs:
@ -131,7 +135,6 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi
rendered_output = resp.render()
if not isinstance(rendered_output, list):
rendered_output = [rendered_output]
encoded_resp = [
chunk.encode(DEFAULT_ENCODING) if isinstance(chunk, str) else chunk
for chunk in rendered_output
@ -183,12 +186,12 @@ class SpiderwebRouter(LocalServerMixin, MiddlewareMixin, RoutesMixin, FernetMixi
def prepare_and_fire_response(self, start_response, request, resp) -> list[bytes]:
try:
if isinstance(resp, dict):
self.fire_response(start_response, request, JsonResponse(data=resp))
return self.fire_response(start_response, request, JsonResponse(data=resp))
if isinstance(resp, TemplateResponse):
resp.set_template_loader(self.env)
resp.set_template_loader(self.template_loader)
resp.set_string_loader(self.string_loader)
for middleware in self.middleware:
middleware.process_response(request, resp)
self.process_response_middleware(request, resp)
return self.fire_response(start_response, request, resp)

View File

@ -21,7 +21,7 @@ class Session(SpiderwebModel):
user_agent = TextField()
class Meta:
table_name = 'spiderweb_sessions'
table_name = "spiderweb_sessions"
class SessionMiddleware(SpiderwebMiddleware):
@ -68,7 +68,7 @@ class SessionMiddleware(SpiderwebMiddleware):
}
# if a new session has been requested, ignore everything else and make that happen
if request._session["new_session"]:
if request._session["new_session"] is True:
# we generated a new one earlier, so we can use it now
session_key = request._session["id"]
response.set_cookie(
@ -76,6 +76,8 @@ class SessionMiddleware(SpiderwebMiddleware):
session_key,
**cookie_settings,
)
if not is_jsonable(request.SESSION):
raise ValueError("Session data is not JSON serializable.")
session = Session(
session_key=session_key,
session_data=json.dumps(request.SESSION),
@ -97,18 +99,6 @@ class SessionMiddleware(SpiderwebMiddleware):
)
session = request.META["SESSION"]
if not session:
if not is_jsonable(request.SESSION):
raise ValueError("Session data is not JSON serializable.")
session = Session(
session_key=session_key,
session_data=json.dumps(request.SESSION),
created_at=datetime.now(),
last_active=datetime.now(),
ip_address=request.META.get("client_address"),
user_agent=request.META.get("HTTP_USER_AGENT"),
)
else:
session.session_data = json.dumps(request.SESSION)
session.last_active = datetime.now()
session.session_data = json.dumps(request.SESSION)
session.last_active = datetime.now()
session.save()

View File

@ -10,7 +10,6 @@ from spiderweb.constants import REGEX_COOKIE_NAME
from spiderweb.exceptions import GeneralException
from spiderweb.request import Request
mimetypes.init()
@ -128,20 +127,39 @@ class RedirectResponse(HttpResponse):
class TemplateResponse(HttpResponse):
def __init__(self, request: Request, template=None, *args, **kwargs):
def __init__(
self,
request: Request,
template_path: str = None,
template_string: str = None,
*args,
**kwargs,
):
super().__init__(*args, **kwargs)
self.context["request"] = request
self.template = template
self.loader = None
self.template_path = template_path
self.template_string = template_string
self.template_loader = None
self.string_loader = None
self._template = None
if not template:
if not template_path and not template_string:
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)
if self.template_loader is None:
if not self.template_string:
raise GeneralException(
"TemplateResponse has no loader. Did you set templates_dirs?"
)
else:
self._template = self.string_loader.from_string(self.template_string)
else:
self._template = self.template_loader.get_template(self.template_path)
return self._template.render(**self.context)
def set_template_loader(self, env):
self.loader = env
def set_template_loader(self, loader):
self.template_loader = loader
def set_string_loader(self, loader):
self.string_loader = loader

View File

@ -1,5 +1,5 @@
import re
from typing import Callable, Any
from typing import Callable, Any, Optional
from spiderweb.constants import DEFAULT_ALLOWED_METHODS
from spiderweb.converters import * # noqa: F403
@ -30,9 +30,9 @@ class RoutesMixin:
# ones that start with underscores are the compiled versions, non-underscores
# are the user-supplied versions
_routes: dict
routes: list[list[str | Callable | dict]]
routes: list[tuple[str, Callable] | tuple[str, Callable, dict]] = None,
_error_routes: dict
error_routes: dict[str, Callable]
error_routes: dict[int, Callable]
append_slash: bool
def route(self, path, allowed_methods=None) -> Callable:

View File

@ -0,0 +1 @@
from spiderweb.tests.middleware import ExplodingResponseMiddleware, ExplodingRequestMiddleware

View File

@ -0,0 +1,28 @@
from wsgiref.util import setup_testing_defaults
from peewee import SqliteDatabase
from spiderweb import SpiderwebRouter
class StartResponse:
def __init__(self):
self.status = None
self.headers = None
def __call__(self, status, headers):
self.status = status
self.headers = headers
def get_headers(self):
return {h[0]: h[1] for h in self.headers}
def setup():
environ = {}
setup_testing_defaults(environ)
return (
SpiderwebRouter(db=SqliteDatabase("spiderweb-tests.db")),
environ,
StartResponse(),
)

View File

@ -0,0 +1,11 @@
from spiderweb import SpiderwebMiddleware, Request, HttpResponse, UnusedMiddleware
class ExplodingRequestMiddleware(SpiderwebMiddleware):
def process_request(self, request: Request) -> HttpResponse | None:
raise UnusedMiddleware("Boom!")
class ExplodingResponseMiddleware(SpiderwebMiddleware):
def process_response(self, request: Request, response: HttpResponse) -> HttpResponse | None:
raise UnusedMiddleware("Unfinished!")

View File

@ -0,0 +1 @@
TEMPLATE! {{ message }}

View File

@ -0,0 +1,88 @@
from datetime import timedelta
from peewee import SqliteDatabase
from spiderweb import SpiderwebRouter, HttpResponse
from spiderweb.constants import DEFAULT_ENCODING
from spiderweb.middleware.sessions import Session
from spiderweb.tests.helpers import setup
# app = SpiderwebRouter(
# middleware=[
# "spiderweb.middleware.sessions.SessionMiddleware",
# "spiderweb.middleware.csrf.CSRFMiddleware",
# "example_middleware.TestMiddleware",
# "example_middleware.RedirectMiddleware",
# "spiderweb.middleware.pydantic.PydanticMiddleware",
# "example_middleware.ExplodingMiddleware",
# ],
# )
def index(request):
if "value" in request.SESSION:
request.SESSION['value'] += 1
else:
request.SESSION['value'] = 0
return HttpResponse(body=str(request.SESSION['value']))
def test_session_middleware():
_, environ, start_response = setup()
app = SpiderwebRouter(
middleware=["spiderweb.middleware.sessions.SessionMiddleware"],
db=SqliteDatabase("spiderweb-tests.db")
)
app.add_route("/", index)
environ["HTTP_USER_AGENT"] = "hi"
environ["REMOTE_ADDR"] = "1.1.1.1"
assert app(environ, start_response) == [bytes(str(0), DEFAULT_ENCODING)]
session_key = Session.select().first().session_key
environ["HTTP_COOKIE"] = f"swsession={session_key}"
assert app(environ, start_response) == [bytes(str(1), DEFAULT_ENCODING)]
assert app(environ, start_response) == [bytes(str(2), DEFAULT_ENCODING)]
def test_expired_session():
_, environ, start_response = setup()
app = SpiderwebRouter(
middleware=["spiderweb.middleware.sessions.SessionMiddleware"],
db=SqliteDatabase("spiderweb-tests.db")
)
app.add_route("/", index)
environ["HTTP_USER_AGENT"] = "hi"
environ["REMOTE_ADDR"] = "1.1.1.1"
assert app(environ, start_response) == [bytes(str(0), DEFAULT_ENCODING)]
session = Session.select().first()
session.created_at = session.created_at - timedelta(seconds=app.session_max_age)
session.save()
environ["HTTP_COOKIE"] = f"swsession={session.session_key}"
# it shouldn't increment because we get a new session
assert app(environ, start_response) == [bytes(str(0), DEFAULT_ENCODING)]
session2 = list(Session.select())[-1]
assert session2.session_key != session.session_key
def test_exploding_middleware():
_, environ, start_response = setup()
app = SpiderwebRouter(
middleware=[
"spiderweb.tests.middleware.ExplodingRequestMiddleware",
"spiderweb.tests.middleware.ExplodingResponseMiddleware",
],
db=SqliteDatabase("spiderweb-tests.db")
)
app.add_route("/", index)
assert app(environ, start_response) == [bytes(str(0), DEFAULT_ENCODING)]

View File

@ -0,0 +1,173 @@
import pytest
from spiderweb import SpiderwebRouter, ConfigError
from spiderweb.constants import DEFAULT_ENCODING
from spiderweb.exceptions import NoResponseError, SpiderwebNetworkException
from spiderweb.response import HttpResponse, JsonResponse, TemplateResponse, RedirectResponse
from hypothesis import given, strategies as st
from spiderweb.tests.helpers import setup
@given(st.text())
def test_http_response(text):
app, environ, start_response = setup()
@app.route("/")
def index(request):
return HttpResponse(text)
assert app(environ, start_response) == [bytes(text, DEFAULT_ENCODING)]
def test_json_response():
app, environ, start_response = setup()
@app.route("/")
def index(request):
return JsonResponse(data={"message": "text"})
assert app(environ, start_response) == [bytes('{"message": "text"}', DEFAULT_ENCODING)]
def test_dict_response():
app, environ, start_response = setup()
@app.route("/")
def index(request):
return {"message": "Hello, World!"}
assert app(environ, start_response) == [b'{"message": "Hello, World!"}']
@given(st.text())
def test_template_response(text):
app, environ, start_response = setup()
template = "MESSAGE: {{ message }}"
@app.route("/")
def index(request):
return TemplateResponse(
request, template_string=template, context={"message": text}
)
assert app(environ, start_response) == [b"MESSAGE: " + bytes(text, DEFAULT_ENCODING)]
def test_redirect_response():
app, environ, start_response = setup()
@app.route("/")
def index(request):
return RedirectResponse(location="/redirected")
assert app(environ, start_response) == [b'None']
assert start_response.get_headers()["Location"] == "/redirected"
def test_add_route_at_server_start():
app, environ, start_response = setup()
def index(request):
return RedirectResponse(location="/redirected")
def view2(request):
return HttpResponse("View 2")
app = SpiderwebRouter(routes=[
("/", index, {"allowed_methods": ["GET", "POST"], "csrf_exempt": True}),
("/view2", view2),
])
assert app(environ, start_response) == [b'None']
assert start_response.get_headers()["Location"] == "/redirected"
def test_redirect_on_append_slash():
_, environ, start_response = setup()
app = SpiderwebRouter(append_slash=True)
@app.route("/hello")
def index(request):
pass
environ["PATH_INFO"] = f"/hello"
assert app(environ, start_response) == [b'None']
assert start_response.get_headers()["Location"] == "/hello/"
@given(st.text())
def test_template_response_with_template(text):
_, environ, start_response = setup()
app = SpiderwebRouter(templates_dirs=["spiderweb/tests"])
@app.route("/")
def index(request):
return TemplateResponse(
request, "test.html", context={"message": text}
)
assert app(environ, start_response) == [b"TEMPLATE! " + bytes(text, DEFAULT_ENCODING)]
def test_view_returns_none():
app, environ, start_response = setup()
@app.route("/")
def index(request):
pass
with pytest.raises(NoResponseError):
assert app(environ, start_response) == [b'None']
def test_exploding_view():
app, environ, start_response = setup()
@app.route("/")
def index(request):
raise SpiderwebNetworkException("Boom!")
assert app(environ, start_response) == [
b'Something went wrong.\n\nCode: Boom!\n\nMsg: None\n\nDesc: None'
]
def test_missing_view():
app, environ, start_response = setup()
assert app(environ, start_response) == [b'{"error": "Route `/` not found"}']
def test_missing_view_with_custom_404():
app, environ, start_response = setup()
@app.error(404)
def custom_404(request):
return HttpResponse("Custom 404")
assert app(environ, start_response) == [b'Custom 404']
def test_duplicate_error_view():
app, environ, start_response = setup()
@app.error(404)
def custom_404(request):
...
with pytest.raises(ConfigError):
@app.error(404)
def custom_404(request):
...
def test_missing_view_with_custom_404_alt():
_, environ, start_response = setup()
def custom_404(request):
return HttpResponse("Custom 404 2")
app = SpiderwebRouter(error_routes={404: custom_404})
assert app(environ, start_response) == [b'Custom 404 2']

View File

@ -0,0 +1,102 @@
import pytest
from spiderweb import SpiderwebRouter
from spiderweb.constants import DEFAULT_ENCODING
from spiderweb.exceptions import ParseError, ConfigError
from spiderweb.response import HttpResponse, JsonResponse, TemplateResponse, RedirectResponse
from hypothesis import given, strategies as st, assume
from peewee import SqliteDatabase
from spiderweb.tests.helpers import setup
@given(st.text())
def test_str_converter(text):
assume(len(text) > 0)
assume("/" not in text)
app, environ, start_response = setup()
@app.route("/<str:test_input>")
def index(request, test_input: str):
return HttpResponse(test_input)
environ["PATH_INFO"] = f"/{text}"
assert app(environ, start_response) == [bytes(text, DEFAULT_ENCODING)]
@given(st.text())
def test_default_str_converter(text):
assume(len(text) > 0)
assume("/" not in text)
app, environ, start_response = setup()
@app.route("/<test_input>")
def index(request, test_input: str):
return HttpResponse(test_input)
environ["PATH_INFO"] = f"/{text}"
assert app(environ, start_response) == [bytes(text, DEFAULT_ENCODING)]
def test_unknown_converter():
app, environ, start_response = setup()
with pytest.raises(ParseError):
@app.route("/<asdf:test_input>")
def index(request, test_input: str):
return HttpResponse(test_input)
def test_duplicate_route():
app, environ, start_response = setup()
@app.route("/")
def index(request):
...
with pytest.raises(ConfigError):
@app.route("/")
def index(request):
...
def test_url_with_double_underscore():
app, environ, start_response = setup()
with pytest.raises(ConfigError):
@app.route("/<asdf:test__input>")
def index(request, test_input: str):
return HttpResponse(test_input)
@given(st.integers())
def test_int_converter(integer):
assume(integer > 0)
app, environ, start_response = setup()
@app.route("/<int:test_input>")
def index(request, test_input: str):
return HttpResponse(test_input)
environ["PATH_INFO"] = f"/{integer}"
assert app(environ, start_response) == [bytes(str(integer), DEFAULT_ENCODING)]
@pytest.mark.parametrize(
"number",
[
1.0000000000000002,
294744.2324,
0000.3,
],
)
def test_float_converter(number):
app, environ, start_response = setup()
@app.route("/<float:test_input>")
def index(request, test_input: str):
return HttpResponse(test_input)
environ["PATH_INFO"] = f"/{number}"
assert app(environ, start_response) == [bytes(str(number), DEFAULT_ENCODING)]

40
test.py Normal file
View File

@ -0,0 +1,40 @@
from peewee import *
from playhouse.migrate import SqliteMigrator, migrate
from spiderweb.db import SpiderwebModel
db = SqliteDatabase("people.db")
migrator = SqliteMigrator(db)
class Person(SpiderwebModel):
name = CharField()
birthday = DateField()
class Meta:
database = db # This model uses the "people.db" database.
class Pet(SpiderwebModel):
owner = ForeignKeyField(Person, backref="pets")
name = CharField(max_length=40)
animal_type = CharField()
age = IntegerField(null=True)
favorite_color = CharField(null=True)
class Meta:
database = db # this model uses the "people.db" database
if __name__ == "__main__":
db.connect()
Pet.check_for_needed_migration()
# try:
# Pet.check_for_needed_migration()
# except:
# migrate(
# migrator.add_column(
# Pet._meta.table_name, 'favorite_color', CharField(null=True)
# ),
# )
db.create_tables([Person, Pet])