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 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` &mdash; 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` &mdash; 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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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` &mdash; 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,3 @@
1
+ dataset>=2.0.0
2
+ pyjwt>=2.13.0
3
+ werkzeug>=3.1.8
@@ -0,0 +1 @@
1
+ wau