Correctly configure auth

This commit is contained in:
Stefan Bethke 2025-05-24 13:01:41 +02:00
commit b437959230
9 changed files with 225 additions and 190 deletions

View file

@ -9,6 +9,7 @@ class AppConfig:
Gets the config from environment variables Gets the config from environment variables
""" """
self.basepath = path.dirname(__file__) self.basepath = path.dirname(__file__)
self.debug = getenv("DEBUG", None)
self.staticpath = path.join(self.basepath, "static") self.staticpath = path.join(self.basepath, "static")
self.templatepath = path.join(self.basepath, "templates") self.templatepath = path.join(self.basepath, "templates")
self.app_email_base_uri = getenv('APP_EMAIL_BASE_URI', 'http://localhost:3000') 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') 'http://localhost:8080/realms/testing/.well-known/openid-configuration')
self.client_id = getenv('HMDOORIS_CLIENT_ID', 'hmdooris') self.client_id = getenv('HMDOORIS_CLIENT_ID', 'hmdooris')
self.client_secret = getenv('HMDOORIS_CLIENT_SECRET') 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.requires_group = getenv('HMDOORIS_REQUIRES_GROUP', None)
self.ccujack_url = getenv('HMDOORIS_CCUJACK_URL', None) self.ccujack_url = getenv('HMDOORIS_CCUJACK_URL', None)
self.ccujack_username = getenv('HMDOORIS_CCUJACK_USERNAME', None) self.ccujack_username = getenv('HMDOORIS_CCUJACK_USERNAME', None)
self.ccujack_password = getenv('HMDOORIS_CCUJACK_PASSWORD', 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: try:
self.ccujack_locks = json.loads(getenv('HMDOORIS_CCUJACK_LOCKS', '[]')) self.ccujack_locks = json.loads(getenv('HMDOORIS_CCUJACK_LOCKS', '[]'))
except json.decoder.JSONDecodeError as e: except json.decoder.JSONDecodeError as e:

View file

@ -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']

View file

@ -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

View file

@ -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/<filename>', 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 {}

View file

@ -1,48 +1,111 @@
"""
FastAPI main entry point
"""
import json
import logging import logging
from argparse import ArgumentParser from typing import Callable
from os import getenv
import waitress
from BottleOIDC import BottleOIDC from BottleOIDC import BottleOIDC
from BottleOIDC.bottle_utils import UnauthorizedError
from BottleSessions import BottleSessions 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.AppConfig import AppConfig
from hmdooris.WebEndpoints import WebEndpoints
from hmdooris.ccujack import CCUJack from hmdooris.ccujack import CCUJack
from hmdooris.updatepoller import UpdatePoller
from hmdooris.websocketcomm import WebSocketClients
if __name__ == '__main__': config = AppConfig()
argp = ArgumentParser(prog="hmdooris") if config.debug:
argp.add_argument('-d', '--debug', action='store_true')
args = argp.parse_args()
if args.debug:
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
config = AppConfig() ccujack = CCUJack(config.ccujack_url, config.ccujack_username, config.ccujack_password, config.ccujack_locks)
app = Bottle() app = Bottle()
TEMPLATE_PATH.insert(0, f'{config.basepath}/views') if config.debug:
app.config.update({"logging.level": "DEBUG"})
BottleSessions(app) app.install(LoggingPlugin(app.config))
auth = BottleOIDC(app, config={ TEMPLATE_PATH.insert(0, config.templatepath)
app.install(BottleSessions())
auth = BottleOIDC(app, config={
"discovery_url": config.discovery_url, "discovery_url": config.discovery_url,
"client_id": config.client_id, "client_id": config.client_id,
"client_secret": config.client_secret, "client_secret": config.client_secret,
}) "client_scope": config.oidc_scope,
ccujack = CCUJack(config.ccujack_url, config.ccujack_username, config.ccujack_password, config.ccujack_locks) "user_attr": config.oidc_user_attr,
WebEndpoints(app, auth, config.basepath, ccujack, config) })
bottle_params = { websocket_clients = WebSocketClients()
'debug': bool(getenv('BOTTLE_DEBUG', 'False')) or args.debug, update_poller = UpdatePoller(websocket_clients, ccujack, 1 if config.debug else 0.1)
}
waitress_params = { def require_login(func: Callable) -> Callable:
'host': getenv('BOTTLE_HOST', 'localhost'), if config.requires_group is not None:
'port': int(getenv('BOTTLE_PORT', '3000')), return auth.require_login(auth.require_attribute('groups', config.requires_group)(func))
'trusted_proxy': getenv('BOTTLE_TRUSTED_PROXY', ''), else:
'trusted_proxy_headers': getenv('BOTTLE_TRUSTED_PROXY_HEADERS', ''), return auth.require_login(func)
}
if getenv('BOTTLE_URL_SCHEME') is not None: def require_authz(func: Callable) -> Callable:
waitress_params['url_scheme'] = getenv('BOTTLE_URL_SCHEME') if config.requires_group is not None:
app.config.update(**bottle_params) return auth.require_attribute('groups', config.requires_group)(func)
waitress.serve(app, **waitress_params) 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/<filepath>")
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/<id>')
def get_api_lock(id):
return update_poller.get_lock(id)
@app.post('/api/lock/<id>')
@require_authz
def post_api_lock(id):
return ccujack.lock_unlock(request.json["id"], None)
if __name__ == '__main__':
app.run(host='localhost', port=3000, server=GeventWebSocketServer, debug=config.debug, quiet=not config.debug)

View file

@ -4,10 +4,12 @@
<title>HM Dooris</title> <title>HM Dooris</title>
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
<link rel=stylesheet type="text/css" href="static/main.css"> <link rel=stylesheet type="text/css" href="static/main.css">
<script src="static/main.js" defer></script> {# <script src="static/main.js" defer></script>#}
</head> </head>
<body> <body>
<h1>HM Dooris</h1> <h1>HM Dooris Status</h1>
<p>Shows the status of the logs</p>
<div id="locks"></div> <div id="locks"></div>
<a href="operate">Lock or unlock a door</a>
</body> </body>
</html> </html>

View file

@ -8,17 +8,18 @@ from hmdooris.websocketcomm import WebSocketClients
class UpdatePoller: class UpdatePoller:
def __init__(self, wsc: WebSocketClients, ccu: CCUJack): def __init__(self, wsc: WebSocketClients, ccu: CCUJack, update_delay = 1.0):
self.wsc = wsc self.wsc = wsc
self.ccu = ccu self.ccu = ccu
self.current = {} self.current = {}
self.update_delay = update_delay
self.log = logging.getLogger(__name__) self.log = logging.getLogger(__name__)
Thread(target=self.run, daemon=True).start() Thread(target=self.run, daemon=True).start()
def run(self): def run(self):
while True: while True:
self.send_locks() self.send_locks()
sleep(1) sleep(self.update_delay)
def get_lock(self, id:str): def get_lock(self, id:str):
return self.ccu.get_locks()[id] return self.ccu.get_locks()[id]

106
poetry.lock generated
View file

@ -57,6 +57,21 @@ files = [
{file = "bottle-0.13.3.tar.gz", hash = "sha256:1c23aeb30aa8a13f39c60c0da494530ddd5de3da235bc431b818a50d999de49f"}, {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]] [[package]]
name = "bottle-websocket" name = "bottle-websocket"
version = "0.2.9" version = "0.2.9"
@ -610,6 +625,24 @@ files = [
[package.extras] [package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 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]] [[package]]
name = "jwcrypto" name = "jwcrypto"
version = "1.5.6" version = "1.5.6"
@ -626,6 +659,77 @@ files = [
cryptography = ">=3.4" cryptography = ">=3.4"
typing-extensions = ">=4.5.0" 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]] [[package]]
name = "packaging" name = "packaging"
version = "25.0" version = "25.0"
@ -867,4 +971,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"]
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.13,<4.0" python-versions = ">=3.13,<4.0"
content-hash = "baf1a64dcdb7402d0d1dab3bb67fdafa4620499aadfed1e5ea9aa4125caf90e7" content-hash = "1cd04a86b43b8f694998f367e70ff2b73fc51ebb0eb44eb4f05ca811d96fbc44"

View file

@ -11,7 +11,9 @@ requires-python = ">=3.13,<4.0"
dependencies = [ dependencies = [
"bottle-websocket (>=0.2.9,<0.3.0)", "bottle-websocket (>=0.2.9,<0.3.0)",
"bottleoidc (>=21.8.30,<22.0.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)"
] ]