From b437959230e9de52ecd46e43fdbb1ca20208566f Mon Sep 17 00:00:00 2001 From: Stefan Bethke Date: Sat, 24 May 2025 13:01:41 +0200 Subject: [PATCH] Correctly configure auth --- hmdooris/AppConfig.py | 8 ++ hmdooris/CSRF.py | 13 --- hmdooris/Template.py | 20 ----- hmdooris/WebEndpoints.py | 112 ------------------------- hmdooris/__main__.py | 141 +++++++++++++++++++++++--------- hmdooris/templates/home.html.j2 | 6 +- hmdooris/updatepoller.py | 5 +- poetry.lock | 106 +++++++++++++++++++++++- pyproject.toml | 4 +- 9 files changed, 225 insertions(+), 190 deletions(-) delete mode 100644 hmdooris/CSRF.py delete mode 100644 hmdooris/Template.py delete mode 100644 hmdooris/WebEndpoints.py diff --git a/hmdooris/AppConfig.py b/hmdooris/AppConfig.py index 494957b..b38fd7b 100644 --- a/hmdooris/AppConfig.py +++ b/hmdooris/AppConfig.py @@ -9,6 +9,7 @@ class AppConfig: Gets the config from environment variables """ self.basepath = path.dirname(__file__) + self.debug = getenv("DEBUG", None) self.staticpath = path.join(self.basepath, "static") self.templatepath = path.join(self.basepath, "templates") self.app_email_base_uri = getenv('APP_EMAIL_BASE_URI', 'http://localhost:3000') @@ -18,10 +19,17 @@ class AppConfig: 'http://localhost:8080/realms/testing/.well-known/openid-configuration') self.client_id = getenv('HMDOORIS_CLIENT_ID', 'hmdooris') self.client_secret = getenv('HMDOORIS_CLIENT_SECRET') + self.oidc_scope = json.loads(getenv('IDINVITE_OIDC_SCOPE', '["openid", "email", "profile"]')) + self.oidc_user_attr = getenv('IDINVITE_OIDC_USER_ATTR', 'email') self.requires_group = getenv('HMDOORIS_REQUIRES_GROUP', None) self.ccujack_url = getenv('HMDOORIS_CCUJACK_URL', None) self.ccujack_username = getenv('HMDOORIS_CCUJACK_USERNAME', None) self.ccujack_password = getenv('HMDOORIS_CCUJACK_PASSWORD', None) + + if self.debug is not None and self.debug != "0" and self.debug.lower() != "false": + self.debug = True + else: + self.debug = False try: self.ccujack_locks = json.loads(getenv('HMDOORIS_CCUJACK_LOCKS', '[]')) except json.decoder.JSONDecodeError as e: diff --git a/hmdooris/CSRF.py b/hmdooris/CSRF.py deleted file mode 100644 index 955b92d..0000000 --- a/hmdooris/CSRF.py +++ /dev/null @@ -1,13 +0,0 @@ -import secrets - -from bottle import request - - -def get_token() -> str: - """ - Returns a string suitable as a csrf token. The token is stored in the session. - :return: - """ - if 'csrf_token' not in request.session: - request.session['csrf_token'] = secrets.token_urlsafe(16) - return request.session['csrf_token'] diff --git a/hmdooris/Template.py b/hmdooris/Template.py deleted file mode 100644 index 3855f08..0000000 --- a/hmdooris/Template.py +++ /dev/null @@ -1,20 +0,0 @@ -import logging -from typing import Callable - -from bottle import jinja2_template - -from hmdooris.AppException import AppException - - -def template_or_error(template: str): - def outer(f: Callable[[], str]): - def wrapper(): - try: - return jinja2_template(template, f()) - except AppException as e: - logging.error(f'unable to process request: {e}') - return jinja2_template('error', {'message': e}) - - return wrapper - - return outer diff --git a/hmdooris/WebEndpoints.py b/hmdooris/WebEndpoints.py deleted file mode 100644 index faa4136..0000000 --- a/hmdooris/WebEndpoints.py +++ /dev/null @@ -1,112 +0,0 @@ -import json -from threading import Thread -from time import sleep -from typing import Callable - -import BottleOIDC -from bottle import static_file, request, Bottle -from bottle_websocket import websocket - -from hmdooris import CSRF -from hmdooris.AppConfig import AppConfig -from hmdooris.Template import template_or_error -from hmdooris.ccujack import CCUJack - - - -class WebEndpoints: - """ - Defines endpoints for interaction with the user's browser. - """ - - def __init__(self, app: Bottle, auth: BottleOIDC, basepath: str, ccujack: CCUJack, config: AppConfig): - self.app = app - self.auth = auth - self.basepath = basepath - self.ccujack = ccujack - self.config = config - self.valid_username_re = '^[a-zA-Z0-9_-]+$' - self.clients = WebSocketClients() - Thread(target=self.poll, daemon=True).start() - - # set up routing directly, since decorators can only be used in a global scope - app.route(path='/static/', callback=self.static) - app.get(path='/', callback=template_or_error('home')(self.home)) - app.get(path='/ws', callback=websocket(self.websocket)) - app.get(path='/foo', callback=self.require_login(template_or_error('home')(self.home))) - app.get(path='/api', callback=self.api_get) - app.post(path='/api', callback=self.require_login(self.api_post), ) - - def poll(self): - previous = {} - while True: - # pull status and push to websocket - current = self.ccujack.get_locks() - update = {} - for lock in list(current.values()): - if lock["name"] not in previous or previous[lock["name"]]["status"] != lock["status"]: - update[lock["name"]] = lock - previous = current - self.clients.send({ - 'type': 'update', - 'locks': update, - }) - sleep(5) - - def require_login(self, func: Callable) -> Callable: - if self.config.requires_group is not None: - return self.auth.require_login(self.auth.require_attribute('groups', self.config.requires_group)(func)) - else: - return self.auth.require_login(func) - - def static(self, filename): - return static_file(filename, root=f'{self.basepath}/static') - - def home(self): - """ - Present the home page - :param username: - :return: - """ - return { - '_csrf': CSRF.get_token(), - # 'username': self.auth.my_attrs['username'], - } - - def websocket(self, ws): - while True: - if ws is None: - print("Unable to add empty websocket") - return - self.clients.add(ws) - # send current state to this client - m = ws.receive() - if m is None: - self.clients.remove(ws) - break - - def api_get(self): - """ - Interact with the HomeMatic CCU through ccu-jack - :return: - """ - - return { - '_csrf': CSRF.get_token(), - 'locks': self.ccujack.get_locks() - } - - def api_post(self): - """ - Interact withe HomeMatic CCU through ccu-jack - :return: - """ - # weird bug - if request.json is None: - data = json.loads(next(iter(request.POST.keys()))) - else: - data = request.json - # self.ccujack.update_lock(name) - print(f"name {data}") - self.ccujack.lock_unlock(data["name"]) - return {} diff --git a/hmdooris/__main__.py b/hmdooris/__main__.py index e0511cc..a916751 100644 --- a/hmdooris/__main__.py +++ b/hmdooris/__main__.py @@ -1,48 +1,111 @@ +""" +FastAPI main entry point +""" +import json import logging -from argparse import ArgumentParser -from os import getenv +from typing import Callable -import waitress from BottleOIDC import BottleOIDC +from BottleOIDC.bottle_utils import UnauthorizedError from BottleSessions import BottleSessions -from bottle import Bottle, TEMPLATE_PATH +from bottle import route, run, Bottle, static_file, TEMPLATE_PATH, jinja2_view, post, get, request +from bottle_log import LoggingPlugin +from bottle_websocket import websocket, GeventWebSocketServer +from geventwebsocket.websocket import WebSocket from hmdooris.AppConfig import AppConfig -from hmdooris.WebEndpoints import WebEndpoints from hmdooris.ccujack import CCUJack +from hmdooris.updatepoller import UpdatePoller +from hmdooris.websocketcomm import WebSocketClients + +config = AppConfig() +if config.debug: + logging.basicConfig(level=logging.DEBUG) + +ccujack = CCUJack(config.ccujack_url, config.ccujack_username, config.ccujack_password, config.ccujack_locks) + +app = Bottle() +if config.debug: + app.config.update({"logging.level": "DEBUG"}) +app.install(LoggingPlugin(app.config)) +TEMPLATE_PATH.insert(0, config.templatepath) +app.install(BottleSessions()) +auth = BottleOIDC(app, config={ + "discovery_url": config.discovery_url, + "client_id": config.client_id, + "client_secret": config.client_secret, + "client_scope": config.oidc_scope, + "user_attr": config.oidc_user_attr, +}) + +websocket_clients = WebSocketClients() +update_poller = UpdatePoller(websocket_clients, ccujack, 1 if config.debug else 0.1) + +def require_login(func: Callable) -> Callable: + if config.requires_group is not None: + return auth.require_login(auth.require_attribute('groups', config.requires_group)(func)) + else: + return auth.require_login(func) + +def require_authz(func: Callable) -> Callable: + if config.requires_group is not None: + return auth.require_attribute('groups', config.requires_group)(func) + else: + def _outer_wrapper(f): + def _wrapper(*args, **kwargs): + if auth.my_username is not None: + return f(*args, **kwargs) + + return UnauthorizedError('Not Authorized') + + _wrapper.__name__ = f.__name__ + return _wrapper + + return _outer_wrapper(func) + + +@app.route("/static/") +def server_static(filepath): + return static_file(filepath, root=config.staticpath) + + +@app.get("/") +@jinja2_view("home.html.j2") +def root(): + return {} + +@app.get("/operate") +@require_login +@jinja2_view("operate.html.j2") +def root(): + return {} + + +@app.get('/ws', apply=[websocket]) +def websocket_endpoint(ws: WebSocket): + try: + websocket_clients.add(ws) + ws.send(json.dumps(update_poller.get_locks(True))) + while True: + m = ws.receive() + except Exception as e: + logging.debug("error in websocket", exc_info=e) + pass + finally: + websocket_clients.remove(ws) + +@app.get('/api/lock') +def get_api_lock(): + return update_poller.get_locks(True) + +@app.get('/api/lock/') +def get_api_lock(id): + return update_poller.get_lock(id) + +@app.post('/api/lock/') +@require_authz +def post_api_lock(id): + return ccujack.lock_unlock(request.json["id"], None) if __name__ == '__main__': - argp = ArgumentParser(prog="hmdooris") - argp.add_argument('-d', '--debug', action='store_true') - args = argp.parse_args() - - if args.debug: - logging.basicConfig(level=logging.DEBUG) - - config = AppConfig() - - app = Bottle() - TEMPLATE_PATH.insert(0, f'{config.basepath}/views') - - BottleSessions(app) - auth = BottleOIDC(app, config={ - "discovery_url": config.discovery_url, - "client_id": config.client_id, - "client_secret": config.client_secret, - }) - ccujack = CCUJack(config.ccujack_url, config.ccujack_username, config.ccujack_password, config.ccujack_locks) - WebEndpoints(app, auth, config.basepath, ccujack, config) - - bottle_params = { - 'debug': bool(getenv('BOTTLE_DEBUG', 'False')) or args.debug, - } - waitress_params = { - 'host': getenv('BOTTLE_HOST', 'localhost'), - 'port': int(getenv('BOTTLE_PORT', '3000')), - 'trusted_proxy': getenv('BOTTLE_TRUSTED_PROXY', ''), - 'trusted_proxy_headers': getenv('BOTTLE_TRUSTED_PROXY_HEADERS', ''), - } - if getenv('BOTTLE_URL_SCHEME') is not None: - waitress_params['url_scheme'] = getenv('BOTTLE_URL_SCHEME') - app.config.update(**bottle_params) - waitress.serve(app, **waitress_params) + app.run(host='localhost', port=3000, server=GeventWebSocketServer, debug=config.debug, quiet=not config.debug) diff --git a/hmdooris/templates/home.html.j2 b/hmdooris/templates/home.html.j2 index 07e77c8..9ee6deb 100644 --- a/hmdooris/templates/home.html.j2 +++ b/hmdooris/templates/home.html.j2 @@ -4,10 +4,12 @@ HM Dooris - +{# #} -

HM Dooris

+

HM Dooris Status

+

Shows the status of the logs

+Lock or unlock a door \ No newline at end of file diff --git a/hmdooris/updatepoller.py b/hmdooris/updatepoller.py index c08c0b0..a9c93c8 100644 --- a/hmdooris/updatepoller.py +++ b/hmdooris/updatepoller.py @@ -8,17 +8,18 @@ from hmdooris.websocketcomm import WebSocketClients class UpdatePoller: - def __init__(self, wsc: WebSocketClients, ccu: CCUJack): + def __init__(self, wsc: WebSocketClients, ccu: CCUJack, update_delay = 1.0): self.wsc = wsc self.ccu = ccu self.current = {} + self.update_delay = update_delay self.log = logging.getLogger(__name__) Thread(target=self.run, daemon=True).start() def run(self): while True: self.send_locks() - sleep(1) + sleep(self.update_delay) def get_lock(self, id:str): return self.ccu.get_locks()[id] diff --git a/poetry.lock b/poetry.lock index 463d92b..35d2ad4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -57,6 +57,21 @@ files = [ {file = "bottle-0.13.3.tar.gz", hash = "sha256:1c23aeb30aa8a13f39c60c0da494530ddd5de3da235bc431b818a50d999de49f"}, ] +[[package]] +name = "bottle-log" +version = "1.0.0" +description = "Improved logging for Bottle." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "bottle-log-1.0.0.tar.gz", hash = "sha256:b5f03b8d64bfd577dea9b23d9cb999df078916be6951c5700644e14caa294f37"}, + {file = "bottle_log-1.0.0-py2.py3-none-any.whl", hash = "sha256:c549e3d1e612b7b8e7ed0ab684676b94007e764b5789cbb2c3eda0d4ae78e2ff"}, +] + +[package.dependencies] +bottle = ">=0.10.0" + [[package]] name = "bottle-websocket" version = "0.2.9" @@ -610,6 +625,24 @@ files = [ [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + [[package]] name = "jwcrypto" version = "1.5.6" @@ -626,6 +659,77 @@ files = [ cryptography = ">=3.4" typing-extensions = ">=4.5.0" +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + [[package]] name = "packaging" version = "25.0" @@ -867,4 +971,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"] [metadata] lock-version = "2.1" python-versions = ">=3.13,<4.0" -content-hash = "baf1a64dcdb7402d0d1dab3bb67fdafa4620499aadfed1e5ea9aa4125caf90e7" +content-hash = "1cd04a86b43b8f694998f367e70ff2b73fc51ebb0eb44eb4f05ca811d96fbc44" diff --git a/pyproject.toml b/pyproject.toml index 9e7b308..db90869 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,9 @@ requires-python = ">=3.13,<4.0" dependencies = [ "bottle-websocket (>=0.2.9,<0.3.0)", "bottleoidc (>=21.8.30,<22.0.0)", - "python-keycloak (>=5.5.0,<6.0.0)" + "python-keycloak (>=5.5.0,<6.0.0)", + "bottle-log (>=1.0.0,<2.0.0)", + "jinja2 (>=3.1.6,<4.0.0)" ]