wau 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wau-0.1.0/LICENSE +14 -0
- wau-0.1.0/PKG-INFO +88 -0
- wau-0.1.0/README.md +67 -0
- wau-0.1.0/pyproject.toml +43 -0
- wau-0.1.0/setup.cfg +4 -0
- wau-0.1.0/tests/test_auth_exempt.py +85 -0
- wau-0.1.0/tests/test_cors_same_host.py +61 -0
- wau-0.1.0/tests/test_hangman_e2e.py +40 -0
- wau-0.1.0/tests/test_hangman_example.py +71 -0
- wau-0.1.0/tests/test_todo_app_e2e.py +24 -0
- wau-0.1.0/wau.egg-info/PKG-INFO +88 -0
- wau-0.1.0/wau.egg-info/SOURCES.txt +14 -0
- wau-0.1.0/wau.egg-info/dependency_links.txt +1 -0
- wau-0.1.0/wau.egg-info/requires.txt +3 -0
- wau-0.1.0/wau.egg-info/top_level.txt +1 -0
- wau-0.1.0/wau.py +1095 -0
wau-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
GNU LESSER GENERAL PUBLIC LICENSE
|
|
2
|
+
Version 3, 29 June 2007
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
5
|
+
Everyone is permitted to copy and distribute verbatim copies
|
|
6
|
+
of this license document, but changing it is not allowed.
|
|
7
|
+
|
|
8
|
+
This project is licensed under the GNU Lesser General Public License,
|
|
9
|
+
version 3 or (at your option) any later version.
|
|
10
|
+
|
|
11
|
+
For the full license text, see:
|
|
12
|
+
https://www.gnu.org/licenses/lgpl-3.0.txt
|
|
13
|
+
|
|
14
|
+
SPDX-License-Identifier: LGPL-3.0-or-later
|
wau-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wau
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Web API Utils
|
|
5
|
+
Author: Marco Schmalz
|
|
6
|
+
License-Expression: LGPL-3.0-or-later
|
|
7
|
+
Keywords: api,json,werkzeug,education
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Education
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
|
+
Requires-Python: >=3.14
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: dataset>=2.0.0
|
|
18
|
+
Requires-Dist: pyjwt>=2.13.0
|
|
19
|
+
Requires-Dist: werkzeug>=3.1.8
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# `wau` — Web API Utils
|
|
23
|
+
|
|
24
|
+
Web API Utils, or short `wau`, is a thin layer on top of Werkzeug to provide a simple and consistent interface for writing APIs in Python. `wau` is built for educational purposes and is not intended for production use. It is opinionated, as it only supports JSON as data format. It uses simple type annotations to define the expected input and output of the API endpoints. Common tasks as authentication, CORS and server-sent events are supported by default.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
Install from PyPI:
|
|
29
|
+
|
|
30
|
+
```powershell
|
|
31
|
+
pip install wau
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
or with uv:
|
|
35
|
+
|
|
36
|
+
```powershell
|
|
37
|
+
uv add wau
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Testing
|
|
41
|
+
|
|
42
|
+
Test dependencies are separated from runtime dependencies in `pyproject.toml`
|
|
43
|
+
using the `test` dependency group.
|
|
44
|
+
|
|
45
|
+
Run the test suite:
|
|
46
|
+
|
|
47
|
+
```powershell
|
|
48
|
+
uv run --group test python -m pytest -q
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Run doctests:
|
|
52
|
+
|
|
53
|
+
```powershell
|
|
54
|
+
uv run --group test python -m doctest .\wau.py
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Publishing
|
|
58
|
+
|
|
59
|
+
Build package artifacts:
|
|
60
|
+
|
|
61
|
+
```powershell
|
|
62
|
+
uv build
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Validate metadata and README rendering:
|
|
66
|
+
|
|
67
|
+
```powershell
|
|
68
|
+
uvx twine check dist/*
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Upload to TestPyPI first:
|
|
72
|
+
|
|
73
|
+
```powershell
|
|
74
|
+
uv publish --publish-url https://test.pypi.org/legacy/
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Then publish to PyPI:
|
|
78
|
+
|
|
79
|
+
```powershell
|
|
80
|
+
uv publish
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
This project is licensed under GNU LGPL v3 or later (`LGPL-3.0-or-later`).
|
|
86
|
+
|
|
87
|
+
If you distribute modified versions of this library, those library
|
|
88
|
+
modifications must be published under the same license terms.
|
wau-0.1.0/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# `wau` — Web API Utils
|
|
2
|
+
|
|
3
|
+
Web API Utils, or short `wau`, is a thin layer on top of Werkzeug to provide a simple and consistent interface for writing APIs in Python. `wau` is built for educational purposes and is not intended for production use. It is opinionated, as it only supports JSON as data format. It uses simple type annotations to define the expected input and output of the API endpoints. Common tasks as authentication, CORS and server-sent events are supported by default.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install from PyPI:
|
|
8
|
+
|
|
9
|
+
```powershell
|
|
10
|
+
pip install wau
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
or with uv:
|
|
14
|
+
|
|
15
|
+
```powershell
|
|
16
|
+
uv add wau
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Testing
|
|
20
|
+
|
|
21
|
+
Test dependencies are separated from runtime dependencies in `pyproject.toml`
|
|
22
|
+
using the `test` dependency group.
|
|
23
|
+
|
|
24
|
+
Run the test suite:
|
|
25
|
+
|
|
26
|
+
```powershell
|
|
27
|
+
uv run --group test python -m pytest -q
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Run doctests:
|
|
31
|
+
|
|
32
|
+
```powershell
|
|
33
|
+
uv run --group test python -m doctest .\wau.py
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Publishing
|
|
37
|
+
|
|
38
|
+
Build package artifacts:
|
|
39
|
+
|
|
40
|
+
```powershell
|
|
41
|
+
uv build
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Validate metadata and README rendering:
|
|
45
|
+
|
|
46
|
+
```powershell
|
|
47
|
+
uvx twine check dist/*
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Upload to TestPyPI first:
|
|
51
|
+
|
|
52
|
+
```powershell
|
|
53
|
+
uv publish --publish-url https://test.pypi.org/legacy/
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Then publish to PyPI:
|
|
57
|
+
|
|
58
|
+
```powershell
|
|
59
|
+
uv publish
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
This project is licensed under GNU LGPL v3 or later (`LGPL-3.0-or-later`).
|
|
65
|
+
|
|
66
|
+
If you distribute modified versions of this library, those library
|
|
67
|
+
modifications must be published under the same license terms.
|
wau-0.1.0/pyproject.toml
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "wau"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Web API Utils"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.14"
|
|
11
|
+
license = "LGPL-3.0-or-later"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Marco Schmalz" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["api", "json", "werkzeug", "education"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Education",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.14",
|
|
21
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
22
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"dataset>=2.0.0",
|
|
26
|
+
"pyjwt>=2.13.0",
|
|
27
|
+
"werkzeug>=3.1.8",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[tool.setuptools]
|
|
31
|
+
py-modules = ["wau"]
|
|
32
|
+
|
|
33
|
+
[dependency-groups]
|
|
34
|
+
test = [
|
|
35
|
+
"playwright>=1.60.0",
|
|
36
|
+
"pytest>=9.0.3",
|
|
37
|
+
"pytest-playwright>=0.8.0",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
dev = [
|
|
41
|
+
"black>=24.10.0",
|
|
42
|
+
"isort>=8.0.1",
|
|
43
|
+
]
|
wau-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Tests for BaseJWTAuthMiddleware exempt-route matching."""
|
|
2
|
+
|
|
3
|
+
from werkzeug.test import Client
|
|
4
|
+
|
|
5
|
+
from wau import API, DummyAuth
|
|
6
|
+
|
|
7
|
+
SECRET = "not a secret but still quite long"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _build_app(exempt):
|
|
11
|
+
api = API()
|
|
12
|
+
|
|
13
|
+
@api.GET("/items/{id:int}")
|
|
14
|
+
def get_item(request, id):
|
|
15
|
+
return {"id": id, "user": request.remote_user}
|
|
16
|
+
|
|
17
|
+
@api.POST("/items/{id:int}")
|
|
18
|
+
def update_item(request, id, name: str):
|
|
19
|
+
return {"id": id, "name": name, "user": request.remote_user}
|
|
20
|
+
|
|
21
|
+
@api.GET("/public")
|
|
22
|
+
def public(request):
|
|
23
|
+
return "public"
|
|
24
|
+
|
|
25
|
+
return DummyAuth(api, SECRET, exempt=exempt)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_exempt_path_with_typed_placeholder_allows_request():
|
|
29
|
+
app = _build_app(exempt=[("GET", "/items/{id:int}")])
|
|
30
|
+
client = Client(app)
|
|
31
|
+
response = client.get("/items/42")
|
|
32
|
+
assert response.status == "200 OK"
|
|
33
|
+
assert response.get_json() == {"id": 42, "user": None}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_exempt_path_does_not_match_wrong_type():
|
|
37
|
+
app = _build_app(exempt=[("GET", "/items/{id:int}")])
|
|
38
|
+
client = Client(app)
|
|
39
|
+
# 'abc' is not an int, so the exempt rule does not match; auth is required.
|
|
40
|
+
response = client.get("/items/abc")
|
|
41
|
+
assert response.status == "401 UNAUTHORIZED"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_exempt_method_is_method_specific():
|
|
45
|
+
app = _build_app(exempt=[("GET", "/items/{id:int}")])
|
|
46
|
+
client = Client(app)
|
|
47
|
+
response = client.post("/items/42", json={"name": "x"})
|
|
48
|
+
assert response.status == "401 UNAUTHORIZED"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_non_exempt_path_still_requires_auth():
|
|
52
|
+
app = _build_app(exempt=[("GET", "/items/{id:int}")])
|
|
53
|
+
client = Client(app)
|
|
54
|
+
response = client.get("/public")
|
|
55
|
+
assert response.status == "401 UNAUTHORIZED"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_plain_exempt_path_allows_request():
|
|
59
|
+
app = _build_app(exempt=[("GET", "/public")])
|
|
60
|
+
client = Client(app)
|
|
61
|
+
response = client.get("/public")
|
|
62
|
+
assert response.status == "200 OK"
|
|
63
|
+
assert response.get_json() == "public"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_login_and_renew_are_auto_exempt():
|
|
67
|
+
app = _build_app(exempt=[])
|
|
68
|
+
client = Client(app)
|
|
69
|
+
|
|
70
|
+
# Login is always reachable without auth.
|
|
71
|
+
response = client.post("/auth/login")
|
|
72
|
+
assert response.status == "200 OK"
|
|
73
|
+
token = response.get_json()["token"]
|
|
74
|
+
|
|
75
|
+
# Renew is reachable without auth (token is supplied in body).
|
|
76
|
+
response = client.post("/auth/renew", json={"token": token})
|
|
77
|
+
assert response.status == "200 OK"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_login_method_restriction():
|
|
81
|
+
# Default login_methods=("POST",); GET on /auth/login must require auth.
|
|
82
|
+
app = _build_app(exempt=[])
|
|
83
|
+
client = Client(app)
|
|
84
|
+
response = client.get("/auth/login")
|
|
85
|
+
assert response.status == "401 UNAUTHORIZED"
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from werkzeug.test import Client
|
|
2
|
+
|
|
3
|
+
from wau import API, _cors_same_host_middleware
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _build_api():
|
|
7
|
+
api = API()
|
|
8
|
+
|
|
9
|
+
@api.GET("/")
|
|
10
|
+
def root():
|
|
11
|
+
return {"ok": True}
|
|
12
|
+
|
|
13
|
+
return api
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_cors_disabled_by_default():
|
|
17
|
+
client = Client(_build_api())
|
|
18
|
+
|
|
19
|
+
response = client.get("/", headers={"Origin": "http://localhost:5173"})
|
|
20
|
+
|
|
21
|
+
assert "Access-Control-Allow-Origin" not in response.headers
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_same_host_allows_any_port():
|
|
25
|
+
app = _cors_same_host_middleware(_build_api(), "localhost")
|
|
26
|
+
client = Client(app)
|
|
27
|
+
|
|
28
|
+
response = client.get("/", headers={"Origin": "http://localhost:5173"})
|
|
29
|
+
|
|
30
|
+
assert response.status_code == 200
|
|
31
|
+
assert response.headers["Access-Control-Allow-Origin"] == "http://localhost:5173"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_other_host_is_not_allowed():
|
|
35
|
+
app = _cors_same_host_middleware(_build_api(), "localhost")
|
|
36
|
+
client = Client(app)
|
|
37
|
+
|
|
38
|
+
response = client.get("/", headers={"Origin": "http://example.com:5173"})
|
|
39
|
+
|
|
40
|
+
assert response.status_code == 200
|
|
41
|
+
assert "Access-Control-Allow-Origin" not in response.headers
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_preflight_for_allowed_host():
|
|
45
|
+
app = _cors_same_host_middleware(_build_api(), "localhost")
|
|
46
|
+
client = Client(app)
|
|
47
|
+
|
|
48
|
+
response = client.open(
|
|
49
|
+
"/",
|
|
50
|
+
method="OPTIONS",
|
|
51
|
+
headers={
|
|
52
|
+
"Origin": "http://localhost:3001",
|
|
53
|
+
"Access-Control-Request-Method": "PUT",
|
|
54
|
+
"Access-Control-Request-Headers": "Content-Type, Authorization",
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
assert response.status_code == 204
|
|
59
|
+
assert response.headers["Access-Control-Allow-Origin"] == "http://localhost:3001"
|
|
60
|
+
assert "PUT" in response.headers["Access-Control-Allow-Methods"]
|
|
61
|
+
assert "Content-Type" in response.headers["Access-Control-Allow-Headers"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from playwright.sync_api import expect
|
|
2
|
+
|
|
3
|
+
from examples.hangman import hangman_api
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _open_hangman_page(page, hangman_server_url):
|
|
7
|
+
page.goto(f"{hangman_server_url}/hangman_client.html")
|
|
8
|
+
expect(page.get_by_role("heading", name="Hangman")).to_be_visible()
|
|
9
|
+
expect(page.locator("button.letter").first).to_be_visible()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_hangman_successful_game(page, monkeypatch, hangman_server_url):
|
|
13
|
+
hangman_api.games.clear()
|
|
14
|
+
monkeypatch.setattr(hangman_api.random, "randrange", lambda _n: 111111)
|
|
15
|
+
monkeypatch.setattr(hangman_api.random, "choice", lambda _words: "abc")
|
|
16
|
+
|
|
17
|
+
_open_hangman_page(page, hangman_server_url)
|
|
18
|
+
|
|
19
|
+
page.get_by_role("button", name="a").click()
|
|
20
|
+
page.get_by_role("button", name="b").click()
|
|
21
|
+
page.get_by_role("button", name="c").click()
|
|
22
|
+
|
|
23
|
+
expect(page.get_by_role("button", name="Neues Spiel")).to_be_visible()
|
|
24
|
+
assert page.locator("span.letters", has_text="_").count() == 0
|
|
25
|
+
assert page.locator("img").get_attribute("src") == "image_0.png"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_hangman_failed_game(page, monkeypatch, hangman_server_url):
|
|
29
|
+
hangman_api.games.clear()
|
|
30
|
+
monkeypatch.setattr(hangman_api.random, "randrange", lambda _n: 222222)
|
|
31
|
+
monkeypatch.setattr(hangman_api.random, "choice", lambda _words: "z")
|
|
32
|
+
|
|
33
|
+
_open_hangman_page(page, hangman_server_url)
|
|
34
|
+
|
|
35
|
+
for letter in "abcdef":
|
|
36
|
+
page.get_by_role("button", name=letter).click()
|
|
37
|
+
|
|
38
|
+
expect(page.get_by_role("button", name="Neues Spiel")).to_be_visible()
|
|
39
|
+
assert page.locator("img").get_attribute("src") == "image_6.png"
|
|
40
|
+
assert page.get_by_role("button", name="a").is_disabled()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from werkzeug.test import Client
|
|
2
|
+
|
|
3
|
+
from examples.hangman import hangman_api
|
|
4
|
+
from wau import API
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_optional_request_and_readable_placeholders():
|
|
8
|
+
api = API()
|
|
9
|
+
|
|
10
|
+
@api.PUT("/sum/{value:int}")
|
|
11
|
+
def add(value, inc: int):
|
|
12
|
+
return {"total": value + inc}
|
|
13
|
+
|
|
14
|
+
client = Client(api)
|
|
15
|
+
response = client.put("/sum/7", json={"inc": 5})
|
|
16
|
+
|
|
17
|
+
assert response.status_code == 200
|
|
18
|
+
assert response.get_json() == {"total": 12}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_typed_url_placeholder_rejects_invalid_type():
|
|
22
|
+
api = API()
|
|
23
|
+
|
|
24
|
+
@api.GET("/items/{item_id:int}")
|
|
25
|
+
def get_item(item_id):
|
|
26
|
+
return {"item_id": item_id}
|
|
27
|
+
|
|
28
|
+
client = Client(api)
|
|
29
|
+
response = client.get("/items/not-a-number")
|
|
30
|
+
|
|
31
|
+
assert response.status_code == 404
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_simple_wrapper_rejects_unexpected_body():
|
|
35
|
+
api = API()
|
|
36
|
+
|
|
37
|
+
@api.POST("/ping")
|
|
38
|
+
def ping():
|
|
39
|
+
return "pong"
|
|
40
|
+
|
|
41
|
+
client = Client(api)
|
|
42
|
+
response = client.post("/ping", data="{}")
|
|
43
|
+
|
|
44
|
+
assert response.status_code == 415
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_hangman_works_with_wau(monkeypatch):
|
|
48
|
+
hangman_api.games.clear()
|
|
49
|
+
|
|
50
|
+
monkeypatch.setattr(hangman_api.random, "randrange", lambda _n: 424242)
|
|
51
|
+
monkeypatch.setattr(hangman_api.random, "choice", lambda _words: "abc")
|
|
52
|
+
|
|
53
|
+
client = Client(hangman_api.api)
|
|
54
|
+
|
|
55
|
+
create = client.post("/api/hangman/")
|
|
56
|
+
assert create.status_code == 200
|
|
57
|
+
game = create.get_json()
|
|
58
|
+
assert game["game_id"] == 424242
|
|
59
|
+
assert game["word_length"] == 3
|
|
60
|
+
assert game["guesses_left"] == 6
|
|
61
|
+
|
|
62
|
+
correct = client.put("/api/hangman/424242", json={"letter": "a"})
|
|
63
|
+
assert correct.status_code == 200
|
|
64
|
+
assert correct.get_json() == {"letter_found": True, "positions": [0]}
|
|
65
|
+
|
|
66
|
+
wrong = client.put("/api/hangman/424242", json={"letter": "z"})
|
|
67
|
+
assert wrong.status_code == 200
|
|
68
|
+
assert wrong.get_json() == {"letter_found": False, "guesses_left": 5}
|
|
69
|
+
|
|
70
|
+
delete = client.delete("/api/hangman/424242")
|
|
71
|
+
assert delete.status_code == 200
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import urllib.parse
|
|
2
|
+
|
|
3
|
+
from playwright.sync_api import expect
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_todo_app_cross_origin_e2e(page, todo_frontend_url, todo_backend_url):
|
|
7
|
+
api_query = urllib.parse.quote(todo_backend_url, safe=":/")
|
|
8
|
+
page.goto(f"{todo_frontend_url}/todo_client.html?api={api_query}")
|
|
9
|
+
|
|
10
|
+
expect(page.get_by_role("heading", name="Todos")).to_be_visible()
|
|
11
|
+
expect(page.locator("li")).to_have_count(3)
|
|
12
|
+
expect(page.get_by_text("Abwaschen")).to_be_visible()
|
|
13
|
+
|
|
14
|
+
page.get_by_placeholder("New todo").fill("Milch kaufen")
|
|
15
|
+
page.get_by_role("button", name="Save").click()
|
|
16
|
+
|
|
17
|
+
new_item = page.locator("li", has_text="Milch kaufen")
|
|
18
|
+
expect(new_item).to_be_visible()
|
|
19
|
+
new_item.get_by_role("button", name="Done").click()
|
|
20
|
+
|
|
21
|
+
expect(new_item.locator("span.strike")).to_have_text("Milch kaufen")
|
|
22
|
+
new_item.get_by_role("button", name="Delete").click()
|
|
23
|
+
|
|
24
|
+
expect(page.locator("li", has_text="Milch kaufen")).to_have_count(0)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wau
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Web API Utils
|
|
5
|
+
Author: Marco Schmalz
|
|
6
|
+
License-Expression: LGPL-3.0-or-later
|
|
7
|
+
Keywords: api,json,werkzeug,education
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Education
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
|
+
Requires-Python: >=3.14
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: dataset>=2.0.0
|
|
18
|
+
Requires-Dist: pyjwt>=2.13.0
|
|
19
|
+
Requires-Dist: werkzeug>=3.1.8
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# `wau` — Web API Utils
|
|
23
|
+
|
|
24
|
+
Web API Utils, or short `wau`, is a thin layer on top of Werkzeug to provide a simple and consistent interface for writing APIs in Python. `wau` is built for educational purposes and is not intended for production use. It is opinionated, as it only supports JSON as data format. It uses simple type annotations to define the expected input and output of the API endpoints. Common tasks as authentication, CORS and server-sent events are supported by default.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
Install from PyPI:
|
|
29
|
+
|
|
30
|
+
```powershell
|
|
31
|
+
pip install wau
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
or with uv:
|
|
35
|
+
|
|
36
|
+
```powershell
|
|
37
|
+
uv add wau
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Testing
|
|
41
|
+
|
|
42
|
+
Test dependencies are separated from runtime dependencies in `pyproject.toml`
|
|
43
|
+
using the `test` dependency group.
|
|
44
|
+
|
|
45
|
+
Run the test suite:
|
|
46
|
+
|
|
47
|
+
```powershell
|
|
48
|
+
uv run --group test python -m pytest -q
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Run doctests:
|
|
52
|
+
|
|
53
|
+
```powershell
|
|
54
|
+
uv run --group test python -m doctest .\wau.py
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Publishing
|
|
58
|
+
|
|
59
|
+
Build package artifacts:
|
|
60
|
+
|
|
61
|
+
```powershell
|
|
62
|
+
uv build
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Validate metadata and README rendering:
|
|
66
|
+
|
|
67
|
+
```powershell
|
|
68
|
+
uvx twine check dist/*
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Upload to TestPyPI first:
|
|
72
|
+
|
|
73
|
+
```powershell
|
|
74
|
+
uv publish --publish-url https://test.pypi.org/legacy/
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Then publish to PyPI:
|
|
78
|
+
|
|
79
|
+
```powershell
|
|
80
|
+
uv publish
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
This project is licensed under GNU LGPL v3 or later (`LGPL-3.0-or-later`).
|
|
86
|
+
|
|
87
|
+
If you distribute modified versions of this library, those library
|
|
88
|
+
modifications must be published under the same license terms.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
wau.py
|
|
5
|
+
tests/test_auth_exempt.py
|
|
6
|
+
tests/test_cors_same_host.py
|
|
7
|
+
tests/test_hangman_e2e.py
|
|
8
|
+
tests/test_hangman_example.py
|
|
9
|
+
tests/test_todo_app_e2e.py
|
|
10
|
+
wau.egg-info/PKG-INFO
|
|
11
|
+
wau.egg-info/SOURCES.txt
|
|
12
|
+
wau.egg-info/dependency_links.txt
|
|
13
|
+
wau.egg-info/requires.txt
|
|
14
|
+
wau.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
wau
|