Correctly configure auth
This commit is contained in:
parent
00a3f41389
commit
b437959230
9 changed files with 225 additions and 190 deletions
|
@ -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:
|
||||||
|
|
|
@ -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']
|
|
|
@ -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
|
|
|
@ -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 {}
|
|
|
@ -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
|
||||||
|
|
||||||
|
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/<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__':
|
if __name__ == '__main__':
|
||||||
argp = ArgumentParser(prog="hmdooris")
|
app.run(host='localhost', port=3000, server=GeventWebSocketServer, debug=config.debug, quiet=not config.debug)
|
||||||
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)
|
|
||||||
|
|
|
@ -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>
|
|
@ -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
106
poetry.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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)"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue