swl 5.0.0__py3-none-any.whl

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.
Files changed (86) hide show
  1. swl/.gitignore +1 -0
  2. swl/__init__.py +26 -0
  3. swl/_version.py +24 -0
  4. swl/actions.py +150 -0
  5. swl/actions_test.py +245 -0
  6. swl/auth_test.py +121 -0
  7. swl/breadcrumbs.py +52 -0
  8. swl/config.py +66 -0
  9. swl/dispatcher.py +46 -0
  10. swl/dispatcher_test.py +45 -0
  11. swl/frameworks.py +179 -0
  12. swl/frameworks_test.py +122 -0
  13. swl/passwd.py +186 -0
  14. swl/passwd_test.py +208 -0
  15. swl/resources/__init__.py +14 -0
  16. swl/resources/bootstrap-web-resources.json +22 -0
  17. swl/resources/bulma-web-resources.json +18 -0
  18. swl/resources/swl-web-resources.json +17 -0
  19. swl/resources/tabler-web-resources.json +22 -0
  20. swl/resources/web_resources.py +64 -0
  21. swl/resources/web_resources_test.py +81 -0
  22. swl/responses.py +212 -0
  23. swl/responses_test.py +258 -0
  24. swl/routes_endpoint.py +135 -0
  25. swl/routes_endpoint_test.py +123 -0
  26. swl/session/__init__.py +15 -0
  27. swl/session/sessionauthbackend.py +30 -0
  28. swl/session/sessionsecretkey.py +28 -0
  29. swl/session/sessionsecretkey_test.py +23 -0
  30. swl/shutdown.py +40 -0
  31. swl/sort/__init__.py +14 -0
  32. swl/sort/info.py +76 -0
  33. swl/sort/info_test.py +81 -0
  34. swl/static/swl/lang/de_DE.json +16 -0
  35. swl/static/swl/lang/en_US.json +16 -0
  36. swl/static/swl/lang/fr_FR.json +16 -0
  37. swl/static/swl/lang/nl_NL.json +16 -0
  38. swl/static/swl/lang.js +37 -0
  39. swl/static/swl/seecr-logo-zonder.png +0 -0
  40. swl/static/swl/swl-base.css +145 -0
  41. swl/static/swl/swl-run.js +21 -0
  42. swl/static/swl/swl.css +412 -0
  43. swl/static/swl/swl.js +535 -0
  44. swl/static/swl/theme.js +63 -0
  45. swl/static/swl/websocket.js +62 -0
  46. swl/swl.py +171 -0
  47. swl/swl_test.py +100 -0
  48. swl/templates/swl/account.j2 +22 -0
  49. swl/templates/swl/account_password.j2 +42 -0
  50. swl/templates/swl/alert.j2 +16 -0
  51. swl/templates/swl/base.j2 +59 -0
  52. swl/templates/swl/breadcrumbs.j2 +33 -0
  53. swl/templates/swl/card.j2 +34 -0
  54. swl/templates/swl/contextmenu.j2 +5 -0
  55. swl/templates/swl/dialog.j2 +29 -0
  56. swl/templates/swl/footer.j2 +25 -0
  57. swl/templates/swl/inputs.j2 +60 -0
  58. swl/templates/swl/kickstart.j2 +13 -0
  59. swl/templates/swl/modal-content.j2 +39 -0
  60. swl/templates/swl/modal.j2 +20 -0
  61. swl/templates/swl/placeholder.j2 +35 -0
  62. swl/templates/swl/toast.j2 +4 -0
  63. swl/templates/swl/users.j2 +56 -0
  64. swl/templates/swl/users_list.j2 +59 -0
  65. swl/test/__init__.py +12 -0
  66. swl/test/js-test/swl.test.js +509 -0
  67. swl/test/server.py +96 -0
  68. swl/test/views/__init__.py +16 -0
  69. swl/test/views/change.py +26 -0
  70. swl/test/views/misc.py +120 -0
  71. swl/test/views/modal.py +38 -0
  72. swl/test/views/rehydrate.py +54 -0
  73. swl/test_utils.py +117 -0
  74. swl/userbase.py +233 -0
  75. swl/userbase_test.py +213 -0
  76. swl/utils.py +36 -0
  77. swl/utils_test.py +36 -0
  78. swl/uvicorn_app.py +41 -0
  79. swl/views/__init__.py +16 -0
  80. swl/views/account.py +178 -0
  81. swl/views/account_test.py +289 -0
  82. swl-5.0.0.dist-info/METADATA +146 -0
  83. swl-5.0.0.dist-info/RECORD +86 -0
  84. swl-5.0.0.dist-info/WHEEL +5 -0
  85. swl-5.0.0.dist-info/licenses/LICENSE +21 -0
  86. swl-5.0.0.dist-info/top_level.txt +1 -0
swl/.gitignore ADDED
@@ -0,0 +1 @@
1
+ _version.py
swl/__init__.py ADDED
@@ -0,0 +1,26 @@
1
+ ## begin license ##
2
+ #
3
+ # "SWL"(Seecr Web Lib) makes developing webbased apps easier
4
+ #
5
+ # Copyright (C) 2024-2025 Seecr (Seek You Too B.V.) https://seecr.nl
6
+ #
7
+ # This file is part of "Seecr Web Lib"
8
+ #
9
+ # "Seecr Web Lib" is licensed under the MIT License; see the LICENSE file.
10
+ # SPDX-License-Identifier: MIT
11
+ #
12
+ ## end license ##
13
+
14
+ # ._version.py is generated by setuptools_scm
15
+
16
+ from pathlib import Path
17
+
18
+ from .actions import Actions
19
+ from .config import configure_logging
20
+ from .responses import *
21
+ from .swl import SWL
22
+ from .uvicorn_app import uvicorn_main
23
+
24
+ VERSION = SWL.SWL_VERSION
25
+ static_dir = Path(__file__).parent / "static"
26
+ templates_dir = Path(__file__).parent / "templates"
swl/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '5.0.0'
22
+ __version_tuple__ = version_tuple = (5, 0, 0)
23
+
24
+ __commit_id__ = commit_id = 'g4ab6b414a'
swl/actions.py ADDED
@@ -0,0 +1,150 @@
1
+ ## begin license ##
2
+ #
3
+ # "SWL"(Seecr Web Lib) makes developing webbased apps easier
4
+ #
5
+ # Copyright (C) 2024-2026 Seecr (Seek You Too B.V.) https://seecr.nl
6
+ #
7
+ # This file is part of "Seecr Web Lib"
8
+ #
9
+ # "Seecr Web Lib" is licensed under the MIT License; see the LICENSE file.
10
+ # SPDX-License-Identifier: MIT
11
+ #
12
+ ## end license ##
13
+
14
+ import logging
15
+ from hashlib import md5
16
+
17
+ from starlette._exception_handler import wrap_app_handling_exceptions
18
+ from starlette.exceptions import HTTPException
19
+ from starlette.requests import Request
20
+ from starlette.responses import PlainTextResponse, RedirectResponse
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ def hash_name(name):
26
+ return md5(name.encode(encoding="utf-8")).hexdigest()
27
+
28
+
29
+ class Actions:
30
+ def __init__(self, path="/action", default_auth=True, key=None):
31
+ self._actions = {}
32
+ self._path = path
33
+ self._extra_kwargs = {}
34
+ self._default_auth = default_auth
35
+ self._key = key or hash_name
36
+ if not self._path.startswith("/"):
37
+ raise ValueError("path must start with /")
38
+
39
+ @property
40
+ def actions_path(self):
41
+ return self._path + "/{action:str}"
42
+
43
+ def register_module(self, *modules):
44
+ for module in modules:
45
+ if hasattr(module, "__actions__"):
46
+ logger.info(
47
+ f"loading module {module.__name__}, "
48
+ + f"methods: {len(module.__actions__)}"
49
+ )
50
+ for item in module.__actions__:
51
+ if not isinstance(item, dict):
52
+ item = dict(method=item, authenticated=self._default_auth)
53
+ self.register_method(**item)
54
+
55
+ def register_method(self, method, authenticated=None, is_authorized=None):
56
+ if authenticated is None:
57
+ authenticated = self._default_auth
58
+ is_allowed = is_authorized_fn(authenticated, is_authorized)
59
+ logger.debug(f" - {method.__name__} - {is_allowed.__doc__}")
60
+
61
+ key = self._key(method.__name__)
62
+ self._actions[key] = dict(func=method, is_allowed=is_allowed)
63
+
64
+ def register_kwarg(self, key, value):
65
+ self._extra_kwargs[key] = value
66
+
67
+ def register(self, method_name, path=None):
68
+ key = self._key(method_name)
69
+ if path is not None:
70
+ return path + f"/{key}"
71
+ if key not in self._actions:
72
+ raise RuntimeError(f"Method not found: {method_name}")
73
+ return self._path + f"/{key}"
74
+
75
+ def get(self, key):
76
+ return self._actions.get(key)
77
+
78
+ def wrap(self, method, authenticated=True, **kwargs):
79
+ return ActionMethodWrap(self, method, authenticated=authenticated, **kwargs)
80
+
81
+ async def handle(self, request):
82
+ if "action" in request.path_params:
83
+ key = request.path_params["action"]
84
+ if (action := self.get(key)) is None:
85
+ raise HTTPException(404)
86
+ logger.debug(f"handle {self._path}/{key[:8]}.. {action['func'].__name__}")
87
+
88
+ user = request.user if "user" in request.scope else None
89
+ if not await action["is_allowed"](user, request, **self._extra_kwargs):
90
+ # action["authenticated"] is True and not request.user.is_authenticated:
91
+ raise HTTPException(403)
92
+
93
+ result = await action["func"](request, **self._extra_kwargs)
94
+ return result
95
+ raise HTTPException(404)
96
+
97
+
98
+ def is_authorized_fn(authenticated, is_authorized):
99
+ doc = "authenticated=False"
100
+ if authenticated:
101
+ doc = "authenticated=True"
102
+ if is_authorized is not None:
103
+ doc += f", is_authorized={is_authorized.__name__}(...)"
104
+
105
+ async def is_allowed(user, request, **kwargs):
106
+ if not authenticated:
107
+ return True
108
+ if user is None or not user.is_authenticated:
109
+ return False
110
+ if is_authorized is not None:
111
+ return await is_authorized(request, **kwargs)
112
+ return True
113
+
114
+ is_allowed.__doc__ = doc
115
+
116
+ return is_allowed
117
+
118
+
119
+ class ActionMethodWrap:
120
+ def __init__(
121
+ self, actions, method, authenticated=True, is_authorized=None, redirect=None
122
+ ):
123
+ self._actions = actions
124
+ self._method = method
125
+ self._authenticated = authenticated
126
+ self._is_authorized = is_authorized
127
+ if is_authorized is not None and not authenticated:
128
+ raise ValueError("is_authorized can only be set when authenticated is True")
129
+ self.is_allowed = is_authorized_fn(authenticated, is_authorized)
130
+ self._redirect = redirect
131
+
132
+ async def __call__(self, scope, receive, send):
133
+ request = Request(scope, receive, send)
134
+
135
+ if not await self.is_allowed(
136
+ scope.get("user"), request, **self._actions._extra_kwargs
137
+ ):
138
+ if self._redirect is None:
139
+ response = PlainTextResponse("Forbidden", status_code=403)
140
+ else:
141
+ request.session["next"] = request.url.path
142
+ response = RedirectResponse(self._redirect, status_code=303)
143
+ await response(scope, receive, send)
144
+ else:
145
+
146
+ async def app(scope, receive, send):
147
+ response = await self._method(request, **self._actions._extra_kwargs)
148
+ await response(scope, receive, send)
149
+
150
+ await wrap_app_handling_exceptions(app, request)(scope, receive, send)
swl/actions_test.py ADDED
@@ -0,0 +1,245 @@
1
+ ## begin license ##
2
+ #
3
+ # "SWL"(Seecr Web Lib) makes developing webbased apps easier
4
+ #
5
+ # Copyright (C) 2024-2026 Seecr (Seek You Too B.V.) https://seecr.nl
6
+ #
7
+ # This file is part of "Seecr Web Lib"
8
+ #
9
+ # "Seecr Web Lib" is licensed under the MIT License; see the LICENSE file.
10
+ # SPDX-License-Identifier: MIT
11
+ #
12
+ ## end license ##
13
+
14
+ import pytest
15
+ from starlette.applications import Starlette
16
+ from starlette.responses import PlainTextResponse
17
+ from starlette.routing import Route
18
+ from starlette.testclient import TestClient
19
+
20
+ from swl.test_utils import environment, create_app, login_user, logout_user
21
+
22
+ from .actions import Actions
23
+
24
+
25
+ @pytest.fixture
26
+ def write_module(tmp_path):
27
+ import sys
28
+
29
+ sys.path.insert(0, tmp_path.as_posix())
30
+
31
+ def write_mod(name, text):
32
+ (tmp_path / name).write_text(text)
33
+
34
+ yield write_mod
35
+ sys.path.pop(0)
36
+
37
+
38
+ def test_register_module_with_actions(tmp_path, write_module):
39
+ actions = Actions()
40
+
41
+ write_module("mod1.py", "def method1(x):\n return 42")
42
+ write_module("mod2.py", "def method2(x):\n return 42\n__actions__ = [method2]")
43
+ write_module(
44
+ "mod3.py",
45
+ "def method3(x):\n return 42"
46
+ + "\n__actions__ = [dict(method=method3, authenticated=False)]",
47
+ )
48
+ write_module(
49
+ "mod4.py",
50
+ "def method4(x):\n return 42"
51
+ + "\n__actions__ = [dict(method=method4, authenticated=True)]",
52
+ )
53
+
54
+ import mod1
55
+ import mod2
56
+ import mod3
57
+ import mod4
58
+
59
+ actions.register_module(mod1)
60
+ assert len(actions._actions) == 0
61
+ actions.register_module(mod2)
62
+ assert len(actions._actions) == 1
63
+ actions.register_module(mod3)
64
+ assert len(actions._actions) == 2
65
+ actions.register_module(mod4)
66
+ assert len(actions._actions) == 3
67
+
68
+ auth = [
69
+ action
70
+ for action in actions._actions.values()
71
+ if action["is_allowed"].__doc__.startswith("authenticated=True")
72
+ ]
73
+ assert len(auth) == 2
74
+
75
+
76
+ async def test_default_auth(write_module):
77
+ write_module("mod2.py", "def method2(x):\n return 42\n__actions__ = [method2]")
78
+ import mod2
79
+
80
+ auth_actions = Actions()
81
+ auth_actions.register_module(mod2)
82
+ assert list(auth_actions._actions.values())[0]["is_allowed"].__doc__.startswith(
83
+ "authenticated=True"
84
+ )
85
+
86
+ noauth_actions = Actions(default_auth=False)
87
+ noauth_actions.register_module(mod2)
88
+ assert list(noauth_actions._actions.values())[0]["is_allowed"].__doc__.startswith(
89
+ "authenticated=False"
90
+ )
91
+
92
+
93
+ async def test_default_auth_with_dict(write_module):
94
+ write_module(
95
+ "mod2.py", "def method2(x):\n return 42\n__actions__ = [dict(method=method2)]"
96
+ )
97
+ import mod2
98
+
99
+ auth_actions = Actions()
100
+ auth_actions.register_module(mod2)
101
+ assert list(auth_actions._actions.values())[0]["is_allowed"].__doc__.startswith(
102
+ "authenticated=True"
103
+ )
104
+
105
+ noauth_actions = Actions(default_auth=False)
106
+ noauth_actions.register_module(mod2)
107
+ assert list(noauth_actions._actions.values())[0]["is_allowed"].__doc__.startswith(
108
+ "authenticated=False"
109
+ )
110
+
111
+
112
+ def test_register_with_different_path(write_module):
113
+ write_module(
114
+ "mod2.py", "def method2(x):\n return 42\n__actions__ = [dict(method=method2)]"
115
+ )
116
+ import mod2
117
+
118
+ actions = Actions(path="/my/action", key=str.upper)
119
+ actions.register_module(mod2)
120
+
121
+ assert actions.register("method2") == "/my/action/METHOD2"
122
+ with pytest.raises(RuntimeError):
123
+ actions.register("unknown")
124
+ assert actions.register("unknown", path="/other/action") == "/other/action/UNKNOWN"
125
+
126
+
127
+ async def test_is_authorized(environment):
128
+ client, swl, userbase = environment
129
+
130
+ async def some_action(request, **kwargs):
131
+ return PlainTextResponse("Some Action")
132
+
133
+ async def userbase_has_user2(request, userbase, **kwargs):
134
+ return (userbase.get("user2")) is not None
135
+
136
+ swl.actions.register_method(some_action, is_authorized=userbase_has_user2)
137
+
138
+ response = client.post(swl.actions.register("some_action"))
139
+ assert response.status_code == 403
140
+
141
+ userbase.create("user2", "user2_password", admin=False)
142
+ response = client.post(swl.actions.register("some_action"))
143
+ assert response.status_code == 200
144
+ assert response.text == "Some Action"
145
+
146
+
147
+ async def test_method_wrap(environment):
148
+ _, swl, userbase = environment
149
+
150
+ async def kwargs_page(request, **kwargs):
151
+ kwarg_keys = sorted(kwargs.keys())
152
+ return PlainTextResponse(f"Kwargs: {kwarg_keys}")
153
+
154
+ app = await create_app(
155
+ swl,
156
+ userbase,
157
+ routes=[
158
+ Route("/wrapped", endpoint=swl.endpoint(kwargs_page)),
159
+ Route("/unwrapped", endpoint=kwargs_page),
160
+ ],
161
+ )
162
+ client = TestClient(app)
163
+ login_user(client, "user", "user_password")
164
+
165
+ response = client.get("/unwrapped")
166
+ assert response.text == "Kwargs: []"
167
+
168
+ response = client.get("/wrapped")
169
+ assert response.text == "Kwargs: ['swl', 'userbase']"
170
+
171
+
172
+ async def test_wrap_authenticated(environment):
173
+ _, swl, userbase = environment
174
+
175
+ async def auth_page(request, **kwargs):
176
+ return PlainTextResponse("Authenticated")
177
+
178
+ async def is_admin(request, **kwargs):
179
+ return request.user.admin
180
+
181
+ async def login_page(request, **kwargs):
182
+ return PlainTextResponse(request.session.get("next", "No next"))
183
+
184
+ app = await create_app(
185
+ swl,
186
+ userbase,
187
+ routes=[
188
+ Route(
189
+ "/auth",
190
+ endpoint=swl.endpoint(
191
+ auth_page, authenticated=True, redirect="/login/here"
192
+ ),
193
+ ),
194
+ Route(
195
+ "/noauth",
196
+ endpoint=swl.endpoint(auth_page, authenticated=False),
197
+ ),
198
+ Route(
199
+ "/login/here",
200
+ endpoint=swl.endpoint(login_page, authenticated=False),
201
+ ),
202
+ Route(
203
+ "/admin_only",
204
+ endpoint=swl.endpoint(
205
+ auth_page,
206
+ authenticated=True,
207
+ is_authorized=is_admin,
208
+ redirect="/login/here",
209
+ ),
210
+ ),
211
+ ],
212
+ )
213
+ client = TestClient(app)
214
+
215
+ response = client.get("/auth", follow_redirects=False)
216
+ assert response.status_code == 303
217
+ assert response.headers["location"] == "/login/here"
218
+ response = client.get("/login/here", follow_redirects=False)
219
+ assert response.status_code == 200
220
+ assert response.text == "/auth"
221
+
222
+ login_user(client, "user", "user_password")
223
+ response = client.get("/auth")
224
+ assert response.status_code == 200
225
+ assert response.text == "Authenticated"
226
+
227
+ response = client.get("/admin_only", follow_redirects=False)
228
+ assert response.status_code == 303
229
+ response = client.get(response.headers["location"], follow_redirects=False)
230
+ assert response.status_code == 200
231
+ assert response.text == "/admin_only"
232
+
233
+ login_user(client, "admin", "admin_password")
234
+ response = client.get("/admin_only")
235
+ assert response.status_code == 200
236
+ assert response.text == "Authenticated"
237
+
238
+ response = client.get("/noauth")
239
+ assert response.status_code == 200
240
+ assert response.text == "Authenticated"
241
+
242
+ logout_user(client)
243
+ response = client.get("/noauth")
244
+ assert response.status_code == 200
245
+ assert response.text == "Authenticated"
swl/auth_test.py ADDED
@@ -0,0 +1,121 @@
1
+ ## begin license ##
2
+ #
3
+ # "SWL"(Seecr Web Lib) makes developing webbased apps easier
4
+ #
5
+ # Copyright (C) 2024 Seecr (Seek You Too B.V.) https://seecr.nl
6
+ #
7
+ # This file is part of "Seecr Web Lib"
8
+ #
9
+ # "Seecr Web Lib" is licensed under the MIT License; see the LICENSE file.
10
+ # SPDX-License-Identifier: MIT
11
+ #
12
+ ## end license ##
13
+
14
+ import uuid
15
+
16
+ from starlette.applications import Starlette
17
+ from starlette.authentication import AuthCredentials, AuthenticationBackend, BaseUser
18
+ from starlette.middleware import Middleware
19
+ from starlette.middleware.authentication import AuthenticationMiddleware
20
+ from starlette.middleware.sessions import SessionMiddleware
21
+ from starlette.responses import PlainTextResponse
22
+ from starlette.routing import Route
23
+ from starlette.testclient import TestClient
24
+
25
+ from swl.swl import SWL
26
+
27
+
28
+ class TestUser(BaseUser):
29
+ @property
30
+ def display_name(self):
31
+ return "TestUser"
32
+
33
+ @property
34
+ def is_authenticated(self):
35
+ return True
36
+
37
+
38
+ class TestAuthBackend(AuthenticationBackend):
39
+ async def authenticate(self, conn):
40
+ if "user" in conn.session:
41
+ return AuthCredentials(["authenticated"]), TestUser()
42
+
43
+
44
+ async def authenticated_method(request, swl):
45
+ return swl.success()
46
+
47
+
48
+ async def unauthenticated_method(request, swl):
49
+ return swl.success()
50
+
51
+
52
+ async def page(request, swl, **kwargs):
53
+ return PlainTextResponse(f"Authenticated: {request.user.is_authenticated}")
54
+
55
+
56
+ async def login(request, swl, **kwargs):
57
+ request.session["user"] = True
58
+ return PlainTextResponse("OK")
59
+
60
+
61
+ async def logout(request, swl, **kwargs):
62
+ request.session.clear()
63
+ return PlainTextResponse("OK")
64
+
65
+
66
+ async def test_auth(tmp_path):
67
+
68
+ swl = SWL(config={})
69
+ swl.actions.register_method(authenticated_method)
70
+ swl.actions.register_method(unauthenticated_method, authenticated=False)
71
+
72
+ app = Starlette(
73
+ routes=[
74
+ swl.action_route,
75
+ Route("/secure", endpoint=swl.endpoint(page)),
76
+ Route(
77
+ "/unsecure",
78
+ endpoint=swl.endpoint(page, authenticated=False, redirect="/login"),
79
+ ),
80
+ Route("/login", endpoint=swl.endpoint(login, authenticated=False)),
81
+ Route("/logout", endpoint=swl.endpoint(logout, authenticated=False)),
82
+ ],
83
+ middleware=[
84
+ Middleware(SessionMiddleware, secret_key=str(uuid.uuid1())),
85
+ Middleware(
86
+ AuthenticationMiddleware,
87
+ backend=TestAuthBackend(),
88
+ ),
89
+ ],
90
+ )
91
+
92
+ client = TestClient(app)
93
+ response = client.post("/secure")
94
+ assert response.status_code == 403
95
+
96
+ response = client.post("/unsecure")
97
+ assert response.status_code == 200
98
+ assert response.text == "Authenticated: False"
99
+
100
+ response = client.post(swl.actions.register("authenticated_method"))
101
+ assert response.status_code == 403
102
+
103
+ response = client.post(swl.actions.register("unauthenticated_method"))
104
+ assert response.status_code == 204 # empty SuccessResponse
105
+
106
+ response = client.get("/login")
107
+ assert response.status_code == 200
108
+
109
+ response = client.post("/secure")
110
+ assert response.status_code == 200
111
+ assert response.text == "Authenticated: True"
112
+
113
+ response = client.post("/unsecure")
114
+ assert response.status_code == 200
115
+ assert response.text == "Authenticated: True"
116
+
117
+ response = client.post(swl.actions.register("authenticated_method"))
118
+ assert response.status_code == 204 # empty SuccessResponse
119
+
120
+ response = client.post(swl.actions.register("unauthenticated_method"))
121
+ assert response.status_code == 204 # empty SuccessResponse
swl/breadcrumbs.py ADDED
@@ -0,0 +1,52 @@
1
+ ## begin license ##
2
+ #
3
+ # "SWL"(Seecr Web Lib) makes developing webbased apps easier
4
+ #
5
+ # Copyright (C) 2024 Seecr (Seek You Too B.V.) https://seecr.nl
6
+ #
7
+ # This file is part of "Seecr Web Lib"
8
+ #
9
+ # "Seecr Web Lib" is licensed under the MIT License; see the LICENSE file.
10
+ # SPDX-License-Identifier: MIT
11
+ #
12
+ ## end license ##
13
+
14
+
15
+ class Crumb:
16
+ @classmethod
17
+ def from_dict(cls, dct):
18
+ return cls(**dct)
19
+
20
+ def __init__(self, label, action, arguments):
21
+ self.label = label
22
+ self.action = action
23
+ self.arguments = arguments
24
+
25
+ def __eq__(self, other):
26
+ return (
27
+ self.label == other.label
28
+ and self.action == other.action
29
+ and self.arguments == other.arguments
30
+ )
31
+
32
+ def as_dict(self):
33
+ return dict(label=self.label, action=self.action, arguments=self.arguments)
34
+
35
+
36
+ class Breadcrumbs:
37
+ def __init__(self, breadcrumbs):
38
+ self._crumbs = [Crumb.from_dict(crumb) for crumb in (breadcrumbs or [])]
39
+
40
+ def add(self, label, action, arguments=None):
41
+ new_crumb = Crumb(label=label, action=action, arguments=arguments or {})
42
+
43
+ if new_crumb in self._crumbs:
44
+ self._crumbs = self._crumbs[: self._crumbs.index(new_crumb) + 1]
45
+ else:
46
+ self._crumbs.append(new_crumb)
47
+
48
+ def __iter__(self):
49
+ return iter(self._crumbs)
50
+
51
+ def as_dict(self):
52
+ return [crumb.as_dict() for crumb in self._crumbs]
swl/config.py ADDED
@@ -0,0 +1,66 @@
1
+ ## begin license ##
2
+ #
3
+ # "SWL"(Seecr Web Lib) makes developing webbased apps easier
4
+ #
5
+ # Copyright (C) 2024-2025 Seecr (Seek You Too B.V.) https://seecr.nl
6
+ #
7
+ # This file is part of "Seecr Web Lib"
8
+ #
9
+ # "Seecr Web Lib" is licensed under the MIT License; see the LICENSE file.
10
+ # SPDX-License-Identifier: MIT
11
+ #
12
+ ## end license ##
13
+
14
+ import logging
15
+ from copy import copy
16
+
17
+ color_end = "\033[0m"
18
+ color_red = "\033[1;31m"
19
+ color_yellow = "\033[1;33m"
20
+
21
+ colors = {
22
+ "WARNING": color_yellow,
23
+ "ERROR": color_red,
24
+ "CRITICAL": color_red,
25
+ }
26
+
27
+
28
+ def colorize(levelname):
29
+ if levelname in colors:
30
+ return colors[levelname] + levelname + color_end
31
+ return levelname
32
+
33
+
34
+ class DefaultFormatter(logging.Formatter):
35
+ """
36
+ A custom log formatter class.
37
+
38
+ It does the following:
39
+ * Outputs the LOG_LEVEL in uppercase, right-aligned to 8 characters
40
+
41
+ Idea based on uvicorn.logging.DefaultFormatter
42
+ """
43
+
44
+ def formatMessage(self, record: logging.LogRecord) -> str:
45
+ recordcopy = copy(record)
46
+ levelname = recordcopy.levelname
47
+ seperator = " " * (8 - len(recordcopy.levelname))
48
+ recordcopy.__dict__["levelprefix"] = colorize(levelname) + ":" + seperator
49
+ return super().formatMessage(recordcopy)
50
+
51
+
52
+ def configure_logging(loglevel):
53
+
54
+ root = logging.getLogger()
55
+ sh = logging.StreamHandler()
56
+ df = DefaultFormatter("%(levelprefix)s %(message)s")
57
+ sh.setFormatter(df)
58
+ root.addHandler(sh)
59
+ root.setLevel(loglevel)
60
+
61
+ # set levels logging
62
+ logging.getLogger("uvicorn").setLevel("INFO")
63
+ logging.getLogger("python_multipart").setLevel("INFO")
64
+ logging.getLogger("sqlalchemy.engine").setLevel("WARNING")
65
+ logging.getLogger("aiosqlite").setLevel("WARNING")
66
+ logging.getLogger("databases").setLevel("WARNING")