Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b1160862a | ||
|
|
58242117f6 | ||
|
|
6e6ae3ccb7 | ||
|
|
5b87d9ff03 | ||
|
|
91eaef7f93 | ||
|
|
53081dd553 | ||
|
|
2fb357ae77 | ||
|
|
931243dd60 | ||
|
|
7ac4e6c3a7 | ||
|
|
9aed6b5227 | ||
|
|
8e32727fcd | ||
|
|
80757fa1d8 | ||
|
|
4a03b5c1a1 | ||
|
|
fc24ba068d | ||
|
|
cc3d064e64 | ||
|
|
ca023e705c | ||
|
|
dc85c578a2 |
No files matched your search
@@ -19,5 +19,15 @@
|
|||||||
"postCreateCommand": "pip install -U pip && pip install -U -r requirements.txt -r requirements-dev.txt",
|
"postCreateCommand": "pip install -U pip && pip install -U -r requirements.txt -r requirements-dev.txt",
|
||||||
"forwardPorts": [
|
"forwardPorts": [
|
||||||
8757
|
8757
|
||||||
]
|
],
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"ms-python.python"
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"python.defaultInterpreterPath": "/opt/venv/bin/python"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.git
|
||||||
|
.devcontainer
|
||||||
|
.vscode
|
||||||
|
.claude
|
||||||
|
docs
|
||||||
|
log
|
||||||
|
**/__pycache__
|
||||||
|
**/*.pyc
|
||||||
@@ -1,2 +1 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
log/
|
|
||||||
+28
-7
@@ -1,20 +1,41 @@
|
|||||||
|
# Build stage: compile the wheels (psycopg2 needs gcc and libpq-dev)
|
||||||
|
FROM python:3.15-slim-trixie AS build
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install --no-install-recommends -y gcc libc-dev libpq-dev && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
|
||||||
|
|
||||||
|
# Runtime stage: no compiler and no tools besides what db-setup.sh needs
|
||||||
FROM python:3.15-slim-trixie
|
FROM python:3.15-slim-trixie
|
||||||
|
|
||||||
RUN apt -y update && apt -y upgrade && \
|
RUN apt-get update && apt-get upgrade -y && \
|
||||||
apt install --no-install-recommends -y wait-for-it lsb-release wget gnupg2 gcc libpq-dev libc-dev vim postgresql-client && apt clean all
|
apt-get install --no-install-recommends -y libpq5 postgresql-client wait-for-it && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY --from=build /wheels /wheels
|
||||||
|
RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels
|
||||||
|
|
||||||
|
RUN groupadd --system --gid 10001 max && \
|
||||||
|
useradd --system --uid 10001 --gid max --no-create-home --shell /usr/sbin/nologin max
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY requirements.txt .
|
|
||||||
RUN pip install -U -r requirements.txt
|
|
||||||
|
|
||||||
RUN mkdir log
|
|
||||||
|
|
||||||
ENV PYTHONPATH=/app/
|
ENV PYTHONPATH=/app/
|
||||||
ENV FLASK_APP=max.app
|
ENV FLASK_APP=max.app
|
||||||
|
# Refuses to start in debug mode, see max/app.py
|
||||||
|
ENV MAX_PRODUCTION=1
|
||||||
|
|
||||||
|
# Owned by root, so the app cannot modify its own code
|
||||||
COPY max/ ./max
|
COPY max/ ./max
|
||||||
COPY scripts/prod/ .
|
COPY scripts/prod/ .
|
||||||
|
# Precompile, since the app cannot write __pycache__ at runtime
|
||||||
|
RUN python -m compileall -q max
|
||||||
|
|
||||||
|
USER max
|
||||||
|
|
||||||
ENTRYPOINT ["./entrypoint.sh"]
|
ENTRYPOINT ["./entrypoint.sh"]
|
||||||
CMD ["gunicorn", "-w", "1", "-b", "0.0.0.0:8000", "max.app:app"]
|
CMD ["gunicorn", "-w", "1", "-b", "0.0.0.0:8000", "max.app:app"]
|
||||||
@@ -6,15 +6,31 @@ clear-dev:
|
|||||||
scripts/dev/clear-db.sh
|
scripts/dev/clear-db.sh
|
||||||
|
|
||||||
run-dev:
|
run-dev:
|
||||||
FLASK_APP=max.app FLASK_DEBUG=1 flask run --host 0.0.0.0 --port 8757
|
FLASK_APP=max.app FLASK_DEBUG=1 flask run --host 127.0.0.1 --port 8757
|
||||||
|
|
||||||
run-dev-as-prod:
|
run-dev-as-prod:
|
||||||
mkdir -p log
|
FLASK_APP=max.app MAX_ORIGIN=http://localhost:8757 gunicorn -w 1 -b 127.0.0.1:8757 max.app:app
|
||||||
FLASK_APP=max.app LOG_DIR=log gunicorn -w 1 -b 0.0.0.0:8757 max.app:app
|
|
||||||
|
|
||||||
run-cleanup:
|
run-cleanup:
|
||||||
scripts/dev/cleanup.sh
|
scripts/dev/cleanup.sh
|
||||||
|
|
||||||
|
# DEPENDENCIES
|
||||||
|
PIP_COMPILE = pip-compile --quiet --generate-hashes --strip-extras --allow-unsafe
|
||||||
|
|
||||||
|
lock-deps:
|
||||||
|
$(PIP_COMPILE) requirements.in
|
||||||
|
$(PIP_COMPILE) requirements-dev.in
|
||||||
|
|
||||||
|
update-deps:
|
||||||
|
$(PIP_COMPILE) --upgrade requirements.in
|
||||||
|
$(PIP_COMPILE) --upgrade requirements-dev.in
|
||||||
|
|
||||||
|
sync-deps:
|
||||||
|
pip-sync requirements.txt requirements-dev.txt
|
||||||
|
|
||||||
|
audit:
|
||||||
|
pip-audit --strict -r requirements.txt
|
||||||
|
|
||||||
# PROD
|
# PROD
|
||||||
build-prod:
|
build-prod:
|
||||||
docker build -t max:$(shell cat VERSION) .
|
docker build -t max:$(shell cat VERSION) .
|
||||||
|
|||||||
@@ -9,3 +9,20 @@ The proxy should add X-Real-IP to get a better logging.
|
|||||||
Requirements: Docker, Make
|
Requirements: Docker, Make
|
||||||
|
|
||||||
Start with `make run-dev`
|
Start with `make run-dev`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
- `MAX_ORIGIN` (required in production): The public origin of MAX, e.g.
|
||||||
|
`https://max.finn.st`
|
||||||
|
- `MAX_PRODUCTION=1` is set in the Docker image and makes MAX refuse to start
|
||||||
|
in debug mode (`FLASK_DEBUG`). Do not override it.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
Direct dependencies are listed in `requirements.in` (production) and
|
||||||
|
`requirements-dev.in` (development tools). `requirements.txt` and
|
||||||
|
`requirements-dev.txt` are lockfiles with exact versions and hashes
|
||||||
|
|
||||||
|
- After changing a `.in` file: `make lock-deps` and `make sync-deps`
|
||||||
|
- Update all dependencies: `make update-deps` and `make sync-deps`
|
||||||
|
- Check for known vulnerabilities: `make audit` (before every release)
|
||||||
@@ -10,6 +10,19 @@ services:
|
|||||||
- PGSQL_USER=max
|
- PGSQL_USER=max
|
||||||
- PGSQL_PASSWORD=max
|
- PGSQL_PASSWORD=max
|
||||||
- PGSQL_NAME=max
|
- PGSQL_NAME=max
|
||||||
|
- MAX_ORIGIN=https://max.finn.st
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /tmp # gunicorn worker heartbeat files
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "5"
|
||||||
networks:
|
networks:
|
||||||
- postgresql
|
- postgresql
|
||||||
- uplink
|
- uplink
|
||||||
|
|||||||
+14
-1
@@ -16,6 +16,7 @@ from .middlewares import (
|
|||||||
add_middleware_stack,
|
add_middleware_stack,
|
||||||
)
|
)
|
||||||
from .routes import init_routes
|
from .routes import init_routes
|
||||||
|
from .security_headers import init_security_headers
|
||||||
from .sessions import SessionInterface
|
from .sessions import SessionInterface
|
||||||
from .translations import TranslationHandler
|
from .translations import TranslationHandler
|
||||||
|
|
||||||
@@ -29,6 +30,10 @@ def main():
|
|||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
init_logging(app)
|
init_logging(app)
|
||||||
app.logger.info("Max started.")
|
app.logger.info("Max started.")
|
||||||
|
if app.debug and os.environ.get("MAX_PRODUCTION") == "1":
|
||||||
|
raise RuntimeError(
|
||||||
|
"Debug mode is not allowed in production (MAX_PRODUCTION=1), unset FLASK_DEBUG"
|
||||||
|
)
|
||||||
if app.debug:
|
if app.debug:
|
||||||
app.logger.info("!! App is in debug mode !!")
|
app.logger.info("!! App is in debug mode !!")
|
||||||
else:
|
else:
|
||||||
@@ -37,12 +42,19 @@ def main():
|
|||||||
app.jinja_env.trim_blocks = True
|
app.jinja_env.trim_blocks = True
|
||||||
app.jinja_env.lstrip_blocks = True
|
app.jinja_env.lstrip_blocks = True
|
||||||
app.url_rule_class = Rule
|
app.url_rule_class = Rule
|
||||||
|
max_origin = os.environ.get("MAX_ORIGIN")
|
||||||
|
if not max_origin and not app.debug:
|
||||||
|
raise RuntimeError(
|
||||||
|
"MAX_ORIGIN must be set to the public origin, e.g. https://max.example.com"
|
||||||
|
)
|
||||||
app.config.update(
|
app.config.update(
|
||||||
|
MAX_ORIGIN=max_origin.rstrip("/") if max_origin else None,
|
||||||
SECRET_KEY=(
|
SECRET_KEY=(
|
||||||
"123456" if app.debug else base64.b64encode(os.urandom(64)).decode("utf-8")
|
"123456" if app.debug else base64.b64encode(os.urandom(64)).decode("utf-8")
|
||||||
),
|
),
|
||||||
|
SESSION_COOKIE_NAME="session" if app.debug else "__Host-session",
|
||||||
SESSION_COOKIE_SECURE=not app.debug,
|
SESSION_COOKIE_SECURE=not app.debug,
|
||||||
SESSION_COOKIE_SAMESITE=False,
|
SESSION_COOKIE_SAMESITE="Lax",
|
||||||
)
|
)
|
||||||
add_middleware_stack(
|
add_middleware_stack(
|
||||||
app,
|
app,
|
||||||
@@ -57,6 +69,7 @@ def main():
|
|||||||
register_cli(app)
|
register_cli(app)
|
||||||
init_auth(app)
|
init_auth(app)
|
||||||
init_routes(app)
|
init_routes(app)
|
||||||
|
init_security_headers(app)
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+19
-4
@@ -6,22 +6,31 @@ from flask import session
|
|||||||
|
|
||||||
from max.crypto import constant_time_compare, force_bytes
|
from max.crypto import constant_time_compare, force_bytes
|
||||||
from max.db import get_user_by_email, update_user
|
from max.db import get_user_by_email, update_user
|
||||||
|
from max.sessions import session_storage
|
||||||
from max.translations import t
|
from max.translations import t
|
||||||
|
|
||||||
SSHA512_PREFIX = "{SSHA512}"
|
SSHA512_PREFIX = "{SSHA512}"
|
||||||
|
LOGIN_FAILED_MESSAGE = "Email or password is wrong, or the account is deactivated"
|
||||||
|
|
||||||
|
|
||||||
def login(email, password):
|
def login(email, password):
|
||||||
|
if not isinstance(email, str) or not isinstance(password, str):
|
||||||
|
return t(LOGIN_FAILED_MESSAGE)
|
||||||
|
if not email or not password:
|
||||||
|
return t(LOGIN_FAILED_MESSAGE)
|
||||||
|
|
||||||
user = get_user_by_email(email)
|
user = get_user_by_email(email)
|
||||||
if user is None:
|
if user is None:
|
||||||
return t("Email or password was wrong!")
|
return t(LOGIN_FAILED_MESSAGE)
|
||||||
|
|
||||||
if not check_password(password, user.passwordhash):
|
if not check_password(password, user.passwordhash):
|
||||||
return t("Email or password was wrong!")
|
return t(LOGIN_FAILED_MESSAGE)
|
||||||
|
|
||||||
if not user.enabled:
|
if not user.enabled:
|
||||||
return t("This account is not enabled!")
|
return t(LOGIN_FAILED_MESSAGE)
|
||||||
|
|
||||||
|
# Start a new session (new id, new CSRF token) against session fixation.
|
||||||
|
session.clear()
|
||||||
session["user_id"] = user.id
|
session["user_id"] = user.id
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -33,7 +42,13 @@ def logout():
|
|||||||
def change_password(user, password):
|
def change_password(user, password):
|
||||||
hash = hash_password(password)
|
hash = hash_password(password)
|
||||||
update_user(user.id, passwordhash=hash)
|
update_user(user.id, passwordhash=hash)
|
||||||
session.set_new_id()
|
if session.get("user_id") == user.id:
|
||||||
|
# Own password: keep this session (with a new id), end all others
|
||||||
|
session.regenerate()
|
||||||
|
session_storage.remove_user_sessions(user.id, except_id=session.id)
|
||||||
|
else:
|
||||||
|
# An admin changes the password of another user: end all of their sessions
|
||||||
|
session_storage.remove_user_sessions(user.id)
|
||||||
|
|
||||||
|
|
||||||
def hash_password(password: str, salt: bytes = None):
|
def hash_password(password: str, salt: bytes = None):
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ class AuthMiddleware(BaseMiddleware):
|
|||||||
if not getattr(self.handler, "no_auth", False) and not auth_user:
|
if not getattr(self.handler, "no_auth", False) and not auth_user:
|
||||||
logout() # remove the session (if any)
|
logout() # remove the session (if any)
|
||||||
# No usage of flash here to not create a session...
|
# No usage of flash here to not create a session...
|
||||||
return redirect(url_for("login", a=1))
|
return redirect(url_for("login", msg="login_required"))
|
||||||
|
|
||||||
if auth_user:
|
if auth_user:
|
||||||
if not auth_user.enabled:
|
if not auth_user.enabled:
|
||||||
logout()
|
logout()
|
||||||
return redirect(url_for("login", a=1))
|
return redirect(url_for("login", msg="login_required"))
|
||||||
|
|
||||||
kwargs["auth_user"] = auth_user
|
kwargs["auth_user"] = auth_user
|
||||||
return self.handler(*args, **kwargs)
|
return self.handler(*args, **kwargs)
|
||||||
|
|||||||
+14
-4
@@ -8,6 +8,12 @@ from max.views import BaseTemplateGetView, BaseView
|
|||||||
from .auth import login, logout
|
from .auth import login, logout
|
||||||
from .middlewares import no_authentication_required
|
from .middlewares import no_authentication_required
|
||||||
|
|
||||||
|
# Messages for the login page (text, category), selected by ?msg=<key>.
|
||||||
|
LOGIN_MESSAGES = {
|
||||||
|
"login_required": ("Please login to visit the requested url", "error"),
|
||||||
|
"logout": ("Logout was sucessfull!", "success"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def init_routes(app):
|
def init_routes(app):
|
||||||
app.add_url_rule(
|
app.add_url_rule(
|
||||||
@@ -26,7 +32,11 @@ class Login(AllowAnyAccess, BaseTemplateGetView):
|
|||||||
|
|
||||||
def get_context(self):
|
def get_context(self):
|
||||||
context = super().get_context()
|
context = super().get_context()
|
||||||
context["alert"] = bool(self.request.args.get("a", False))
|
if "message" not in context:
|
||||||
|
message = LOGIN_MESSAGES.get(self.request.args.get("msg"))
|
||||||
|
if message is not None:
|
||||||
|
context["message"] = t(message[0])
|
||||||
|
context["message_category"] = message[1]
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_webpagetitle(self):
|
def get_webpagetitle(self):
|
||||||
@@ -41,7 +51,8 @@ class Login(AllowAnyAccess, BaseTemplateGetView):
|
|||||||
self.app.logger.info(
|
self.app.logger.info(
|
||||||
"Failed login attempt from %s", self.request.real_remote_addr
|
"Failed login attempt from %s", self.request.real_remote_addr
|
||||||
)
|
)
|
||||||
flash(error, category="error")
|
self.context["message"] = error
|
||||||
|
self.context["message_category"] = "error"
|
||||||
self.context["email"] = email
|
self.context["email"] = email
|
||||||
else:
|
else:
|
||||||
flash(t("Login was sucessfull!"), category="success")
|
flash(t("Login was sucessfull!"), category="success")
|
||||||
@@ -51,5 +62,4 @@ class Login(AllowAnyAccess, BaseTemplateGetView):
|
|||||||
class Logout(AllowAnyAccess, BaseView):
|
class Logout(AllowAnyAccess, BaseView):
|
||||||
def post(self):
|
def post(self):
|
||||||
logout()
|
logout()
|
||||||
flash(t("Logout was sucessfull!"), category="success")
|
return self.redirect(url_for("login", msg="logout"))
|
||||||
return self.redirect(url_for("login"))
|
|
||||||
+46
-15
@@ -3,39 +3,70 @@ from functools import wraps
|
|||||||
from flask import request, session
|
from flask import request, session
|
||||||
|
|
||||||
from max.crypto import constant_time_compare, get_random_string
|
from max.crypto import constant_time_compare, get_random_string
|
||||||
from max.middlewares import BaseMiddleware
|
from max.middlewares import BaseMiddleware, get_real_remote_addr
|
||||||
|
|
||||||
CSRF_TOKEN_KEY = "csrf_token"
|
CSRF_TOKEN_KEY = "csrf_token"
|
||||||
CSRF_TOKEN_LENGTH = 32
|
CSRF_TOKEN_LENGTH = 32
|
||||||
|
SAFE_METHODS = ("GET", "HEAD", "OPTIONS")
|
||||||
|
# "none": navigation started by the user (e.g. reload). "same-site" is rejected,
|
||||||
|
# since other services on a subdomain must not trigger requests.
|
||||||
|
ALLOWED_FETCH_SITES = ("same-origin", "none")
|
||||||
|
|
||||||
|
|
||||||
class CsrfMiddleware(BaseMiddleware):
|
class CsrfMiddleware(BaseMiddleware):
|
||||||
def __call__(self, *args, **kwargs):
|
def __call__(self, *args, **kwargs):
|
||||||
error = self.process_request()
|
error = self.process_request()
|
||||||
if error is not None:
|
if error is not None:
|
||||||
# clear csrf from session
|
return error
|
||||||
if CSRF_TOKEN_KEY in session:
|
|
||||||
del session[CSRF_TOKEN_KEY]
|
|
||||||
return error, 400
|
|
||||||
|
|
||||||
return self.handler(*args, **kwargs)
|
return self.handler(*args, **kwargs)
|
||||||
|
|
||||||
def process_request(self):
|
def process_request(self):
|
||||||
"""If a string is returned, it mist be used as a 400er error"""
|
"""If a (message, status) tuple is returned, it must be used as the response"""
|
||||||
if getattr(self.handler, "no_csrf", False):
|
no_csrf = getattr(self.handler, "no_csrf", False)
|
||||||
|
|
||||||
|
if request.method in SAFE_METHODS:
|
||||||
|
if not no_csrf and CSRF_TOKEN_KEY not in session:
|
||||||
|
session[CSRF_TOKEN_KEY] = get_random_string(CSRF_TOKEN_LENGTH)
|
||||||
return
|
return
|
||||||
|
|
||||||
session_has_csrf_token = CSRF_TOKEN_KEY in session
|
# Stateless check, so it also protects routes without a session (login)
|
||||||
method_is_unsafe = request.method.lower() != "get"
|
if not self.is_origin_allowed():
|
||||||
|
self.app.logger.debug(
|
||||||
|
'Blocked cross-origin request %s %s origin="%s" sec_fetch_site="%s" from %s',
|
||||||
|
request.method,
|
||||||
|
request.path,
|
||||||
|
request.headers.get("Origin"),
|
||||||
|
request.headers.get("Sec-Fetch-Site"),
|
||||||
|
get_real_remote_addr(self.app),
|
||||||
|
)
|
||||||
|
return "Cross-origin request blocked", 403
|
||||||
|
|
||||||
if not session_has_csrf_token and method_is_unsafe:
|
if no_csrf:
|
||||||
return "CSRF token is missing"
|
return
|
||||||
|
|
||||||
elif not session_has_csrf_token and not method_is_unsafe:
|
if CSRF_TOKEN_KEY not in session:
|
||||||
session[CSRF_TOKEN_KEY] = get_random_string(CSRF_TOKEN_LENGTH)
|
return "CSRF token is missing", 400
|
||||||
|
|
||||||
elif session_has_csrf_token and method_is_unsafe:
|
error = self.check_token(session[CSRF_TOKEN_KEY], request.form)
|
||||||
return self.check_token(session[CSRF_TOKEN_KEY], request.form)
|
if error is not None:
|
||||||
|
return error, 400
|
||||||
|
|
||||||
|
def is_origin_allowed(self):
|
||||||
|
fetch_site = request.headers.get("Sec-Fetch-Site")
|
||||||
|
if fetch_site is not None:
|
||||||
|
return fetch_site in ALLOWED_FETCH_SITES
|
||||||
|
|
||||||
|
origin = request.headers.get("Origin")
|
||||||
|
if origin is None:
|
||||||
|
return True # not a browser, so no CSRF possible
|
||||||
|
return origin == self.get_expected_origin()
|
||||||
|
|
||||||
|
def get_expected_origin(self):
|
||||||
|
origin = self.app.config["MAX_ORIGIN"]
|
||||||
|
if origin is None:
|
||||||
|
origin = request.host_url.rstrip("/")
|
||||||
|
return origin
|
||||||
|
|
||||||
def check_token(self, session_token, formdata):
|
def check_token(self, session_token, formdata):
|
||||||
if CSRF_TOKEN_KEY not in formdata:
|
if CSRF_TOKEN_KEY not in formdata:
|
||||||
|
|||||||
+8
-3
@@ -10,7 +10,12 @@ class NotFound(MaxException):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SessionWithoutUser(MaxException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PermissionDenied(MaxException):
|
class PermissionDenied(MaxException):
|
||||||
def __init__(self, message):
|
"""
|
||||||
super().__init__()
|
Deliberately answered with a 404, like NotFound, so nobody can tell
|
||||||
self.message = message
|
whether the object exists.
|
||||||
|
"""
|
||||||
+8
-22
@@ -1,37 +1,23 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
from logging.config import dictConfig
|
from logging.config import dictConfig
|
||||||
|
|
||||||
|
|
||||||
def init_logging(app):
|
def init_logging(app):
|
||||||
handlers = {
|
|
||||||
"stdout": {
|
|
||||||
"class": "logging.StreamHandler",
|
|
||||||
"stream": sys.stdout,
|
|
||||||
"formatter": "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
active_handlers = ["stdout"]
|
|
||||||
if not app.debug:
|
|
||||||
handlers["file"] = {
|
|
||||||
"class": "logging.handlers.RotatingFileHandler",
|
|
||||||
"filename": os.path.join(os.environ.get("LOG_DIR", "/app/log"), "max.log"),
|
|
||||||
"formatter": "default",
|
|
||||||
"maxBytes": 1024 * 1024,
|
|
||||||
"backupCount": 5,
|
|
||||||
}
|
|
||||||
active_handlers.append("file")
|
|
||||||
|
|
||||||
dictConfig(
|
dictConfig(
|
||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"formatters": {
|
"formatters": {
|
||||||
"default": {"format": "[%(asctime)s] [%(levelname)s]: %(message)s"}
|
"default": {"format": "[%(asctime)s] [%(levelname)s]: %(message)s"}
|
||||||
},
|
},
|
||||||
"handlers": handlers,
|
"handlers": {
|
||||||
"root": {"level": "INFO", "handlers": active_handlers},
|
"stdout": {
|
||||||
"gunicorn": {"level": "INFO", "handlers": active_handlers},
|
"class": "logging.StreamHandler",
|
||||||
|
"stream": sys.stdout,
|
||||||
|
"formatter": "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"root": {"level": "INFO", "handlers": ["stdout"]},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
logger = logging.getLogger(app.name)
|
logger = logging.getLogger(app.name)
|
||||||
|
|||||||
+8
-4
@@ -20,12 +20,16 @@ class BaseMiddleware:
|
|||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
def get_real_remote_addr(app):
|
||||||
|
real_ip = request.headers.get("X-Real-IP", None)
|
||||||
|
if real_ip is None and app.debug:
|
||||||
|
real_ip = request.remote_addr
|
||||||
|
return real_ip
|
||||||
|
|
||||||
|
|
||||||
class LoggingMiddleware(BaseMiddleware):
|
class LoggingMiddleware(BaseMiddleware):
|
||||||
def __call__(self, *args, **kwargs):
|
def __call__(self, *args, **kwargs):
|
||||||
real_ip = request.headers.get("X-Real-IP", None)
|
setattr(request, "real_remote_addr", get_real_remote_addr(self.app))
|
||||||
if real_ip is None and self.app.debug:
|
|
||||||
real_ip = request.remote_addr
|
|
||||||
setattr(request, "real_remote_addr", real_ip)
|
|
||||||
|
|
||||||
start = time.time()
|
start = time.time()
|
||||||
|
|
||||||
|
|||||||
@@ -38,8 +38,7 @@
|
|||||||
"Delete alias {alias}": "Lösche Alias {alias}",
|
"Delete alias {alias}": "Lösche Alias {alias}",
|
||||||
"Alias {alias} for {user}": "Alias {alias} für {user}",
|
"Alias {alias} for {user}": "Alias {alias} für {user}",
|
||||||
"Edit note for alias {alias}": "Notiz für Alias {alias} bearbeiten",
|
"Edit note for alias {alias}": "Notiz für Alias {alias} bearbeiten",
|
||||||
"Email or password was wrong!": "Email oder Password ist falsch!",
|
"Email or password is wrong, or the account is deactivated": "E-Mail oder Passwort falsch, oder der Account ist deaktiviert",
|
||||||
"This account is not enabled!": "Dieser Account ist nicht aktiviert!",
|
|
||||||
"Login was sucessfull!": "Erfolgreich angemeldet!",
|
"Login was sucessfull!": "Erfolgreich angemeldet!",
|
||||||
"Logout was sucessfull!": "Erfolgreich abgemeldet!",
|
"Logout was sucessfull!": "Erfolgreich abgemeldet!",
|
||||||
"The email is not valid": "Die Email ist nicht valide",
|
"The email is not valid": "Die Email ist nicht valide",
|
||||||
@@ -51,6 +50,9 @@
|
|||||||
"Note changed successfully!": "Notiz wurde erfolgreich bearbeitet!",
|
"Note changed successfully!": "Notiz wurde erfolgreich bearbeitet!",
|
||||||
"Both passwords must be given": "Beide Passwörter müssen gegeben sein",
|
"Both passwords must be given": "Beide Passwörter müssen gegeben sein",
|
||||||
"Both passwords must be the same": "Beide Passwörter müssen übereinstimmen",
|
"Both passwords must be the same": "Beide Passwörter müssen übereinstimmen",
|
||||||
|
"Current password": "Aktuelles Passwort",
|
||||||
|
"The current password must be given": "Das aktuelle Passwort muss gegeben sein",
|
||||||
|
"The current password is wrong": "Das aktuelle Passwort ist falsch",
|
||||||
"Password changed successfully!": "Passwort erfolgreich geändert!",
|
"Password changed successfully!": "Passwort erfolgreich geändert!",
|
||||||
"Admin is not set": "Admin ist nicht gesetzt",
|
"Admin is not set": "Admin ist nicht gesetzt",
|
||||||
"Creation of {email} was successful!": "{email} wurde erfolgreich erstellt!",
|
"Creation of {email} was successful!": "{email} wurde erfolgreich erstellt!",
|
||||||
@@ -64,6 +66,9 @@
|
|||||||
"Used as an user.": "Wird für einen Nutzer verwendet",
|
"Used as an user.": "Wird für einen Nutzer verwendet",
|
||||||
"Used as an alias by {alias}.": "Word für den Alias {alias} verwendet",
|
"Used as an alias by {alias}.": "Word für den Alias {alias} verwendet",
|
||||||
"Invalid domain": "Invalide Domain",
|
"Invalid domain": "Invalide Domain",
|
||||||
|
"Invalid data": "Ungültige Daten",
|
||||||
|
"The email is too long": "Die E-Mail ist zu lang",
|
||||||
"Can use different alias domains": "Kann unterschiedliche Aliasdomains benutzen",
|
"Can use different alias domains": "Kann unterschiedliche Aliasdomains benutzen",
|
||||||
"Can use different alias domains is not set": "Kann unterschiedliche Aliasdomains benutzen ist nicht gesetzt"
|
"Can use different alias domains is not set": "Kann unterschiedliche Aliasdomains benutzen ist nicht gesetzt",
|
||||||
|
"This address is reserved and can only be created by an admin.": "Diese Adresse ist reserviert und kann nur von einem Admin angelegt werden."
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@ def init_routes(app):
|
|||||||
UserToggleAdmin.as_view(),
|
UserToggleAdmin.as_view(),
|
||||||
)
|
)
|
||||||
app.add_url_rule(
|
app.add_url_rule(
|
||||||
"/user/<int:user_id>/ser-toggle-can-use-different-alias-domain",
|
"/user/<int:user_id>/toggle-can-use-different-alias-domain",
|
||||||
"user-toggle-can-use-different-alias-domain",
|
"user-toggle-can-use-different-alias-domain",
|
||||||
UserToggleCanUseDifferentAliasDomain.as_view(),
|
UserToggleCanUseDifferentAliasDomain.as_view(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
from flask import flash
|
from flask import flash, url_for
|
||||||
|
|
||||||
from max.db import EmailAlreadyExists, create_alias, get_domains
|
from max.db import EmailAlreadyExists, create_alias, get_domains
|
||||||
from max.permissions import AllowAdminOrSelf
|
from max.permissions import AllowAdminOrSelf
|
||||||
from max.translations import t
|
from max.translations import t
|
||||||
|
|
||||||
from ..base_alias_views import BackToAliasesMixin
|
|
||||||
from ..base_user_views import FetchUserTemplateGetView
|
from ..base_user_views import FetchUserTemplateGetView
|
||||||
from ..check_email_mixin import CheckEmailMixin
|
from ..check_email_mixin import CheckEmailMixin
|
||||||
|
|
||||||
|
|
||||||
class AliasCreate(
|
class AliasCreate(AllowAdminOrSelf, CheckEmailMixin, FetchUserTemplateGetView):
|
||||||
AllowAdminOrSelf, CheckEmailMixin, BackToAliasesMixin, FetchUserTemplateGetView
|
|
||||||
):
|
|
||||||
template_name = "alias/create.html"
|
template_name = "alias/create.html"
|
||||||
|
|
||||||
def get_webpagetitle(self):
|
def get_webpagetitle(self):
|
||||||
return f"Create new alias for {self.user.email}"
|
return f"Create new alias for {self.user.email}"
|
||||||
|
|
||||||
|
def get_back_text_and_url(self):
|
||||||
|
return self.user.email, url_for("user-detail", user_id=self.user.id)
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
error = False
|
error = False
|
||||||
email = self.request.form.get("email")
|
email = self.request.form.get("email")
|
||||||
@@ -39,9 +39,16 @@ class AliasCreate(
|
|||||||
else:
|
else:
|
||||||
self.domain = self.get_user_domain()
|
self.domain = self.get_user_domain()
|
||||||
|
|
||||||
full_email = f"{self.email}@{self.domain}"
|
if not error:
|
||||||
if not self.is_email_valid(full_email):
|
full_email = f"{self.email}@{self.domain}"
|
||||||
error = True
|
if not self.is_email_valid(full_email):
|
||||||
|
error = True
|
||||||
|
elif not self.auth_user.is_admin and self.is_reserved_email(full_email):
|
||||||
|
flash(
|
||||||
|
t("This address is reserved and can only be created by an admin."),
|
||||||
|
category="error",
|
||||||
|
)
|
||||||
|
error = True
|
||||||
|
|
||||||
enabled = self.request.form.get("enabled")
|
enabled = self.request.form.get("enabled")
|
||||||
if enabled == "on":
|
if enabled == "on":
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class AliasEditNote(AllowAdminOrSelf, BackToAliasesMixin, FetchAliasTemplateGetV
|
|||||||
def post(self):
|
def post(self):
|
||||||
note = self.request.form.get("note", "")
|
note = self.request.form.get("note", "")
|
||||||
if not isinstance(note, str):
|
if not isinstance(note, str):
|
||||||
flash("invalid data", category="error")
|
flash(t("Invalid data"), category="error")
|
||||||
return
|
return
|
||||||
|
|
||||||
update_alias(self.alias.id, note=note)
|
update_alias(self.alias.id, note=note)
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ from max.db import get_alias_with_user_by_id
|
|||||||
from max.exceptions import NotFound
|
from max.exceptions import NotFound
|
||||||
from max.views import BaseTemplateGetView
|
from max.views import BaseTemplateGetView
|
||||||
|
|
||||||
|
from .return_to_mixin import RETURN_DETAIL, ReturnToMixin
|
||||||
|
|
||||||
|
|
||||||
class FetchAliasMixin:
|
class FetchAliasMixin:
|
||||||
def before_handler(self):
|
def before_handler(self):
|
||||||
@@ -21,9 +23,9 @@ class FetchAliasTemplateGetView(FetchAliasMixin, BaseTemplateGetView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class BackToAliasesMixin:
|
class BackToAliasesMixin(ReturnToMixin):
|
||||||
def get_back_text_and_url(self):
|
def get_back_text_and_url(self):
|
||||||
if self.request.args.get("return") == "detail":
|
if self.get_return_to() == RETURN_DETAIL:
|
||||||
return self.user.email, url_for("alias-detail", alias_id=self.alias.id)
|
return self.user.email, url_for("alias-detail", alias_id=self.alias.id)
|
||||||
else:
|
else:
|
||||||
return self.user.email, url_for("user-detail", user_id=self.user.id)
|
return self.user.email, url_for("user-detail", user_id=self.user.id)
|
||||||
@@ -5,6 +5,8 @@ from max.exceptions import NotFound
|
|||||||
from max.translations import t
|
from max.translations import t
|
||||||
from max.views import BaseTemplateGetView
|
from max.views import BaseTemplateGetView
|
||||||
|
|
||||||
|
from .return_to_mixin import RETURN_DETAIL, ReturnToMixin
|
||||||
|
|
||||||
|
|
||||||
class FetchUserMixin:
|
class FetchUserMixin:
|
||||||
def before_handler(self):
|
def before_handler(self):
|
||||||
@@ -20,9 +22,9 @@ class FetchUserTemplateGetView(FetchUserMixin, BaseTemplateGetView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class BackToUsersMixin:
|
class BackToUsersMixin(ReturnToMixin):
|
||||||
def get_back_text_and_url(self):
|
def get_back_text_and_url(self):
|
||||||
if self.request.args.get("return") == "detail":
|
if self.get_return_to() == RETURN_DETAIL:
|
||||||
return self.user.email, url_for("user-detail", user_id=self.user.id)
|
return self.user.email, url_for("user-detail", user_id=self.user.id)
|
||||||
else:
|
else:
|
||||||
return t("User list"), url_for("user-list")
|
return t("User list"), url_for("user-list")
|
||||||
@@ -5,14 +5,43 @@ from flask import flash
|
|||||||
from max.db import get_domains
|
from max.db import get_domains
|
||||||
from max.translations import t
|
from max.translations import t
|
||||||
|
|
||||||
|
# RFC 5321, both below the varchar(255) of emails.email
|
||||||
|
MAX_LOCAL_PART_LENGTH = 64
|
||||||
|
MAX_EMAIL_LENGTH = 254
|
||||||
|
|
||||||
|
RESERVED_LOCAL_PARTS = frozenset(
|
||||||
|
(
|
||||||
|
"admin",
|
||||||
|
"administrator",
|
||||||
|
"webmaster",
|
||||||
|
"hostmaster",
|
||||||
|
"postmaster",
|
||||||
|
"abuse",
|
||||||
|
"security",
|
||||||
|
"noc",
|
||||||
|
"root",
|
||||||
|
"mailer-daemon",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CheckEmailMixin:
|
class CheckEmailMixin:
|
||||||
def is_email_valid(self, email: str):
|
def is_email_valid(self, email: str):
|
||||||
"""Checks for the email being valid and that it has a valid domain."""
|
"""Checks for the email being valid and that it has a valid domain."""
|
||||||
regex = re.compile(f"^[a-zA-Z0-9+_-]+@({'|'.join(get_domains())})$")
|
local_part = email.split("@")[0]
|
||||||
|
if len(local_part) > MAX_LOCAL_PART_LENGTH or len(email) > MAX_EMAIL_LENGTH:
|
||||||
|
flash(t("The email is too long"), category="error")
|
||||||
|
return False
|
||||||
|
|
||||||
|
domains = "|".join(map(re.escape, get_domains()))
|
||||||
|
regex = re.compile(f"^[a-zA-Z0-9+_-]+@({domains})$")
|
||||||
match = regex.match(email)
|
match = regex.match(email)
|
||||||
if not match:
|
if not match:
|
||||||
flash(t("The email is not valid"), category="error")
|
flash(t("The email is not valid"), category="error")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def is_reserved_email(self, email: str):
|
||||||
|
local_part = email.split("@")[0]
|
||||||
|
return local_part.lower() in RESERVED_LOCAL_PARTS
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
RETURN_DETAIL = "detail"
|
||||||
|
|
||||||
|
|
||||||
|
class ReturnToMixin:
|
||||||
|
"""
|
||||||
|
Reads the `return` query parameter, which tells where to go back to
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_return_to(self):
|
||||||
|
if self.request.args.get("return") == RETURN_DETAIL:
|
||||||
|
return RETURN_DETAIL
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_context(self):
|
||||||
|
context = super().get_context()
|
||||||
|
context["return_to"] = self.get_return_to()
|
||||||
|
return context
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
from flask import flash, session
|
from flask import flash
|
||||||
|
|
||||||
from max.auth import change_password
|
from max.auth import change_password, check_password
|
||||||
from max.permissions import AllowAdminOrSelf
|
from max.permissions import AllowAdminOrSelf
|
||||||
from max.sessions import session_storage
|
|
||||||
from max.translations import t
|
from max.translations import t
|
||||||
|
|
||||||
from ..base_user_views import BackToUsersMixin, FetchUserTemplateGetView
|
from ..base_user_views import BackToUsersMixin, FetchUserTemplateGetView
|
||||||
@@ -14,20 +13,38 @@ class UserChangePassword(AllowAdminOrSelf, BackToUsersMixin, FetchUserTemplateGe
|
|||||||
def get_webpagetitle(self):
|
def get_webpagetitle(self):
|
||||||
return t("Change password for {user}", user=self.user.email)
|
return t("Change password for {user}", user=self.user.email)
|
||||||
|
|
||||||
|
def is_own_password(self):
|
||||||
|
return self.user.id == self.auth_user.id
|
||||||
|
|
||||||
|
def get_context(self):
|
||||||
|
context = super().get_context()
|
||||||
|
context["is_own_password"] = self.is_own_password()
|
||||||
|
return context
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
pw1 = self.request.form.get("password1")
|
pw1 = self.request.form.get("password1")
|
||||||
pw2 = self.request.form.get("password2")
|
pw2 = self.request.form.get("password2")
|
||||||
if not pw1 or not pw2:
|
if not pw1 or not pw2:
|
||||||
flash(t("Both passwords must be given"), category="error")
|
flash(t("Both passwords must be given"), category="error")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not isinstance(pw1, str) or not isinstance(pw2, str):
|
if not isinstance(pw1, str) or not isinstance(pw2, str):
|
||||||
flash("Invalid data")
|
flash(t("Invalid data"), category="error")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if self.is_own_password():
|
||||||
|
current_password = self.request.form.get("current_password")
|
||||||
|
if not current_password or not isinstance(current_password, str):
|
||||||
|
flash(t("The current password must be given"), category="error")
|
||||||
|
return
|
||||||
|
if not check_password(current_password, self.user.passwordhash):
|
||||||
|
flash(t("The current password is wrong"), category="error")
|
||||||
|
return
|
||||||
|
|
||||||
if pw1 != pw2:
|
if pw1 != pw2:
|
||||||
flash(t("Both passwords must be the same"), category="error")
|
flash(t("Both passwords must be the same"), category="error")
|
||||||
return
|
return
|
||||||
|
|
||||||
change_password(self.user, pw1)
|
change_password(self.user, pw1)
|
||||||
session_storage.remove_user_sessions(self.user.id, except_id=session["user_id"])
|
|
||||||
flash(t("Password changed successfully!"), category="success")
|
flash(t("Password changed successfully!"), category="success")
|
||||||
return self.redirect()
|
return self.redirect()
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from flask import flash
|
from flask import flash, url_for
|
||||||
|
|
||||||
from max.auth import hash_password
|
from max.auth import hash_password
|
||||||
from max.db import EmailAlreadyExists, create_user, get_domains
|
from max.db import EmailAlreadyExists, create_user, get_domains
|
||||||
@@ -6,11 +6,10 @@ from max.permissions import AllowAdmin
|
|||||||
from max.translations import t
|
from max.translations import t
|
||||||
from max.views import BaseTemplateGetView
|
from max.views import BaseTemplateGetView
|
||||||
|
|
||||||
from ..base_user_views import BackToUsersMixin
|
|
||||||
from ..check_email_mixin import CheckEmailMixin
|
from ..check_email_mixin import CheckEmailMixin
|
||||||
|
|
||||||
|
|
||||||
class UserCreate(AllowAdmin, CheckEmailMixin, BackToUsersMixin, BaseTemplateGetView):
|
class UserCreate(AllowAdmin, CheckEmailMixin, BaseTemplateGetView):
|
||||||
template_name = "user/create.html"
|
template_name = "user/create.html"
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
@@ -32,9 +31,10 @@ class UserCreate(AllowAdmin, CheckEmailMixin, BackToUsersMixin, BaseTemplateGetV
|
|||||||
else:
|
else:
|
||||||
self.domain = domain
|
self.domain = domain
|
||||||
|
|
||||||
full_email = f"{self.email}@{self.domain}"
|
if not error:
|
||||||
if not self.is_email_valid(full_email):
|
full_email = f"{self.email}@{self.domain}"
|
||||||
error = True
|
if not self.is_email_valid(full_email):
|
||||||
|
error = True
|
||||||
|
|
||||||
pw1 = self.request.form.get("password1")
|
pw1 = self.request.form.get("password1")
|
||||||
pw2 = self.request.form.get("password2")
|
pw2 = self.request.form.get("password2")
|
||||||
@@ -42,7 +42,7 @@ class UserCreate(AllowAdmin, CheckEmailMixin, BackToUsersMixin, BaseTemplateGetV
|
|||||||
flash(t("Both passwords must be given"), category="error")
|
flash(t("Both passwords must be given"), category="error")
|
||||||
error = True
|
error = True
|
||||||
elif not isinstance(pw1, str) or not isinstance(pw2, str):
|
elif not isinstance(pw1, str) or not isinstance(pw2, str):
|
||||||
flash("Invalid data")
|
flash(t("Invalid data"), category="error")
|
||||||
error = True
|
error = True
|
||||||
elif pw1 != pw2:
|
elif pw1 != pw2:
|
||||||
flash(t("Both passwords must be the same"), category="error")
|
flash(t("Both passwords must be the same"), category="error")
|
||||||
@@ -120,7 +120,6 @@ class UserCreate(AllowAdmin, CheckEmailMixin, BackToUsersMixin, BaseTemplateGetV
|
|||||||
for key in (
|
for key in (
|
||||||
"email",
|
"email",
|
||||||
"domain",
|
"domain",
|
||||||
"password",
|
|
||||||
"enabled",
|
"enabled",
|
||||||
"is_admin",
|
"is_admin",
|
||||||
"can_use_different_alias_domain",
|
"can_use_different_alias_domain",
|
||||||
@@ -133,3 +132,6 @@ class UserCreate(AllowAdmin, CheckEmailMixin, BackToUsersMixin, BaseTemplateGetV
|
|||||||
|
|
||||||
def get_webpagetitle(self):
|
def get_webpagetitle(self):
|
||||||
return t("Create new user")
|
return t("Create new user")
|
||||||
|
|
||||||
|
def get_back_text_and_url(self):
|
||||||
|
return t("User list"), url_for("user-list")
|
||||||
@@ -16,7 +16,7 @@ class UserEditNote(AllowAdmin, BackToUsersMixin, FetchUserTemplateGetView):
|
|||||||
def post(self):
|
def post(self):
|
||||||
note = self.request.form.get("note", "")
|
note = self.request.form.get("note", "")
|
||||||
if not isinstance(note, str):
|
if not isinstance(note, str):
|
||||||
flash("invalid data", category="error")
|
flash(t("Invalid data"), category="error")
|
||||||
return
|
return
|
||||||
|
|
||||||
update_user(self.user.id, note=note)
|
update_user(self.user.id, note=note)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from flask import flash
|
|||||||
|
|
||||||
from max.db import is_last_enabled_admin, update_user
|
from max.db import is_last_enabled_admin, update_user
|
||||||
from max.permissions import AllowAdmin
|
from max.permissions import AllowAdmin
|
||||||
|
from max.sessions import session_storage
|
||||||
from max.translations import t
|
from max.translations import t
|
||||||
from max.views import BaseView
|
from max.views import BaseView
|
||||||
|
|
||||||
@@ -20,6 +21,8 @@ class UserToggleEnabled(AllowAdmin, FetchUserMixin, BackToUsersMixin, BaseView):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
update_user(self.user.id, enabled=(not self.user.enabled))
|
update_user(self.user.id, enabled=(not self.user.enabled))
|
||||||
|
if self.user.enabled: # the user was deactivated
|
||||||
|
session_storage.remove_user_sessions(self.user.id)
|
||||||
|
|
||||||
_, back_url = self.get_back_text_and_url()
|
_, back_url = self.get_back_text_and_url()
|
||||||
return self.redirect(back_url)
|
return self.redirect(back_url)
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
CONTENT_SECURITY_POLICY = "; ".join(
|
||||||
|
(
|
||||||
|
"default-src 'none'",
|
||||||
|
"style-src 'self'",
|
||||||
|
"img-src 'self'",
|
||||||
|
"font-src 'self'",
|
||||||
|
"form-action 'self'",
|
||||||
|
"frame-ancestors 'none'",
|
||||||
|
"base-uri 'none'",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
HSTS_MAX_AGE = 7 * 24 * 60 * 60 # in seconds
|
||||||
|
|
||||||
|
|
||||||
|
def init_security_headers(app):
|
||||||
|
headers = {
|
||||||
|
"Content-Security-Policy": CONTENT_SECURITY_POLICY,
|
||||||
|
"X-Frame-Options": "DENY",
|
||||||
|
"X-Content-Type-Options": "nosniff",
|
||||||
|
"Referrer-Policy": "same-origin",
|
||||||
|
}
|
||||||
|
if not app.debug:
|
||||||
|
headers["Strict-Transport-Security"] = f"max-age={HSTS_MAX_AGE}"
|
||||||
|
|
||||||
|
# after_request also covers static files, 404 and error responses
|
||||||
|
@app.after_request
|
||||||
|
def set_security_headers(response):
|
||||||
|
response.headers.update(headers)
|
||||||
|
return response
|
||||||
+95
-40
@@ -1,78 +1,102 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
from flask import request
|
||||||
from flask.sessions import SessionInterface as FlaskSessionInterface, SessionMixin
|
from flask.sessions import SessionInterface as FlaskSessionInterface, SessionMixin
|
||||||
from itsdangerous import BadSignature, Signer, want_bytes
|
from itsdangerous import BadSignature, Signer, want_bytes
|
||||||
from werkzeug.datastructures import CallbackDict
|
from werkzeug.datastructures import CallbackDict
|
||||||
|
|
||||||
from max.crypto import get_random_string
|
from max.crypto import get_random_string
|
||||||
|
from max.exceptions import SessionWithoutUser
|
||||||
|
|
||||||
|
# in seconds
|
||||||
|
SESSION_IDLE_TIMEOUT = 30 * 60
|
||||||
|
SESSION_ABSOLUTE_TIMEOUT = 12 * 60 * 60
|
||||||
|
|
||||||
|
# Keys for the session metadata in the session storage
|
||||||
|
CREATED_AT_KEY = "_created_at"
|
||||||
|
LAST_SEEN_KEY = "_last_seen"
|
||||||
|
|
||||||
|
|
||||||
|
SESSION_MAX_PER_USER = 10
|
||||||
|
|
||||||
|
|
||||||
class SessionStorage:
|
class SessionStorage:
|
||||||
def __init__(self, maxcap=100):
|
def __init__(self):
|
||||||
self.maxcap = maxcap
|
|
||||||
self.lru_ids = [] # newest ids are on the end (so do append to add a fresh id)
|
|
||||||
self.data = {}
|
self.data = {}
|
||||||
|
|
||||||
def __getitem__(self, id):
|
def __getitem__(self, id):
|
||||||
if id not in self.data:
|
return self.data.get(id)
|
||||||
return None
|
|
||||||
|
|
||||||
self._refresh_id(id)
|
|
||||||
return self.data[id]
|
|
||||||
|
|
||||||
def __delitem__(self, id):
|
def __delitem__(self, id):
|
||||||
if id in self.data:
|
self.data.pop(id, None)
|
||||||
del self.data[id]
|
|
||||||
self._remove_id(id)
|
|
||||||
|
|
||||||
def __setitem__(self, id, data):
|
def __setitem__(self, id, data):
|
||||||
self._refresh_id(id)
|
if data.get("user_id") is None:
|
||||||
|
raise SessionWithoutUser("Refusing to store a session without user_id")
|
||||||
|
|
||||||
|
if id not in self.data:
|
||||||
|
self._limit_user_sessions(data["user_id"])
|
||||||
self.data[id] = data
|
self.data[id] = data
|
||||||
|
|
||||||
while len(self.lru_ids) > self.maxcap:
|
def _limit_user_sessions(self, user_id):
|
||||||
removed_id = self.lru_ids.pop(0) # remove first element
|
"""Makes room for one new session by removing the least recently used ones"""
|
||||||
del self.data[removed_id]
|
user_ids = [id for id, data in self.data.items() if data["user_id"] == user_id]
|
||||||
|
user_ids.sort(key=lambda id: self.data[id][LAST_SEEN_KEY])
|
||||||
def _remove_id(self, id):
|
for id in user_ids[: max(0, len(user_ids) - SESSION_MAX_PER_USER + 1)]:
|
||||||
self.lru_ids = [_id for _id in self.lru_ids if _id != id]
|
del self.data[id]
|
||||||
|
|
||||||
def _refresh_id(self, id):
|
|
||||||
self._remove_id(id)
|
|
||||||
self.lru_ids.append(id)
|
|
||||||
|
|
||||||
def remove_user_sessions(self, user_id, except_id=None):
|
def remove_user_sessions(self, user_id, except_id=None):
|
||||||
ids_to_remove = []
|
ids_to_remove = []
|
||||||
for id, data in self.data.items():
|
for id, data in self.data.items():
|
||||||
if data.get("user_id", None) == user_id and id != except_id:
|
if data["user_id"] == user_id and id != except_id:
|
||||||
ids_to_remove.append(id)
|
ids_to_remove.append(id)
|
||||||
|
|
||||||
for id in ids_to_remove:
|
for id in ids_to_remove:
|
||||||
del self.data[id]
|
del self.data[id]
|
||||||
self._remove_id(id)
|
|
||||||
|
|
||||||
|
|
||||||
session_storage = SessionStorage()
|
session_storage = SessionStorage()
|
||||||
|
|
||||||
|
|
||||||
class Session(CallbackDict, SessionMixin):
|
class Session(CallbackDict, SessionMixin):
|
||||||
def __init__(self, id=None, data=None):
|
def __init__(self, id=None, data=None, created_at=None, last_seen=None):
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
self.modified = True
|
self.modified = True
|
||||||
if not self.id:
|
if not self.id:
|
||||||
self.set_new_id()
|
self.set_new_id()
|
||||||
|
|
||||||
self.id = id
|
self.id = id
|
||||||
|
self.created_at = created_at
|
||||||
|
self.last_seen = last_seen
|
||||||
self.modified = False
|
self.modified = False
|
||||||
CallbackDict.__init__(self, data, on_update)
|
CallbackDict.__init__(self, data, on_update)
|
||||||
|
|
||||||
def is_empty(self):
|
def is_empty(self):
|
||||||
return len(self) == 0
|
return len(self) == 0
|
||||||
|
|
||||||
|
def is_expired(self, now):
|
||||||
|
return (
|
||||||
|
now - self.created_at > SESSION_ABSOLUTE_TIMEOUT
|
||||||
|
or now - self.last_seen > SESSION_IDLE_TIMEOUT
|
||||||
|
)
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
|
"""Removes all data, including the entry in the session storage"""
|
||||||
|
del session_storage[self.id]
|
||||||
self.id = None
|
self.id = None
|
||||||
|
self.created_at = None
|
||||||
|
self.last_seen = None
|
||||||
super().clear()
|
super().clear()
|
||||||
|
|
||||||
def set_new_id(self):
|
def set_new_id(self):
|
||||||
self.id = get_random_string(32)
|
self.id = get_random_string(32)
|
||||||
|
|
||||||
|
def regenerate(self):
|
||||||
|
"""Keeps the data under a new id. The old id becomes invalid."""
|
||||||
|
del session_storage[self.id]
|
||||||
|
self.set_new_id()
|
||||||
|
self.modified = True
|
||||||
|
|
||||||
|
|
||||||
class SessionInterface(FlaskSessionInterface):
|
class SessionInterface(FlaskSessionInterface):
|
||||||
def _get_signer(self, app):
|
def _get_signer(self, app):
|
||||||
@@ -84,7 +108,14 @@ class SessionInterface(FlaskSessionInterface):
|
|||||||
return app.config["SESSION_COOKIE_NAME"]
|
return app.config["SESSION_COOKIE_NAME"]
|
||||||
|
|
||||||
def make_session_object(self, session_id=None, data=None):
|
def make_session_object(self, session_id=None, data=None):
|
||||||
return Session(session_id, data=data)
|
created_at = last_seen = None
|
||||||
|
if data is not None:
|
||||||
|
data = dict(data) # do not modify the entry in the session storage
|
||||||
|
created_at = data.pop(CREATED_AT_KEY)
|
||||||
|
last_seen = data.pop(LAST_SEEN_KEY)
|
||||||
|
return Session(
|
||||||
|
session_id, data=data, created_at=created_at, last_seen=last_seen
|
||||||
|
)
|
||||||
|
|
||||||
def open_session(self, app, request):
|
def open_session(self, app, request):
|
||||||
session_id = request.cookies.get(self.get_cookie_name(app))
|
session_id = request.cookies.get(self.get_cookie_name(app))
|
||||||
@@ -104,30 +135,54 @@ class SessionInterface(FlaskSessionInterface):
|
|||||||
session_id = session_id.decode("utf-8", "strict")
|
session_id = session_id.decode("utf-8", "strict")
|
||||||
|
|
||||||
data = session_storage[session_id]
|
data = session_storage[session_id]
|
||||||
return self.make_session_object(session_id, data=data)
|
if data is None:
|
||||||
|
# Never adopt an id chosen by the client (session fixation)
|
||||||
|
return self.make_session_object()
|
||||||
|
|
||||||
|
session = self.make_session_object(session_id, data=data)
|
||||||
|
if session.is_expired(time.monotonic()):
|
||||||
|
del session_storage[session_id]
|
||||||
|
return self.make_session_object()
|
||||||
|
return session
|
||||||
|
|
||||||
|
def get_cookie_attributes(self, app):
|
||||||
|
# Attributes for setting and deleting the cookie must match, because
|
||||||
|
# browsers ignore a deletion of a __Host- cookie without Secure and Path.
|
||||||
|
return dict(
|
||||||
|
domain=self.get_cookie_domain(app),
|
||||||
|
path=self.get_cookie_path(app),
|
||||||
|
secure=self.get_cookie_secure(app),
|
||||||
|
httponly=self.get_cookie_httponly(app),
|
||||||
|
samesite=self.get_cookie_samesite(app),
|
||||||
|
)
|
||||||
|
|
||||||
def save_session(self, app, session, response):
|
def save_session(self, app, session, response):
|
||||||
domain = self.get_cookie_domain(app)
|
|
||||||
path = self.get_cookie_path(app)
|
|
||||||
cookie_name = self.get_cookie_name(app)
|
cookie_name = self.get_cookie_name(app)
|
||||||
|
cookie_attributes = self.get_cookie_attributes(app)
|
||||||
if session.is_empty():
|
if session.is_empty():
|
||||||
if session.modified:
|
if session.modified:
|
||||||
del session_storage[session.id]
|
del session_storage[session.id]
|
||||||
response.delete_cookie(cookie_name, domain=domain, path=path)
|
# Only send the deletion header if the client sent a cookie
|
||||||
|
if cookie_name in request.cookies:
|
||||||
|
response.delete_cookie(cookie_name, **cookie_attributes)
|
||||||
else:
|
else:
|
||||||
httponly = self.get_cookie_httponly(app)
|
|
||||||
secure = self.get_cookie_secure(app)
|
|
||||||
expires = self.get_expiration_time(app, session)
|
expires = self.get_expiration_time(app, session)
|
||||||
session_storage[session.id] = dict(session)
|
session.last_seen = time.monotonic()
|
||||||
|
if session.created_at is None:
|
||||||
|
session.created_at = session.last_seen
|
||||||
|
try:
|
||||||
|
session_storage[session.id] = {
|
||||||
|
**session,
|
||||||
|
CREATED_AT_KEY: session.created_at,
|
||||||
|
LAST_SEEN_KEY: session.last_seen,
|
||||||
|
}
|
||||||
|
except SessionWithoutUser:
|
||||||
|
# Flask saves the session again for the error response
|
||||||
|
session.clear()
|
||||||
|
raise
|
||||||
session_id = (
|
session_id = (
|
||||||
self._get_signer(app).sign(want_bytes(session.id)).decode("utf-8")
|
self._get_signer(app).sign(want_bytes(session.id)).decode("utf-8")
|
||||||
)
|
)
|
||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
cookie_name,
|
cookie_name, session_id, expires=expires, **cookie_attributes
|
||||||
session_id,
|
|
||||||
expires=expires,
|
|
||||||
httponly=httponly,
|
|
||||||
domain=domain,
|
|
||||||
path=path,
|
|
||||||
secure=secure,
|
|
||||||
)
|
)
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
{% for category, message in get_flashed_messages(with_categories=true) %}
|
{% for category, message in get_flashed_messages(with_categories=true) %}
|
||||||
<div class="msg-{{ category }}">{{ t(message) }}</div>
|
<div class="msg-{{ category }}">{{ message }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<form id="form" class="pure-form pure-form-aligned" action="{{ url_for('alias-create', user_id=user.id) }}" method="post">
|
<form id="form" class="pure-form pure-form-aligned" action="{{ url_for('alias-create', user_id=user.id) }}" method="post">
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<label for="email">{{ t("Email") }}</label>
|
<label for="email">{{ t("Email") }}</label>
|
||||||
<input id="email" type="text" name="email" {% if email is defined %}value="{{ email }}"{% endif %} placeholder="{{ t("Email") }}">
|
<input id="email" type="text" name="email" {% if email is defined %}value="{{ email }}"{% endif %} autocomplete="off" placeholder="{{ t("Email") }}">
|
||||||
|
|
||||||
<div class="domain-selector">
|
<div class="domain-selector">
|
||||||
{% if can_use_different_alias_domain %}
|
{% if can_use_different_alias_domain %}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
{% include "alerts.html" %}
|
{% include "alerts.html" %}
|
||||||
|
|
||||||
<form action="{{ url_for('alias-delete', alias_id=alias.id, **request.args) }}" method="post">
|
<form action="{{ url_for('alias-delete', alias_id=alias.id, return=return_to) }}" method="post">
|
||||||
<button type="submit" class="pure-button button-error">{{ t("Delete") }}</button>
|
<button type="submit" class="pure-button button-error">{{ t("Delete") }}</button>
|
||||||
<a href="{{ back_url }}" class="pure-button">{{ t("Cancel") }}</a>
|
<a href="{{ back_url }}" class="pure-button">{{ t("Cancel") }}</a>
|
||||||
{% include "csrf.html" %}
|
{% include "csrf.html" %}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
{% include "alerts.html" %}
|
{% include "alerts.html" %}
|
||||||
|
|
||||||
<form id="form" class="pure-form pure-form-stacked" action="{{ url_for('alias-edit-note', alias_id=alias.id, **request.args) }}" method="post">
|
<form id="form" class="pure-form pure-form-stacked" action="{{ url_for('alias-edit-note', alias_id=alias.id, return=return_to) }}" method="post">
|
||||||
<label for="note">{{ t("Note") }}</label>
|
<label for="note">{{ t("Note") }}</label>
|
||||||
<textarea id="note" name="note" form="form">{{ alias.note }}</textarea>
|
<textarea id="note" name="note" form="form">{{ alias.note }}</textarea>
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,8 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>{{ t("Login") }}</h1>
|
<h1>{{ t("Login") }}</h1>
|
||||||
|
|
||||||
{% for category, message in get_flashed_messages(with_categories=true) %}
|
{% if message %}
|
||||||
<div class="msg-{{ category }}">{{ t(message) }}</div>
|
<div class="msg-{{ message_category }}">{{ message }}</div>
|
||||||
{% endfor %}
|
|
||||||
{% if alert %}
|
|
||||||
<div class="msg-error">{{ t("Please login to visit the requested url") }}</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form class="pure-form" action="/login" method="post">
|
<form class="pure-form" action="/login" method="post">
|
||||||
|
|||||||
@@ -4,15 +4,22 @@
|
|||||||
|
|
||||||
{% include "alerts.html" %}
|
{% include "alerts.html" %}
|
||||||
|
|
||||||
<form class="pure-form pure-form-aligned" action="{{ url_for('user-change-password', user_id=user.id, **request.args) }}" method="post">
|
<form class="pure-form pure-form-aligned" action="{{ url_for('user-change-password', user_id=user.id, return=return_to) }}" method="post">
|
||||||
|
{% if is_own_password %}
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<label for="current_password">{{ t("Current password") }}</label>
|
||||||
|
<input id="current_password" type="password" name="current_password" autocomplete="current-password" placeholder="{{ t("Current password") }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<label for="password1">{{ t("Password") }}</label>
|
<label for="password1">{{ t("Password") }}</label>
|
||||||
<input id="password1" type="password" name="password1" placeholder="{{ t("New password") }}">
|
<input id="password1" type="password" name="password1" autocomplete="new-password" placeholder="{{ t("New password") }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<label for="password2">{{ t("Reenter Password") }}</label>
|
<label for="password2">{{ t("Reenter Password") }}</label>
|
||||||
<input id="password2" type="password" name="password2" placeholder="{{ t("New password") }}">
|
<input id="password2" type="password" name="password2" autocomplete="new-password" placeholder="{{ t("New password") }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pure-controls">
|
<div class="pure-controls">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<form id="form" class="pure-form pure-form-aligned" action="{{ url_for('user-create') }}" method="post">
|
<form id="form" class="pure-form pure-form-aligned" action="{{ url_for('user-create') }}" method="post">
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<label for="email">{{ t("Email") }}</label>
|
<label for="email">{{ t("Email") }}</label>
|
||||||
<input id="email" type="text" name="email" {% if email is defined %}value="{{ email }}"{% endif %} placeholder="{{ t("Email") }}">
|
<input id="email" type="text" name="email" {% if email is defined %}value="{{ email }}"{% endif %} autocomplete="off" placeholder="{{ t("Email") }}">
|
||||||
|
|
||||||
<div class="domain-selector">
|
<div class="domain-selector">
|
||||||
@
|
@
|
||||||
@@ -19,12 +19,12 @@
|
|||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<label for="password1">{{ t("Password") }}</label>
|
<label for="password1">{{ t("Password") }}</label>
|
||||||
<input id="password1" type="password" name="password1" {% if password is defined %}value="{{ password }}"{% endif %} placeholder="{{ t("Password") }}">
|
<input id="password1" type="password" name="password1" autocomplete="new-password" placeholder="{{ t("Password") }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<label for="password2">{{ t("Reenter Password") }}</label>
|
<label for="password2">{{ t("Reenter Password") }}</label>
|
||||||
<input id="password2" type="password" name="password2" {% if password is defined %}value="{{ password }}"{% endif %} placeholder="{{ t("Reenter Password") }}">
|
<input id="password2" type="password" name="password2" autocomplete="new-password" placeholder="{{ t("Reenter Password") }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pure-controls checkbox">
|
<div class="pure-controls checkbox">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
{% include "alerts.html" %}
|
{% include "alerts.html" %}
|
||||||
|
|
||||||
<form action="{{ url_for('user-delete', user_id=user.id, **request.args) }}" method="post">
|
<form action="{{ url_for('user-delete', user_id=user.id, return=return_to) }}" method="post">
|
||||||
<button type="submit" class="pure-button button-error">{{ t("Delete") }}</button>
|
<button type="submit" class="pure-button button-error">{{ t("Delete") }}</button>
|
||||||
<a href="{{ back_url }}" class="pure-button">{{ t("Cancel") }}</a>
|
<a href="{{ back_url }}" class="pure-button">{{ t("Cancel") }}</a>
|
||||||
{% include "csrf.html" %}
|
{% include "csrf.html" %}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
{% include "alerts.html" %}
|
{% include "alerts.html" %}
|
||||||
|
|
||||||
<form id="form" class="pure-form pure-form-stacked" action="{{ url_for('user-edit-note', user_id=user.id, **request.args) }}" method="post">
|
<form id="form" class="pure-form pure-form-stacked" action="{{ url_for('user-edit-note', user_id=user.id, return=return_to) }}" method="post">
|
||||||
<label for="note">{{ t("Note") }}</label>
|
<label for="note">{{ t("Note") }}</label>
|
||||||
<textarea id="note" name="note" form="form">{{ user.note }}</textarea>
|
<textarea id="note" name="note" form="form">{{ user.note }}</textarea>
|
||||||
|
|
||||||
|
|||||||
+14
-6
@@ -1,6 +1,12 @@
|
|||||||
from functools import update_wrapper
|
from functools import update_wrapper
|
||||||
|
|
||||||
from flask import current_app, redirect as flask_redirect, render_template, request
|
from flask import (
|
||||||
|
current_app,
|
||||||
|
redirect as flask_redirect,
|
||||||
|
render_template,
|
||||||
|
request,
|
||||||
|
session,
|
||||||
|
)
|
||||||
from werkzeug.exceptions import MethodNotAllowed
|
from werkzeug.exceptions import MethodNotAllowed
|
||||||
|
|
||||||
from max.exceptions import ImproperlyConfigured, PermissionDenied
|
from max.exceptions import ImproperlyConfigured, PermissionDenied
|
||||||
@@ -72,11 +78,13 @@ class BaseView:
|
|||||||
|
|
||||||
def check_access(self):
|
def check_access(self):
|
||||||
if not self.can_access():
|
if not self.can_access():
|
||||||
print("Cannot access for ", self.__class__.__name__)
|
self.app.logger.debug(
|
||||||
raise PermissionDenied(
|
"Access denied for user %s to %s (%s)",
|
||||||
"Not found"
|
session.get("user_id"),
|
||||||
) # we do not want to pass PermissionDenied directly to the user.
|
self.request.path,
|
||||||
# TODO: Better message.
|
self.__class__.__name__,
|
||||||
|
)
|
||||||
|
raise PermissionDenied()
|
||||||
|
|
||||||
def can_access(self):
|
def can_access(self):
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Development tools. Generated into requirements-dev.txt by `make lock-deps`.
|
||||||
|
-c requirements.txt
|
||||||
|
black
|
||||||
|
flake8
|
||||||
|
isort
|
||||||
|
pip-audit
|
||||||
|
pip-tools
|
||||||
+622
-3
@@ -1,3 +1,622 @@
|
|||||||
black
|
#
|
||||||
isort
|
# This file is autogenerated by pip-compile with Python 3.15
|
||||||
flake8
|
# by the following command:
|
||||||
|
#
|
||||||
|
# pip-compile --allow-unsafe --generate-hashes --strip-extras requirements-dev.in
|
||||||
|
#
|
||||||
|
black==26.10.1 \
|
||||||
|
--hash=sha256:03c0ddd93bb392e71209903a691767eb366fe1a76deb9509ccbaae9e1f14bb52 \
|
||||||
|
--hash=sha256:0ce08b367307b0fd91c9dd1d4084e62b05b3055475f951f0f34a46b6e2393b64 \
|
||||||
|
--hash=sha256:182f6c32be38074b16d378498c498b32cb51928178ee611485344972c35ec9c6 \
|
||||||
|
--hash=sha256:1935b32f5326028019856e18cb42b4da63db23765dc84464cec723e0de478a9b \
|
||||||
|
--hash=sha256:19fa8f5beb5e77c54c9c7e21d00cc93ed6c8b6228ee385616906d6befe081143 \
|
||||||
|
--hash=sha256:2520037aa62f8a1454d0811b8f5c88b444445b03a4bfba480d8d220893b64c34 \
|
||||||
|
--hash=sha256:28842f9a8207cc1df6eb983a35a14c5a0dfcd603d214fe82d84bef552afd2e3a \
|
||||||
|
--hash=sha256:289282aa2e09d3162312a3be1788ff21b08e9ea9cc4a81e656024728b32428fb \
|
||||||
|
--hash=sha256:2ffbc023a12d0c729408823b8f10514490bd0baa301d0d4e21a7240249f9507f \
|
||||||
|
--hash=sha256:3414a0c52901964dceabd98c7c56beac0f964115a116ecedcce7247359b14017 \
|
||||||
|
--hash=sha256:4d9a90516db1d99c25dbb20cc0998e0e01531dd903466c7744e56d66f864220a \
|
||||||
|
--hash=sha256:51d5e417e700fe6ec0b0ecdc408c6f6cb5def80328f31f724993d82c6486b746 \
|
||||||
|
--hash=sha256:5cd88fd7b444ca51f3fc883b6f6657ea53a258b0b2eef6d9f2dfcfa17ce0e27b \
|
||||||
|
--hash=sha256:5f9f83beae62437e060dafd53d7f1fc327e3d3494f74d72ee5c2b73eb90fc4e7 \
|
||||||
|
--hash=sha256:70ccbd175b7f6be29d2b727ee7ca6b4c54053df59da653a6df80b175d20a94fa \
|
||||||
|
--hash=sha256:7bdade400bfe24d78a7762896acc2f9a8e1a17fb0fd0536bf6b7c7097cf3eec7 \
|
||||||
|
--hash=sha256:8375962579d537364cc0efa19b1474481915d3a793f9fc0774901814c5e5b5f4 \
|
||||||
|
--hash=sha256:978113a40223a6aaefc17364176a809a320e6b288683841427fff04c6d7b4130 \
|
||||||
|
--hash=sha256:9a0219b29cd70e49f920acb7081e6ce5025c719008447c521d0200dcad93206a \
|
||||||
|
--hash=sha256:b5347d760f0c02bb00dd249384cab71c3bf828b4f68d5b401eb116e0390f147d \
|
||||||
|
--hash=sha256:b6272cfd7e1e8e271f5b0e0207259fe2834687e5cb9b5f620b34a44db9754993 \
|
||||||
|
--hash=sha256:d42dd2fac7c342ae67e64ee99c9532e20b2a84e92c79ed3317fa2ef54c801d93 \
|
||||||
|
--hash=sha256:d5bd3518d8e97138fef295230b1e9804076d69fa4e3594071494a8c68abe6266 \
|
||||||
|
--hash=sha256:d8b3a9074a680b3c5749633714e9ae3992a1e5a23343a97ad61cd9b119b444d2 \
|
||||||
|
--hash=sha256:f6dba8138cdc99061ef07b958ac082d2aa057b6961d1936f9717c350f02bab5f \
|
||||||
|
--hash=sha256:fe85fc4019bee59bc495c0f2a8ee76c5cd02c7015508d94a967ba2376f39a52c \
|
||||||
|
--hash=sha256:ff57f63029aa1353fa8b1b0c8971fd88a6c92dc766608d2eee33ad2deb23270e
|
||||||
|
# via -r requirements-dev.in
|
||||||
|
boolean-py==5.0 \
|
||||||
|
--hash=sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95 \
|
||||||
|
--hash=sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9
|
||||||
|
# via license-expression
|
||||||
|
build==1.6.1 \
|
||||||
|
--hash=sha256:51cc11666391ab6f092070437ac747002ff46f3e4113a3622177ee6b488bfc53 \
|
||||||
|
--hash=sha256:ecd351a4be9d35a9eaaba244a7687143c9c7d4aea6ac964e7e7ddab20cbcf4e7
|
||||||
|
# via pip-tools
|
||||||
|
cachecontrol==0.14.4 \
|
||||||
|
--hash=sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b \
|
||||||
|
--hash=sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1
|
||||||
|
# via
|
||||||
|
# cachecontrol
|
||||||
|
# pip-audit
|
||||||
|
certifi==2026.10.19 \
|
||||||
|
--hash=sha256:035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae \
|
||||||
|
--hash=sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b
|
||||||
|
# via requests
|
||||||
|
charset-normalizer==3.5.2 \
|
||||||
|
--hash=sha256:01077390b03f7988f11d700a2194e69b119741a86b1a638b1db88891e3eced8e \
|
||||||
|
--hash=sha256:01b0c0d2262a9e28e8484a278c7e1b5d650e3ac8cf2683d2967e25899f208bdf \
|
||||||
|
--hash=sha256:04851f73ae72b8413dddadb16a49dfee95263553741fd42d546f7d66907e6be5 \
|
||||||
|
--hash=sha256:0521c5665880b33d603717defa76c094048900010897909952397feb3039da56 \
|
||||||
|
--hash=sha256:0774bf9bf620249fee3e0b8b9fd3065de213be30f3aa94ce2494b3b638949e26 \
|
||||||
|
--hash=sha256:0891b9d3903c5571c03771ca669a4b0ec5618ca722a5c957d3d29cd4e5062848 \
|
||||||
|
--hash=sha256:0c951d5e6dd9c2ff60609476752bee49da4206adde960ebc247766937f72e718 \
|
||||||
|
--hash=sha256:0fed1d06615f022ee3b13caf5e8b180cfea32bb2c5aded8a9d44277afc040f93 \
|
||||||
|
--hash=sha256:114e4d0c92d618409ed82a99e22b5c5e768fe995f2973f78265f4524f49d4640 \
|
||||||
|
--hash=sha256:11912e4bb14baae7c5d8791aa55ba0a3a03ec6729073307b0f57270abaa713d3 \
|
||||||
|
--hash=sha256:11a4d68a6ecda3292cb1e50239e111543ba5d709bb62a6b4ea1afcfa729d8875 \
|
||||||
|
--hash=sha256:124fbf1a8ff966d87ae05bb8bd45a71f966055ed8bba320d0c7cf450bc5f4d0e \
|
||||||
|
--hash=sha256:1461ac396c4fdb983a675f20aa555624f0ee18ac83d832b9244ffff3d8055275 \
|
||||||
|
--hash=sha256:1503bccbeb36d5527790c3930327704c39af22de3112f1b1666a9f3ce15ee204 \
|
||||||
|
--hash=sha256:15bb4005af6320d259dc7593ca84a38d7fe06a421dbcf7b910ae23979101e787 \
|
||||||
|
--hash=sha256:15c44f7edfd477b06f517a5cc317fc1707edb9de2c865f43d4b6513907473234 \
|
||||||
|
--hash=sha256:16fa0eccf81304b79c5cd87f9271c3b85dd9dd99245e4422ae9c0dd45e0f99d3 \
|
||||||
|
--hash=sha256:183b88127acdb4fabe59d951ab424faf1af7b63cdbb5f776186c1ea2ffcaed98 \
|
||||||
|
--hash=sha256:195c26fb65950f8fce54e26349852b7bdd7c5f120aeefbcc440b8a20faaed4a3 \
|
||||||
|
--hash=sha256:1afb975bd5d68d5ce9f6b6d44fdf2f7e34b895a35e95708a7a91b20a3b51d187 \
|
||||||
|
--hash=sha256:1b4cbc7c3491ccb4aa17fcd8165649d01cf39f76de1696da8631b5f71b85401d \
|
||||||
|
--hash=sha256:1bc0baf5ef96b6ede57d47f4b8fe4d9d84019c3bfcbeb20a41edc6a6ee341f1f \
|
||||||
|
--hash=sha256:1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7 \
|
||||||
|
--hash=sha256:1db38f4c5496827c1a501846d64d14c3b80c7e6714e406cd7dc36a9899fa1011 \
|
||||||
|
--hash=sha256:211d5a3eb6af8f513b8d4ca19a8c1b7accab1b5f0d3175f9826b03c1a920dc1f \
|
||||||
|
--hash=sha256:23851fb4e1b85ed3f6c2a27b777cdfe2e19fb5b38429a8faf38c7542b7665869 \
|
||||||
|
--hash=sha256:254eb48b9fa5ee9898a3c445825a1f340fe53712a098904b39b0bddba8ea3cb1 \
|
||||||
|
--hash=sha256:2625388c6c754520c37abaf3b41eb34d1cc4a373f457898f08606c8e362b891d \
|
||||||
|
--hash=sha256:281cb91036248400f4cc957495cccd44c275c2e0c5854f7e45ac5cf7dc193847 \
|
||||||
|
--hash=sha256:28a15fdad492a99b6eccfaaed66ef3f74050680545ea61ec8b2f4c538f1f1320 \
|
||||||
|
--hash=sha256:28b4f0d66fb834ff90f28209ac7bce77868c45d8c93e26f906709d9b7c2e1af9 \
|
||||||
|
--hash=sha256:2a925889534b3748302dae5dead07cc13480de1dac3aea80a941b729b471ef93 \
|
||||||
|
--hash=sha256:2b7b3bbfb4fe8ef40600792d762fbaa9057559f9d3fad209525b7a22b99e91fd \
|
||||||
|
--hash=sha256:2c9ad19a6cfcd5ea5c0d41161d22f9df1dcc277e9bef2751391334546a314c00 \
|
||||||
|
--hash=sha256:2cc961b171b3f3440f410489ab3573e86aea8736134ebbb40ea1338b7f0831bc \
|
||||||
|
--hash=sha256:2ce45c6627b22c47e390bc91a41c3d13032192e699fa0bea96e9671b373d69b0 \
|
||||||
|
--hash=sha256:2e06a3a98f916dd41d27f3105e02e7a40181c98c94b9158733d03a6f80506c09 \
|
||||||
|
--hash=sha256:304d5463e65a35d7bb0850550e0780395395f6fcf452f04db7d5ca7cecc425ac \
|
||||||
|
--hash=sha256:304d8e4d493af723536393eee0c689eb7813f4a474c8b479dee63f1fdd98f621 \
|
||||||
|
--hash=sha256:30fcd120b732aa79317f08dee04d7de0847822e4cf7ee0e9f445bb958832252c \
|
||||||
|
--hash=sha256:31f3930700408d211f13378ccbe1c40845d8da54bd0681fac3a9b5aae81c7aa8 \
|
||||||
|
--hash=sha256:34276fd796040bf0993ab33a369aa572e6979c7aab225a88893667ad8eac8f7a \
|
||||||
|
--hash=sha256:355ad8011081dec5412240c087a9a0c9d4d5039f3ed11a3f13e18c2b29b56c51 \
|
||||||
|
--hash=sha256:38a873987f3be698494da8b2e3085e29da02da7b633dce73e79c699a113d7bf0 \
|
||||||
|
--hash=sha256:39de2a259fc954455c57274dc94c79d5842774e1247a016aff30bc0efed0f4ef \
|
||||||
|
--hash=sha256:3d14b50de6bf4d0edf857a9386836846f982b8f524e188e2e68b96d702bcf4aa \
|
||||||
|
--hash=sha256:3d21b8b13c7592db2ac5e544a6d83187b995257472b0c9e8351b6d507ae37ed6 \
|
||||||
|
--hash=sha256:3d31298449090ab8d47b7b1b2a555ff73cac7ed438a08b7ac160980c7ebed649 \
|
||||||
|
--hash=sha256:3ddacd27458c45bdacd6bd6db644bfb730efbf9e830310186e3045c9c5be8fb2 \
|
||||||
|
--hash=sha256:3df041de8887954562c9b261cba85ca0e9ded74048daf125f45edcfaa4832229 \
|
||||||
|
--hash=sha256:40ab6bffa02ae10a0581e6c198be7d2d8ca5c2a0c64e4ed3465d766df457573e \
|
||||||
|
--hash=sha256:4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd \
|
||||||
|
--hash=sha256:443eae2bf318abeaf6f15d785138f71fd6de770e99a92158b8b814265e079115 \
|
||||||
|
--hash=sha256:447441e76ec720b15e64418d32e092297340387053047c7c694f579efb0ee1d9 \
|
||||||
|
--hash=sha256:4495c5002a7b28557e7e222e77e0b661183e432b7d6d2e788101e3f240e05b8c \
|
||||||
|
--hash=sha256:44bd4fbb29dfbeba60e7d2bd000c59e4b21ddb3cc53912b14048d37092706d7c \
|
||||||
|
--hash=sha256:4685902cf26edf013ed7a3da0f426ebba7a00ebb9541386d835afbf002c11cab \
|
||||||
|
--hash=sha256:498dc3188ca05a68231ac3fdbfc7f57eb67e1343c30e0fea17f8218c1599b253 \
|
||||||
|
--hash=sha256:4c2b5031f63e331e3839b40aed2dd6f191e9c07edbde303e7876846ea1946995 \
|
||||||
|
--hash=sha256:4d48f2d08b9de5864e2c8744d4461b862fb149a18274abc8b698c45975573438 \
|
||||||
|
--hash=sha256:4f87960d57feabfb618e4e0af6e7371645fa26a277860739d6e5d6e0012c92f0 \
|
||||||
|
--hash=sha256:50e3adfb96fc189eb27b1cf62d3b598b89b4bb0420d93a3d3e42e137409011be \
|
||||||
|
--hash=sha256:51cf45226a9b588d0d2b4880c62d686934b63ab0bd79ca23ab0e9762eb27441b \
|
||||||
|
--hash=sha256:52aa6992700996af31f375de0c6bacd402b0097fe40b53c426b9f51a90ebabc7 \
|
||||||
|
--hash=sha256:55ea99acb17b9325618de155a0cd6a2e8f5d10be008113e1d433bbb58db543b2 \
|
||||||
|
--hash=sha256:56bc200a365efb37383b7852e4cc5898d3b2da5987289b543956cf8cad71018a \
|
||||||
|
--hash=sha256:588461c2e8384d309bd63e5826019b6977bc66d629b99ac8737bb795d7b2cb5a \
|
||||||
|
--hash=sha256:58ca3755ee7ff7f59b57789ec9833c9de9ea275405cdd240eda1f193112e398a \
|
||||||
|
--hash=sha256:58f361dcbab699cf8f42db3f47c8e7fd1036f138c23a5d08de9fde5f425a730c \
|
||||||
|
--hash=sha256:598a11a2c7ebaa5334bf698bf29568c9c390abac6a154d8170fedecd1cea38c5 \
|
||||||
|
--hash=sha256:59f63901b0031c3136cf64704dcb21de0bbae62ce2c9529bc39d27665463de37 \
|
||||||
|
--hash=sha256:5cde776b7cc66e4f6c99612cea4aa7269aa65863f7a15841b2c264f103822f4e \
|
||||||
|
--hash=sha256:5e2b6b57e9733d39f0c9fd3185efa6b8e29652c4cd8fe94180272cf6ed9a78c4 \
|
||||||
|
--hash=sha256:5fb29fb8cd1a46c27a1bf9613ad5ec2599310d46b4025d9556404a6b6a292800 \
|
||||||
|
--hash=sha256:6045373d5a89a5ec71afde535db987ca28e76dfa276c2d4c818265b375d4b055 \
|
||||||
|
--hash=sha256:619799369eeef6366ed3e8755a5670f4f2f0fb6b30a0fd7264dc0fdc2357058e \
|
||||||
|
--hash=sha256:62588a277bfb59def052abd940703fa35107152bf479781a878617d60faf8fb5 \
|
||||||
|
--hash=sha256:62603db9a7caa0802eaa28c1c46fecd7b3a263a774069c24c3c28c302448721c \
|
||||||
|
--hash=sha256:65cd72beeeca9d3aaea1201e5923859f308f952f9c71de93f06063c79f0f7a3b \
|
||||||
|
--hash=sha256:68eb192d85ab8e5f6ec69c2bc6ac0179fbf04a5ac1569d12fbef74883fe102d0 \
|
||||||
|
--hash=sha256:6bd128f206a7752ae1f2ab6c61bf8a24ba28913a10df8b14c2637b973ff97a80 \
|
||||||
|
--hash=sha256:6be488a102b8cf28d0391d8c4ba7748938ae28b78ad901f8585520fca33ead1a \
|
||||||
|
--hash=sha256:7218e8f32b0956cfcd048fd42d9d5779809745ca1d86113ca56f66e7ae1549c4 \
|
||||||
|
--hash=sha256:7441d755b7ab94f8d4eb3e43ec05482d760842fd263d003a99102d742cd835e2 \
|
||||||
|
--hash=sha256:749e97e1b32313717a565abbe321bc2190bc8b35f1a67e4cdbc7c56c8d8ffe58 \
|
||||||
|
--hash=sha256:75a3ceed0724d625d64b86ca20aba182e4df462e04c2414fc941c0f523f06aac \
|
||||||
|
--hash=sha256:780fbe7cab297b81dad9fb8dc5eb003c0468ffb0d9e5f65068c53a34661a96bc \
|
||||||
|
--hash=sha256:78456a747de8dc58360ffa581f30a002baf5aa28cb262536545e91f113ed7639 \
|
||||||
|
--hash=sha256:7967d08cf06dee78443b874f98c98036f624f3a4e73e11f9f64f5be4d25393cf \
|
||||||
|
--hash=sha256:7a881931aa470808df94a8c380eed2bbbc76cd9dc622310f99665658c821eb6d \
|
||||||
|
--hash=sha256:7dcd882da75ef9adf94903b1e3b9419e8aa8fb4c7396822b834b9ef7fb96954f \
|
||||||
|
--hash=sha256:7e841fb9010836c992c9f12fcbd43a831de93a5f726fc1ccd8ca1d0268c5014c \
|
||||||
|
--hash=sha256:7fdde2c9fd9e3eca40631e024664cf2584272cc8f96308cbe5fdfc930f51d8bc \
|
||||||
|
--hash=sha256:8024d00c3faf3fc0c16e07a69f4405e8eac7cc0ab15f65fe6cf43827c4cf72b4 \
|
||||||
|
--hash=sha256:80d02b6f04e92601a081dd97b23d3128033098bff5d35d392ddcc0476ea11253 \
|
||||||
|
--hash=sha256:838dcc90063569a0448120554591a1d6c4a4ffe11babf048908793154ab86ade \
|
||||||
|
--hash=sha256:849df64e889b2e17230d58410a03dba311a65b163508fd33679b2b737d4b7858 \
|
||||||
|
--hash=sha256:87475fabc8d9996fd9c27debb395e642e8c838d78a00b6e932227a0e06b81e26 \
|
||||||
|
--hash=sha256:87e50a3e7cb90af586b6c5faf23e302a970415ac73bd7bd90a515a04b427ef96 \
|
||||||
|
--hash=sha256:89b53f3cda69831909888e0494f4fa0bcd3537e3e138dabeb620bd6ad946bae8 \
|
||||||
|
--hash=sha256:8a893cc101149f80a653f82062ebc95b34525a2614382e1da5458fe7c6997249 \
|
||||||
|
--hash=sha256:8b2bfab86aa71ae13aa41a6a26aab338e0db2b8bc75434b05aea89e011ff35a4 \
|
||||||
|
--hash=sha256:8d86d6fc60743dc916eb79e2eb1ec4818e21e427731543af40a3021851174a13 \
|
||||||
|
--hash=sha256:915563965d418f986e7e145accc592eae9e1a1be3566ff98a05d7a9ec42a76e1 \
|
||||||
|
--hash=sha256:92888bb3187c5ba50500b00b3b310c9f2c651709d28036077680cb5255450a03 \
|
||||||
|
--hash=sha256:93223adc95033dd47133a46ccfc316a0139176fd79085762e27202ec56018f03 \
|
||||||
|
--hash=sha256:9373ad13ef0d2c0fb761e04e55bfdee5a08b52cef2c882c8fbe9935b1517152e \
|
||||||
|
--hash=sha256:9409a8bf35cf78353942504b24a57de3d75b708997a1e4bd8db71ac8633ce364 \
|
||||||
|
--hash=sha256:9b7f416ff0978e2f2249330527f0ad6fa02f4932e6199692d3b52da2048c19e4 \
|
||||||
|
--hash=sha256:9bde855991b7e362c146535e3136a50bfaffc0487d38b33ca7e5edefc6e23849 \
|
||||||
|
--hash=sha256:9cae88599c7219005d879f98e5ed53341e9a122af585e1091200358a3003d2a0 \
|
||||||
|
--hash=sha256:9cf9b1a857e25c4baceeb3624e92a56df3668f398c4acba74e174d81fb4d1d3a \
|
||||||
|
--hash=sha256:9f56f72050826f63dcee7a7f55b0a77168cb3bfc553fd405e7f8f9ece75a4036 \
|
||||||
|
--hash=sha256:a090bb2c68df85450502e3e20d665e3a5af9c65a84d6508ed477badd49166fd3 \
|
||||||
|
--hash=sha256:a192e2c40070d92c3ccf777e3a5c4ff515573cd2bb7ed0c537fdadbbec5bbf21 \
|
||||||
|
--hash=sha256:a19a731138fc27d5682277d3b9df22855cea1239bce7fcec5f78f42ef2d1f3c3 \
|
||||||
|
--hash=sha256:a66c3bc5ab1f0ff2164fc9965ddd611ff0802173f4b9d24554c563f6ab7e1d6e \
|
||||||
|
--hash=sha256:a815775b6c38d4e0ff7bcffbeba67feded90202bb6a226b8dd35f1c855217413 \
|
||||||
|
--hash=sha256:a89012d6d5476ee112d20d998570ed58df2260a852afb1758809cd6900411d21 \
|
||||||
|
--hash=sha256:ae4f5fea5b8b8ccff88238cc8569303e5ee95efae67fa62922a311397a71f346 \
|
||||||
|
--hash=sha256:b6856554c4f44d79fc2307d5768854310a8f0096e501c75637542c82292b0429 \
|
||||||
|
--hash=sha256:b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685 \
|
||||||
|
--hash=sha256:b736353c0a625bbd5fcec108576e2385db3496f4f771f785ff32e108d3c3bc45 \
|
||||||
|
--hash=sha256:b7fd005a73d9e657273b7a10dc71a9e03c8fb9ee6999798d6918ce095b81ac7f \
|
||||||
|
--hash=sha256:b91363207bd9dc966a691e959bb47f64b30f7ac4b072be9968b366982f7db77c \
|
||||||
|
--hash=sha256:ba0b1d2620edf869789c3879223f52bf2afc5d31b3cb47cc57b3a12c05e2aa9d \
|
||||||
|
--hash=sha256:bbbfc8e28816f19d7c0f1816664980c0a9875d01b27cdf8eedddb639d9e108ad \
|
||||||
|
--hash=sha256:bd16aabe4a02a297c23417aa17ac6299dbd8c49f673bcd645b4929b11f5a4400 \
|
||||||
|
--hash=sha256:c0afc6800ba57ccc350374c5bd6150419915d95ce93cdbab2d783d75eaf30ecb \
|
||||||
|
--hash=sha256:c6708715abcf3c73b99508253e961a9967f02fe536532834149574eda6de0d1c \
|
||||||
|
--hash=sha256:c7c9ab723cde841fefb34efbad91e87f00a674b1fe1cd0784fde742bf2c154dc \
|
||||||
|
--hash=sha256:c8f3d67aeaf55f017982b73683f0e7342ba2f6635a78f69ce89ebb26aa411e5c \
|
||||||
|
--hash=sha256:c9790464842f85f437dbbb54417eda1e0e6bfc52dd8d22d6fd1c994b73b2dc74 \
|
||||||
|
--hash=sha256:ca403d7e4798f525fdfc78e258820419cbbd0f0ecbab9de7840e3c017cf6b8cf \
|
||||||
|
--hash=sha256:d008d90a7f2471519aef0c90dfbe73b3e6e4d5e66ac48e19154c17e89e98b604 \
|
||||||
|
--hash=sha256:d19fbd981a488e22cd04883659ca6b08f50b5974f9fd7c95655ef6a043e5893f \
|
||||||
|
--hash=sha256:d1befeed746d247c81127bb14de9dc3d30edb6e5976d34f83f86ed262b1d9105 \
|
||||||
|
--hash=sha256:d2374b62878abb00cd8309b32af6c0b715cd02dec0ca74ef12e5069bdc64144a \
|
||||||
|
--hash=sha256:d376bbd28b3a8999db1a103b3b388aee6f1ddeb3e51bc2172993efdcd86e064d \
|
||||||
|
--hash=sha256:d4a7319f304a774bed22115bc891618e45f85065ab44ea6acd07d274e750519a \
|
||||||
|
--hash=sha256:d6734d2ef8a50fbf8445c139477da401f50d62a0606bf00e20ec6d87773fefb1 \
|
||||||
|
--hash=sha256:d760fe2a4d7c3b226cb9026d6a842868d52a7901bd98420e1baf14e80da85cf5 \
|
||||||
|
--hash=sha256:d913de495d90407cd859d263bee2e5d1a4ed3eb6573c04e70d9ec619a7cbed7f \
|
||||||
|
--hash=sha256:db19d07e2e0129e974a0e65d0064fc222a446cd5122c2fd4184d2af9fc734a9e \
|
||||||
|
--hash=sha256:dca9ab98072a5a54ebacebdc45f53e645336b320c667410b061be1ca588ae709 \
|
||||||
|
--hash=sha256:ddc7dacc8ece3a182e7f15cb862d1fd616b46d076cb1ae9dd232b2c38b655874 \
|
||||||
|
--hash=sha256:ddf19c062bea7a0cc80f519243d2c01dd091be0cf952a0750d4ad576709559f5 \
|
||||||
|
--hash=sha256:def79fa35ef0cef8d2accec024f4fdc7ead3012ff02f5215c783f39f03ef8cfc \
|
||||||
|
--hash=sha256:df29a0a7107f7011e77f4eebdddec4c7331e24d787a0b21a46d63bdf7445da95 \
|
||||||
|
--hash=sha256:e09a3942ecbdee5cce73ea9d42da82b81b72ac1bf031ce069b93b5adf4eac8cd \
|
||||||
|
--hash=sha256:e242bb1c5e76e97dfa9e7f209a71e93a01d7f19ffdd5cfbb2e2d55b4f08f8ab0 \
|
||||||
|
--hash=sha256:e243bd13217235fc7290c621941c3f5cc8b66e4872495be821d7436ba2fb838d \
|
||||||
|
--hash=sha256:e2af3aad578aa6bd1384bcf4750fc285e5a9de53f40b7d41e5a0bf748edeb2b3 \
|
||||||
|
--hash=sha256:e4e81e09c1578b8df602e3db08b0b3ea0a6947ad612f52bf8dc5ea8d47691f0c \
|
||||||
|
--hash=sha256:e54da4baf05720032d527874d40b65fa4d7e5c6c6a43d0c3adbeffcaf275a2b3 \
|
||||||
|
--hash=sha256:e80e6c2f55656b4824d72065abb4ddd6a525c74bd78a0aab5d9fc2cf4fb5af50 \
|
||||||
|
--hash=sha256:ed2a239c0ea213acc1908150a3037257083c7c083128f1a4cec2ec4b97dca491 \
|
||||||
|
--hash=sha256:ed905975ab14056a2e5eb1c376cb2e1ebc5396baf84163939c518556fccde9f5 \
|
||||||
|
--hash=sha256:ee21e28f0430bd6dc9086c6e525d5e818a44a5ad19720c8a0ef766792f3eb5e5 \
|
||||||
|
--hash=sha256:ee43c17b173d46a3212baa6ead3ae258eeabdae48c263a01ccf0218c366dd655 \
|
||||||
|
--hash=sha256:ef4fcbf3327382cd4c9f540babd61248208af7b93eec4de397b4d5f58a09e288 \
|
||||||
|
--hash=sha256:eff0ac9dbe711a4aee69bf04a83896aa9b85f19641264053a9f6d48573abb7dd \
|
||||||
|
--hash=sha256:f0aa869112ef88429ae17820d99c3dd9504c9e9c671d3c246f3d7442cb051084 \
|
||||||
|
--hash=sha256:f3c96f633825733f735c5a9cf21d21a257d8e1edf0b1cee0a064b9c424ca0f7d \
|
||||||
|
--hash=sha256:f5833ad231be5eb6553de524a70f48d71b2c8563101750531e0b80184e175cd4 \
|
||||||
|
--hash=sha256:f5ec61164adcec446f8969a3358ec3f9b26bbda3b9213e5586d219afa8df2915 \
|
||||||
|
--hash=sha256:f7d486c83842422badd511868fd8a9a20e9407ace71564b6af47ce7e60a336c1 \
|
||||||
|
--hash=sha256:fb9e68df06293761f9fe66ade60a9bc6d0f5e42b8acf2939a9158af86ab0e5bd \
|
||||||
|
--hash=sha256:fc14a032f813bf5fe624d991960ea83e9715adc27e4c1830a2361eb1d02ac341 \
|
||||||
|
--hash=sha256:fcff63213e8e6e47770541a4607175404f47cbb3ebea7b6058cc82d524a0e424 \
|
||||||
|
--hash=sha256:fd1fbe0f116b6e55da77aca2c6ddcddcfac2186cbf78bdebf40fc156efca389d \
|
||||||
|
--hash=sha256:fe9753dfee015c570d73df76f899f18444d41388bffcde097deba51c4fadbb9f
|
||||||
|
# via requests
|
||||||
|
click==8.5.0 \
|
||||||
|
--hash=sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360 \
|
||||||
|
--hash=sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34
|
||||||
|
# via
|
||||||
|
# -c requirements.txt
|
||||||
|
# black
|
||||||
|
# pip-tools
|
||||||
|
cyclonedx-python-lib==11.12.0 \
|
||||||
|
--hash=sha256:0e807521a921a5c3cb8ce1153f8a61d29eedfe76a46aac2796b7c6b573391a54 \
|
||||||
|
--hash=sha256:16767c4039de90c04e9f03348f8f0ed4b8ff842eaa7eefcad3a95685f970dacf
|
||||||
|
# via pip-audit
|
||||||
|
defusedxml==0.7.1 \
|
||||||
|
--hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \
|
||||||
|
--hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61
|
||||||
|
# via py-serializable
|
||||||
|
filelock==4.2.4 \
|
||||||
|
--hash=sha256:301241438c57936ef93f4243a94f2559022268bfcf9b375a9a72eb0f8db8358e \
|
||||||
|
--hash=sha256:c6d8663535e1650ee430507b4ffbac7b464dd64b7647ea0112a9f406499d163d
|
||||||
|
# via cachecontrol
|
||||||
|
flake8==7.4.1 \
|
||||||
|
--hash=sha256:78480274a6d7289d9cb8eafeda241fac57d4ea687d26e32dfdca37b72cdeddad \
|
||||||
|
--hash=sha256:84ea5afcaf344487b0ea5baaebb8100f4cfaebc01f755998f75876664029f587
|
||||||
|
# via -r requirements-dev.in
|
||||||
|
idna==3.20 \
|
||||||
|
--hash=sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44 \
|
||||||
|
--hash=sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c
|
||||||
|
# via requests
|
||||||
|
isort==9.0.2 \
|
||||||
|
--hash=sha256:11da67a30f5a88383c71db075488ca3d081f427f53368f90bb1d74e958a9b040 \
|
||||||
|
--hash=sha256:16436aefeebe3aa2d5d7ae1ca895b2278f770fc4a41d95c22569a30f7413ec45 \
|
||||||
|
--hash=sha256:1c134ef9d94943eae14bf31c634db1904dd875e6e7280a60baee10ca06132db6 \
|
||||||
|
--hash=sha256:288a320e6d52ba2d3447345390c8a8400591e4033ffbe4ce6bc3e50e5b4818e1 \
|
||||||
|
--hash=sha256:29669ea6c410528ffe3b632a41835757f08282257e4ddac892a5e6d01bd35201 \
|
||||||
|
--hash=sha256:2a960e4252ac5b00f78adc0f731529e122657ee642e650896b36e1ff83028023 \
|
||||||
|
--hash=sha256:3cd67d39c3501d7227e8b229476da1d8679c03e0af97bd295876cf7070e5b709 \
|
||||||
|
--hash=sha256:3fe693c1e56781de387a6c206306e9e5e560cfeb4acdfd85f0c46122afd48792 \
|
||||||
|
--hash=sha256:4315e23e701bb1fcdfd364da59da61d78c3332c554318b7eb635ea3924d24c5e \
|
||||||
|
--hash=sha256:5c929e8ec9d9fb83f034d5f50895503f40c624605f552b97ad090a37e62407ca \
|
||||||
|
--hash=sha256:5f448510ef0a92fa626a975759d76bdbe3b721c3d615da6d1010cc451de5610d \
|
||||||
|
--hash=sha256:67b12d9504e5bc6359bb3bb4493f36cf1093d15477c61c349f52f7d04209fb5d \
|
||||||
|
--hash=sha256:6c29deeb39698a8717823b7f75b2ac58c5e8ab8dcf6cf31205a72a6617fb454e \
|
||||||
|
--hash=sha256:6eb3e714d64de6eba78ee29051f7fc80613c74e90c6f54f84082f59c429c0a0b \
|
||||||
|
--hash=sha256:71870ac3b1afdf3c259b8404c05076d3ab874122fec6f78339f1c92d2c29b012 \
|
||||||
|
--hash=sha256:810561edf6f1f5f3600f02aa709603a4360d5290c5fff2ae4b370090dd1a5445 \
|
||||||
|
--hash=sha256:85e859fd72e50c27306d05185f9472ed97fae9e1cce91c0e891260d16f2ecece \
|
||||||
|
--hash=sha256:8dde4e2d9cfb35390437353f0861ec41378f91ff958d8cd3051fb95cae59315a \
|
||||||
|
--hash=sha256:91b60ce3d96fcb0730d61fc5ab84ee5b56d676fbb92550f7ea333f58778f2f20 \
|
||||||
|
--hash=sha256:a05dc63cb6ae2a8e62ec4184153f424b1650593e00a24e6138184c46193891e9 \
|
||||||
|
--hash=sha256:a36f30b6b85d9726f79c7623d35f3e966d5d7d9d0a005af91ba19988fccd038b \
|
||||||
|
--hash=sha256:aa810daf72ff5d8ade462b2190dad9c0e16d6d428a3f9aea210f14cca2487d58 \
|
||||||
|
--hash=sha256:af8be0b5cac101202c8255360e5de832ebbb84b2e863dc0f65dbb1a3d63dd40a \
|
||||||
|
--hash=sha256:b34a165cd4e25726930ed2eed8cf2fe46fb1a5ebacd9b28eaf566b343a6457ca \
|
||||||
|
--hash=sha256:b3e81cae981a52f94d5b31a474e1cbb033ea9cc850bc4c922117c0534a1864dd \
|
||||||
|
--hash=sha256:bd8c4fb9829a5e7117d9f71f540ff1e8caafb471e574012057ce6dc35fda2d7b \
|
||||||
|
--hash=sha256:bf3ef0a91974f29f406e25eef0e04781fd5c2254b8ab55e7655b20d8cd7c5514 \
|
||||||
|
--hash=sha256:cd1e0e5e61497e95a4e5be269088e6a1013f530aeccf6ebd6134f403285ecd63 \
|
||||||
|
--hash=sha256:d03c68e9d0a83b51ed381d04b0919f2d918fb66c1ca1766761157ff44149366f \
|
||||||
|
--hash=sha256:d2298980ce44350f11d9d24c8150eaef1883431ec203dddbb4e9b5c3ceb54c70 \
|
||||||
|
--hash=sha256:d4da51a99dfd00e5c51e507ed91ebad6aafd44dc65135c17e2ef37355cd9fa98 \
|
||||||
|
--hash=sha256:e2636222848a48cadbd712280058b5da19fa147c501132e04a486a5bddcc9e28 \
|
||||||
|
--hash=sha256:e4a54aed1bb731d7cf80ef5dfbae5b960f777cea70523b751ee6049bcb604371 \
|
||||||
|
--hash=sha256:e5f11c7ccd5f079ac0431fe52c7b38ea5d9f4e31a1889746de81dac0e7b0a766 \
|
||||||
|
--hash=sha256:f65ff614632ddc3306c40f619717b3b3ca69938ffee21d97110056d52472c79a \
|
||||||
|
--hash=sha256:f7a9efeb3689c7327a0d637eb4e12691e8d5ab1297caee997b144dc595ccb93f \
|
||||||
|
--hash=sha256:f7c2fa33e1c9fbcf9fd639997e4550515c0b712b52ed70a059124a5247825480
|
||||||
|
# via -r requirements-dev.in
|
||||||
|
license-expression==30.4.4 \
|
||||||
|
--hash=sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4 \
|
||||||
|
--hash=sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd
|
||||||
|
# via cyclonedx-python-lib
|
||||||
|
markdown-it-py==4.2.0 \
|
||||||
|
--hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \
|
||||||
|
--hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a
|
||||||
|
# via rich
|
||||||
|
mccabe==0.7.0 \
|
||||||
|
--hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \
|
||||||
|
--hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e
|
||||||
|
# via flake8
|
||||||
|
mdurl==0.1.2 \
|
||||||
|
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
|
||||||
|
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
|
||||||
|
# via markdown-it-py
|
||||||
|
msgpack==1.2.3 \
|
||||||
|
--hash=sha256:07c9733089d1b176c3dd2f7fa268452f9d5d784d076473499d754a58e8d1fbbb \
|
||||||
|
--hash=sha256:0955b9000725573d1457c1676944b370dd9643c8d18f25bda5ac72913f850949 \
|
||||||
|
--hash=sha256:0c91762c48cd686dc9cf2b142c0bc544083952de32f5853d6624c956e54b85e5 \
|
||||||
|
--hash=sha256:0ed5823c4efc20fe87d3530665f40ec18a002be003114814c21235cc8d256207 \
|
||||||
|
--hash=sha256:13221a6c81ebb8e43ea63a7251c35d54e4175cea37ebf3a62e911bdf42562a3c \
|
||||||
|
--hash=sha256:186e6c602b8a9968b8e864c67d622a69279f7d1e55ae25f40e3bff7e815b2b62 \
|
||||||
|
--hash=sha256:18a6ed513023001b28dcd3ba54966f6bb90a38274ba8d2640464bcab3a1b81d4 \
|
||||||
|
--hash=sha256:1d6bcec3dbbdb89ca385d3a73e63ceae7b841fa0d7ca7c676f1a7bfe7fb2cdb8 \
|
||||||
|
--hash=sha256:1f4ae8bd4ad9ba085fde95e95d055a896d19210238a4199a771a3cf36dceed49 \
|
||||||
|
--hash=sha256:1f585407f740a9eac04a3bb82c61d68a0ea78f90e29e670bfb086b9ce3a518dd \
|
||||||
|
--hash=sha256:21bfa4d2aa0b04c1806ef778a1199e9e53ea2441bcbf284420a32083896320b8 \
|
||||||
|
--hash=sha256:2487453ca1b6104442c6442f9a1a8fee1fe8f428a70d99d4cba799108b304150 \
|
||||||
|
--hash=sha256:2574ef81c1c8c38b10e330f3f9406fd09198a776b002030fafcf8e7647e9e06e \
|
||||||
|
--hash=sha256:30e1522e4173230dca4d9ad896f038f73c0da6c1edd42f4dbad88ac583cf5d46 \
|
||||||
|
--hash=sha256:32edb81a2b5eb7cd7c9d941b2bfbbb082fd2cd09e0e725930316af6b708db186 \
|
||||||
|
--hash=sha256:3372475211a9ce1a23acefe512cb3e121d18c95dc74ed56cb1819ef40836ebf4 \
|
||||||
|
--hash=sha256:382b219de3d436de3baba0f4b0c6d4336e8f5858d0eb047918b13b69a71c6c55 \
|
||||||
|
--hash=sha256:382bc88fe90f29f5ac8a0b65c7046ff255356f2f2f3186c30e370215736fa1dc \
|
||||||
|
--hash=sha256:39b6986c19e1f2dfa549d185dba6ccf1de2e4c0ba10d8cfc0048935b1c5f9109 \
|
||||||
|
--hash=sha256:3a31905206722103a84c1f72633fe30692cff6732c9d262e09a27dbc468797c8 \
|
||||||
|
--hash=sha256:3d4c807ed050fe3ddbea5ba7e9f63d7136871ce42861be1f50ff739f0e91047a \
|
||||||
|
--hash=sha256:3ec409b0d6aa8e9eec6eaf881b893caa215dbe68c5319ca96e8a271d81bb111d \
|
||||||
|
--hash=sha256:471e12a6a42498a31490c206e0069e343b6a7c35db540be73a879eb06f5be047 \
|
||||||
|
--hash=sha256:4c0780095871ecc49a58b2ff6b1b43b25214704da67646557ca287a3f49fb2dd \
|
||||||
|
--hash=sha256:59612b4ed48a04cf024584218e813562f3b30a3bafa5f55abe300b15da314751 \
|
||||||
|
--hash=sha256:5bd5f91ea75c45cafcc5433ba8fae59b708b736ec178d2441c40c499e9e079db \
|
||||||
|
--hash=sha256:5bf390259cb25a6a1cd197c65810999b811f64cd38683251538bcc5a1e41f7d3 \
|
||||||
|
--hash=sha256:5c1efdd9181cb1b719ee46865f368a927f1c0c65d577798340b1194545b7515a \
|
||||||
|
--hash=sha256:5e0d7950ca3c1bbae291d0552dd3bb2792fc680629c4c0d44e47e5bab969f3ca \
|
||||||
|
--hash=sha256:5f304123b90e8b2e49867981b7f6061612c39f50cca51ee88de007c084cf68d3 \
|
||||||
|
--hash=sha256:62cc1a4ef0e553bac32c8342e1f04834aca7de276b92744eb7307db77759b890 \
|
||||||
|
--hash=sha256:63bb7448a1e9111319ae2430c09a5596140c160422830d6271bc75730ff2ff9a \
|
||||||
|
--hash=sha256:6576f348ed6cc4f31db6fd915a8e94245f042f50eae08d48732425e70638ea37 \
|
||||||
|
--hash=sha256:666ef5601ab0e6e345e47febc96aa81143cc932201543480cbb9499164f05ffb \
|
||||||
|
--hash=sha256:6707d2fa2aa1bb5424ea0b05f44ffc989b15ab41a73ff5855bff4944fec7c8ac \
|
||||||
|
--hash=sha256:69ad12cedb674c73527bed869cddb42b742cac79a207a614202a4abaa24ea173 \
|
||||||
|
--hash=sha256:6a834097144aabe948b8ca9020a833e8026f7d0abbd0ec54bc7e50f45a8ce012 \
|
||||||
|
--hash=sha256:6df430419f2338cb71e4a34d6e64f83c88ccd321f91f40ba4513400b36d864ec \
|
||||||
|
--hash=sha256:700bc0fc9e968a292b9137ee70e7a012f7e115bf0107ce45e3a88202788dfc1e \
|
||||||
|
--hash=sha256:7013534a7163aa4f213c4d9864f1a8a7555daac6fcd48f699a198e29b436bfab \
|
||||||
|
--hash=sha256:7995a7c6a62a1d6e7df211b4a16de513bd99fd053525050a319f80f44fb8015e \
|
||||||
|
--hash=sha256:79dfa38faf92f804aa61beec140d70b18418e1dde1778dbb77a87a4cce85aa8a \
|
||||||
|
--hash=sha256:7a003b02c6ee2eea6dfe0bb08818631e3597e69f0131f2a8250488a1cc553290 \
|
||||||
|
--hash=sha256:7c047250096f9fc19dba26e3d1639b5e7a84114003605c94def667149a70ced1 \
|
||||||
|
--hash=sha256:84a6616d396ec1bc18a1e83e67c96a393ec35dfe5e17434a5be7b9aa0fe988ab \
|
||||||
|
--hash=sha256:87cf2ef05ff2f2493ba29fcdaef27e960ca64dacfd13460ae29e6f92e0ed05bb \
|
||||||
|
--hash=sha256:89c930aece4e972b208ba589c8410b4167b05e411a5ea2cb25fd96f8bc47ee43 \
|
||||||
|
--hash=sha256:8ca67f77938ea6a3663aa9bd22b3e031f6da84d665be850abab910ee90728dfd \
|
||||||
|
--hash=sha256:8e51eca14fbb65c4e0a5a9657346962bd3dca78c08e04e3d4dee70ef48687d30 \
|
||||||
|
--hash=sha256:8ec7a1d49ca6c2569d722ab5ec86e90089b0713900aa31905b47b4c4d9e78ce0 \
|
||||||
|
--hash=sha256:902f3490db0e07a7d40b48536a85c9b28fbf1397e7e1658a45a55f958e303620 \
|
||||||
|
--hash=sha256:905a189853d6bdb204c7ae5f4ab77fb857448abfff574d3d93c62e2815b24b4f \
|
||||||
|
--hash=sha256:9276ba88891338f2617044429dfd080ae008c9868a25f6f1a7d004a35dc9ac0a \
|
||||||
|
--hash=sha256:9324c54995641c3d1f92a9d55093c8cde0ffa2fbc87a467a688ef60428393220 \
|
||||||
|
--hash=sha256:968583e956d0427878050b371308c5f8647088732ef3e66a117dbe1192ec91e0 \
|
||||||
|
--hash=sha256:9d7e9cbb0998bbfd363fd9a09c330520d5e9cb323c05b5a1a05865d23ccf2226 \
|
||||||
|
--hash=sha256:a393e428f6ffb0dcb73308c1fff5593041c16ff42da66e5bac8a83a6107a54b0 \
|
||||||
|
--hash=sha256:a6b63917d60d6df451f328bd6afba8565e33c4afe1f62ec4ad758b78731c827b \
|
||||||
|
--hash=sha256:b1631e12fe572e181cd77e831f69335d6cd5278eac22e3db3f33cf264ac2ac18 \
|
||||||
|
--hash=sha256:b774ff994d844e541439ac5d2d49a14def4104830c3465e9394c153f86200ffb \
|
||||||
|
--hash=sha256:b949cc25e4a09252cbcc54e66e507de914d0e94a3a7039bd54c299bf7037c098 \
|
||||||
|
--hash=sha256:bb89b5dc30469c84bbf8684826eb851d82412ca95690e111b9ac5e8fb343961a \
|
||||||
|
--hash=sha256:bfe7d5b62cbe7aa664f0b3e2c49077f10fcdd06183d3014f8271ff3c5edbfbf9 \
|
||||||
|
--hash=sha256:c309a7abae1d14ba29a8bd0ddbd704a5e469d8e9bd9c3dee0e4ff53d7ae01d56 \
|
||||||
|
--hash=sha256:c77e27790ad72989db783d5303825fba0b71550f00a490efba35cde7dc4b719f \
|
||||||
|
--hash=sha256:c942c21a93f36b3a69e828c8945bb72c94dc2ffe488a2086950c812f3edf046c \
|
||||||
|
--hash=sha256:ccea05b5542f6d283fef3f0a8e93a7f0be90af0ddeeef84c25c0216ba76dcae1 \
|
||||||
|
--hash=sha256:cd5a9f9f86a52c24713679aa2631956835f3842512964ff93f736ff76f1f530d \
|
||||||
|
--hash=sha256:d0238cd05dec9ffbe0de1071df685ba63e30a36ac155285b1a094e727c38cbe9 \
|
||||||
|
--hash=sha256:d1c1e8989a855b7f1f2a64ec4a80b23a631822903952770813857b2e4f460471 \
|
||||||
|
--hash=sha256:d2f9c4f85e47a44d26d5baf3b041eef23436e224d44eed273f01bd8a12048d9f \
|
||||||
|
--hash=sha256:d31864ba3933a589b6a00249f89c0eb422197f49128fc10da550e57e9cb0f377 \
|
||||||
|
--hash=sha256:d8ef3a66e4b52d2d7fdd90df2984670124b2ff7546d76bb25dcf68ef47f7df58 \
|
||||||
|
--hash=sha256:db84203b13aecc222f465061397fdd5b53b7ae73d2c95ffc1c8dc5be0153a709 \
|
||||||
|
--hash=sha256:db9fb67a3a2e75247bae569d34ebb5ff61c0448a4f0d6dbf991dae68af39b007 \
|
||||||
|
--hash=sha256:e0bd394e999949c814f7912284243298de1b5a17b6a3dcb6cc8a79b156ffc4fa \
|
||||||
|
--hash=sha256:e15f70588f4db8cd10df0930145b186de70feb9db51710cd378b1399009655bd \
|
||||||
|
--hash=sha256:e54394b7dbe2e12ab032d9d21feef7bb61a90a150a2623633ba3781ba69dcb1f \
|
||||||
|
--hash=sha256:eaf7e82249837e3aa97297b34a0bb9ff562027381631e057cea6e1367f10b438 \
|
||||||
|
--hash=sha256:ec0030361cc861ac699b2ef1c695b741fa145c88f8667fa3d7e3f73deeb648a3 \
|
||||||
|
--hash=sha256:ec90a9ae3e1169fa1171147340f0e97d941aa19fcd3b34e8339a55933ed042af \
|
||||||
|
--hash=sha256:ed899d73a22f286a72bd9528d63f2ab3030dbad8bf1527fc249319a50d61fb9d \
|
||||||
|
--hash=sha256:ede33b2892ceb976283e009ad12fa1834cfdf1f9c43ee9c97849fc588d00a618 \
|
||||||
|
--hash=sha256:f24a43b3560e20f825b807fe1e874bd73d53abaf8bbdcf258a6eb152cddbc1f5 \
|
||||||
|
--hash=sha256:f3d7b3d0018746b5997dd6b14a1870b07cc4c327d9101145d94a1fc264a51a06 \
|
||||||
|
--hash=sha256:f41ca154b7737b11893cdce3c78c61d703398a1cd54d4297bdad908392338a8e \
|
||||||
|
--hash=sha256:f42f146752eedb6765f07dcc04d72dab0a25779ec8d4a88c0085263ce114f22c \
|
||||||
|
--hash=sha256:f56fba61b2516be7917cb00151f0d060b5b21184e3499bb57f0f7d9259bea124 \
|
||||||
|
--hash=sha256:f9ddd28d3e9bbc602a9dced1591882c7fb9ab776eef8837da2c326fde19e2853 \
|
||||||
|
--hash=sha256:fafc3b8898b432b841d30a61082c599fa7f4d06885f9dc58ad72259e12059fa6 \
|
||||||
|
--hash=sha256:fcc6800daac4922960f6eeb7a0dda3dd4105e0bf7bce0e83ebc465a78cb7bdba
|
||||||
|
# via cachecontrol
|
||||||
|
mypy-extensions==1.1.0 \
|
||||||
|
--hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \
|
||||||
|
--hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558
|
||||||
|
# via
|
||||||
|
# black
|
||||||
|
# isort
|
||||||
|
packageurl-python==0.17.6 \
|
||||||
|
--hash=sha256:1252ce3a102372ca6f86eb968e16f9014c4ba511c5c37d95a7f023e2ca6e5c25 \
|
||||||
|
--hash=sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9
|
||||||
|
# via cyclonedx-python-lib
|
||||||
|
packaging==26.3 \
|
||||||
|
--hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \
|
||||||
|
--hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
|
||||||
|
# via
|
||||||
|
# black
|
||||||
|
# build
|
||||||
|
# pip-audit
|
||||||
|
# pip-requirements-parser
|
||||||
|
# wheel
|
||||||
|
pathspec==1.1.1 \
|
||||||
|
--hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \
|
||||||
|
--hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189
|
||||||
|
# via black
|
||||||
|
pip-api==0.0.35 \
|
||||||
|
--hash=sha256:14dea05d7b3b68f4c7582213e3ae8932bbba8e1f754bf06b95263d5fa6ab4363 \
|
||||||
|
--hash=sha256:66990d0da25378555cbb538db79384bab92d4617c6350dfd7a2126ba2a3a9894
|
||||||
|
# via pip-audit
|
||||||
|
pip-audit==2.10.1 \
|
||||||
|
--hash=sha256:1eb4565d19ebe5d48996f4b770b4d2b32887e12cb12cfa637f1a064011b55ffc \
|
||||||
|
--hash=sha256:99ef3f600a317c1945f1e89e227ef26e1c2d618429b8bd3fa6f4f7c440c4611a
|
||||||
|
# via -r requirements-dev.in
|
||||||
|
pip-requirements-parser==32.0.1 \
|
||||||
|
--hash=sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526 \
|
||||||
|
--hash=sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3
|
||||||
|
# via pip-audit
|
||||||
|
pip-tools==7.6.2 \
|
||||||
|
--hash=sha256:04dbe0e25f3b60fbdeee17d0590dac7fe20039876aa410a946fc54dc926c09ba \
|
||||||
|
--hash=sha256:f37eb4b736859c2c30230e50977312c61918b7ef931071ff7811fd067715c25f
|
||||||
|
# via -r requirements-dev.in
|
||||||
|
platformdirs==4.13.5 \
|
||||||
|
--hash=sha256:05831adf87d392760f4b5c3a0a95579ab5a10b18b9b9bd43680dcb47544788a0 \
|
||||||
|
--hash=sha256:53d3ec4280b815c1e0b02edad7f9ee8fda1ba9c0865dd9c2e6e63849ce82fc1e
|
||||||
|
# via
|
||||||
|
# black
|
||||||
|
# pip-audit
|
||||||
|
py-serializable==2.1.0 \
|
||||||
|
--hash=sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103 \
|
||||||
|
--hash=sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304
|
||||||
|
# via cyclonedx-python-lib
|
||||||
|
pycodestyle==2.15.0 \
|
||||||
|
--hash=sha256:12fd2f73c7b8ee8845a0431111df8faf4c1a07d6e64e2ee7f0c74014dab14181 \
|
||||||
|
--hash=sha256:318f5db083869b4c4dad922d0b11124fb27ab181b6730b93371da671e31bd50e
|
||||||
|
# via flake8
|
||||||
|
pyflakes==4.0.3 \
|
||||||
|
--hash=sha256:330ba92b8c1db2eb0b8f4068f6c58674e2649a99e334769aa50e3e9c5b11c23a \
|
||||||
|
--hash=sha256:94762a3a5a343a79b28754f96c554bce057a592a4896907d73f0369fe824e053
|
||||||
|
# via flake8
|
||||||
|
pygments==2.21.0 \
|
||||||
|
--hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 \
|
||||||
|
--hash=sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c
|
||||||
|
# via rich
|
||||||
|
pyparsing==3.3.3 \
|
||||||
|
--hash=sha256:928ae7e20211f3b6f3915a72f06a0cfd29ab9d24279dd6346b6b1a7146397d36 \
|
||||||
|
--hash=sha256:ece8c00a69cf01b45d0b1dedabb469c90d8caf996d4fda40f147627a122849a4
|
||||||
|
# via pip-requirements-parser
|
||||||
|
pyproject-hooks==1.3.3 \
|
||||||
|
--hash=sha256:5fc53fdac9f7bd63fbcdc868fb5f90b4784d78a53a3d3388cd738b807441a20b \
|
||||||
|
--hash=sha256:defda19b854fa0d3bd4f76ea4ddcba8abd7dcfcdd585a6690ade050744fc5f43
|
||||||
|
# via
|
||||||
|
# build
|
||||||
|
# pip-tools
|
||||||
|
pytokens==0.4.1 \
|
||||||
|
--hash=sha256:0fc71786e629cef478cbf29d7ea1923299181d0699dbe7c3c0f4a583811d9fc1 \
|
||||||
|
--hash=sha256:11edda0942da80ff58c4408407616a310adecae1ddd22eef8c692fe266fa5009 \
|
||||||
|
--hash=sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083 \
|
||||||
|
--hash=sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1 \
|
||||||
|
--hash=sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de \
|
||||||
|
--hash=sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2 \
|
||||||
|
--hash=sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a \
|
||||||
|
--hash=sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1 \
|
||||||
|
--hash=sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5 \
|
||||||
|
--hash=sha256:30f51edd9bb7f85c748979384165601d028b84f7bd13fe14d3e065304093916a \
|
||||||
|
--hash=sha256:34bcc734bd2f2d5fe3b34e7b3c0116bfb2397f2d9666139988e7a3eb5f7400e3 \
|
||||||
|
--hash=sha256:3ad72b851e781478366288743198101e5eb34a414f1d5627cdd585ca3b25f1db \
|
||||||
|
--hash=sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68 \
|
||||||
|
--hash=sha256:42f144f3aafa5d92bad964d471a581651e28b24434d184871bd02e3a0d956037 \
|
||||||
|
--hash=sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321 \
|
||||||
|
--hash=sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc \
|
||||||
|
--hash=sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7 \
|
||||||
|
--hash=sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f \
|
||||||
|
--hash=sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918 \
|
||||||
|
--hash=sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9 \
|
||||||
|
--hash=sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c \
|
||||||
|
--hash=sha256:682fa37ff4d8e95f7df6fe6fe6a431e8ed8e788023c6bcc0f0880a12eab80ad1 \
|
||||||
|
--hash=sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1 \
|
||||||
|
--hash=sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3 \
|
||||||
|
--hash=sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b \
|
||||||
|
--hash=sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb \
|
||||||
|
--hash=sha256:941d4343bf27b605e9213b26bfa1c4bf197c9c599a9627eb7305b0defcfe40c1 \
|
||||||
|
--hash=sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a \
|
||||||
|
--hash=sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4 \
|
||||||
|
--hash=sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa \
|
||||||
|
--hash=sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78 \
|
||||||
|
--hash=sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe \
|
||||||
|
--hash=sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9 \
|
||||||
|
--hash=sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d \
|
||||||
|
--hash=sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975 \
|
||||||
|
--hash=sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440 \
|
||||||
|
--hash=sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16 \
|
||||||
|
--hash=sha256:da5baeaf7116dced9c6bb76dc31ba04a2dc3695f3d9f74741d7910122b456edc \
|
||||||
|
--hash=sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d \
|
||||||
|
--hash=sha256:dcafc12c30dbaf1e2af0490978352e0c4041a7cde31f4f81435c2a5e8b9cabb6 \
|
||||||
|
--hash=sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6 \
|
||||||
|
--hash=sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324
|
||||||
|
# via black
|
||||||
|
requests==2.34.2 \
|
||||||
|
--hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \
|
||||||
|
--hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed
|
||||||
|
# via
|
||||||
|
# cachecontrol
|
||||||
|
# pip-audit
|
||||||
|
rich==15.0.0 \
|
||||||
|
--hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \
|
||||||
|
--hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36
|
||||||
|
# via pip-audit
|
||||||
|
sortedcontainers==2.4.0 \
|
||||||
|
--hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \
|
||||||
|
--hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0
|
||||||
|
# via cyclonedx-python-lib
|
||||||
|
tomli==2.5.0 \
|
||||||
|
--hash=sha256:069435bd5480429b98c5e5afb02ab21c219b6f0064680671c6dc0d46817346ea \
|
||||||
|
--hash=sha256:0dc598040da8d42cf20f0be588ed7004f46db12a0ac6c32e03a59dccedaaadcd \
|
||||||
|
--hash=sha256:1245a6638fc4bb0a60af38a7d45413db34a13842027c77597c712c998c62fdf0 \
|
||||||
|
--hash=sha256:19b0dd8749f4ea2f112c5fcfb3c5248390c899d7e2e173f1d91abee1fa0ff391 \
|
||||||
|
--hash=sha256:1f4a40d03fb9f63424f0979855bdeaf44dd7696b8d59501822c10ed30ba532df \
|
||||||
|
--hash=sha256:20aa36de8f2cf87237143bc1fa1aae8d6612c09118f4da21c6a684db5dd1f6f9 \
|
||||||
|
--hash=sha256:21e4cae4114aba25aa0d4f85cdf486d290fb35c0954d7bba536248da64d43066 \
|
||||||
|
--hash=sha256:22185fad8a1e622f064e78008018a0dd3323550dcb479cb7a1d296888d74024f \
|
||||||
|
--hash=sha256:2419c2a189551987b59d80e63ec355671283336f41c6b9b89462df679c7d0c57 \
|
||||||
|
--hash=sha256:264507556cd8b8c8e7c6ee037cdf443a463f03f4c958e57195e3d369711b8ff6 \
|
||||||
|
--hash=sha256:32a7b79ac57a2e83670ce329ccf675798bc5a2094783a63676866b70503f2e2b \
|
||||||
|
--hash=sha256:3f89d10c1ff6a38d992c27fc8a4816af71a909e08a40ec66934240b1e74347c3 \
|
||||||
|
--hash=sha256:463b16086865b97facd8d0b3fb4cb7c544e3f58d2a69dc3113d6db9653fdb043 \
|
||||||
|
--hash=sha256:49096930c8d886c9bbdab62d2d0d17ce823ddeea522309a190b36245d5b49e01 \
|
||||||
|
--hash=sha256:521345fd1f19d45b8df87657aaa38b6f2ca3800059fadf428e7ebf479a383646 \
|
||||||
|
--hash=sha256:57b1c3b01fab802e2899bc3d168dca320e14165e2fd9fd584760fb4ca5826859 \
|
||||||
|
--hash=sha256:5d8bac3d603c97e6854424e5b2b5b741bdbde387e09f162fb0446812b4a8362b \
|
||||||
|
--hash=sha256:610b27d99f28ec5f191c7064a48f3ddb179a1fe6ca73d571483ae859f57b605e \
|
||||||
|
--hash=sha256:61ea1ebe1e55a34ea8199cc8dbff398d35027b82271c8ac4802fd3a1fd5b1bcc \
|
||||||
|
--hash=sha256:62fc1bc8eb03e3a9cadfca713d65614ed8e09d974a283295ffe3a831976b4dc5 \
|
||||||
|
--hash=sha256:6664b7ae7af7294256c53960a6103077f4914cec8ff98479c352f622c6f6b2f0 \
|
||||||
|
--hash=sha256:667e521b37a6c5ccaa044202c235b530f90177ffe2cd4a64ecc213c7dd535feb \
|
||||||
|
--hash=sha256:69491c143d2fe063046e0301e62a810bed338fa4d1ce0fd870c27dc1e09b0d84 \
|
||||||
|
--hash=sha256:6cf74416bdc94ae458b14e37286c1073081850ac8459a00d0c5efef5d44294c6 \
|
||||||
|
--hash=sha256:6e95c7614e705bfe2b04b27aa124adec59752d15813df37e2156747cab3a006b \
|
||||||
|
--hash=sha256:6f041843c4d3a37245c0c056fd955b186bf8b1fb85690cbe40b81230891dc34b \
|
||||||
|
--hash=sha256:752e8b1aa6a4367ef8bf6a1a1e005540f7ed055ba36d7193796812ca5404eb52 \
|
||||||
|
--hash=sha256:75dbcde8751b0a960aa3de173aa5e894d590755c6d7758b7e774c06f1dc3cbdd \
|
||||||
|
--hash=sha256:7ac2027d37c3afbdf4bdd377f2676f6f1d2122a5be1f1137b49dced590b37e75 \
|
||||||
|
--hash=sha256:7ad1ea345759240d6463efa0ed1c704402752e49aa21476620738d74d72d8aa1 \
|
||||||
|
--hash=sha256:86665cee9c4835b7a7f1e8ec2c719b5258d4dc782887aded5a8ae7352a96843b \
|
||||||
|
--hash=sha256:8ff3a2ca028c7eee0c777f9a092038d0a594a9fa04e215f929a22c329e2cb142 \
|
||||||
|
--hash=sha256:91294a9fb94a75542f6e46e4a2ae709bd8d9b51134098cae5cf3bea5478b6d03 \
|
||||||
|
--hash=sha256:943276cf269e0071948d9ff697159c1735e623c1151d88abb09b74659ef0cbea \
|
||||||
|
--hash=sha256:96243987194634bd411066ce40c952e108f86af04db533ecd8ac3ff2a85b1885 \
|
||||||
|
--hash=sha256:984012f71908165449a951de2050d52f276bfe3aa5d5f570f63ddad814370374 \
|
||||||
|
--hash=sha256:9b03d7dc168353b4132965bde20feceabaa470e570c6f59660dfae59b1f9eeb3 \
|
||||||
|
--hash=sha256:9dbb18c1cfb2f6517942fc9314437f66aa06d94436ffb1f06102ef3572f35276 \
|
||||||
|
--hash=sha256:9ebf8d19b17bd0daeb7b7dec81a946a439b753942fd0210d6e96c532249eea6b \
|
||||||
|
--hash=sha256:a525685c2f97da40762b8695eb7aa0af4c8344ca1905c73e4e29cb04d34607dc \
|
||||||
|
--hash=sha256:abdbf6313b8d9efe157edeb7ab6eae4de064b1300ad31abf73755154b30abe68 \
|
||||||
|
--hash=sha256:b69564772b5c8f22ea5f498dff08cfa825045b4d4c4400529000bdf818aa3b2a \
|
||||||
|
--hash=sha256:b8ade5023067f99fe72b88accd30d0ea05a158e9e32a11f124e731ea9695313f \
|
||||||
|
--hash=sha256:bbaefc84548d754be821bba7c4141c4787dda182f9e77f2f87b71213529efa7b \
|
||||||
|
--hash=sha256:bd05de8c1698f8413dd7d869492693a0bf2211543b787ac78cd5e7536af1a6d7 \
|
||||||
|
--hash=sha256:bf0b5e8e0f68ebb494356e577c06c139161efd8d3b9050f93b39b7c26cc54ff0 \
|
||||||
|
--hash=sha256:c414be4ed9d3cac80c42e348fa5a956117d1a48227f48026e31f59cb4a7671eb \
|
||||||
|
--hash=sha256:c47300f9bf791808f77d82747691c4bb09cb14bdf3060cca99b42cdc4361d5a7 \
|
||||||
|
--hash=sha256:c4dc1c1781f2f716de763d1e9a7b34c6a894e167e291c7c5d16c72f7a9538545 \
|
||||||
|
--hash=sha256:c804ae44fe7b4bab5da295e4f980a1ff04670bca9d23fe0a4e887e08ebd741a8 \
|
||||||
|
--hash=sha256:cfac177ebd6236003846ea339981f71457cb6eb748f23381eb257e45092e3980 \
|
||||||
|
--hash=sha256:d2ba24db8a9376921b5e87b4762b9adb0f3f1deaea68f2b8b0bb2c11efb9c3e7 \
|
||||||
|
--hash=sha256:d3182ee2d887e507bd67319a0a61105d1dd33facc111329559a233b772c1a105 \
|
||||||
|
--hash=sha256:d747252933c8a65ef6bd8da0fbb7ce28a90eb6119d8cd00772cd528aa07b68d5 \
|
||||||
|
--hash=sha256:d7e369fd63331746182360977b1892bfc215476a30d61612d732425311639f56 \
|
||||||
|
--hash=sha256:e12bbcd32897272fb05929110362ae9ff4c1b9bb26bd9e971e71dcd3275b4c3d \
|
||||||
|
--hash=sha256:e7ad033e27a516a233bea839cdb77b80146facb3b4f40bf02cd0cac165cdd5c2 \
|
||||||
|
--hash=sha256:e9e15b4a6c7dd6b85b5fbab29488a73f1f70de516942308daa266bf0e0aeb0d4 \
|
||||||
|
--hash=sha256:ed53f7e89bb04f6d9e8e7799112360b0c4d5cbff067de0814c98c37c39b920f7 \
|
||||||
|
--hash=sha256:eff8babca5a7999bc137acbc7482a8b7e17ffca5075ab41f5d770ab408c7bfef \
|
||||||
|
--hash=sha256:f15e3e0b835a6d68b10c86bf80a3149780498d6911c93c3ffd1861d19f9200f1 \
|
||||||
|
--hash=sha256:f3fcbc57b1791fa6cbe5d8434179d51de12be1a4811469529f47f6e7487a2571 \
|
||||||
|
--hash=sha256:f4b653094e18f9031102d3a1da5c729c8f222d85225b18037dac621695e46e1a \
|
||||||
|
--hash=sha256:f79203b3965b4000e91808aaa7c040206093f2b8bf86f455982f2274c9ccf442 \
|
||||||
|
--hash=sha256:fd4dc129784e0c5335bd4e61dfcc4487499a013419e655cf2da1d091b7e0efdc
|
||||||
|
# via pip-audit
|
||||||
|
tomli-w==1.2.0 \
|
||||||
|
--hash=sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 \
|
||||||
|
--hash=sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021
|
||||||
|
# via pip-audit
|
||||||
|
urllib3==2.8.0 \
|
||||||
|
--hash=sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3 \
|
||||||
|
--hash=sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63
|
||||||
|
# via requests
|
||||||
|
wheel==0.48.0 \
|
||||||
|
--hash=sha256:3217dcc807155e45db462d7ef2431f5ddda0d7273b700d05a67b271ceb1287ab \
|
||||||
|
--hash=sha256:94800765601e9171bf5d58d066e640662842bcedcbab982b2c90787a2c987322
|
||||||
|
# via pip-tools
|
||||||
|
|
||||||
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
|
pip==26.2.2 \
|
||||||
|
--hash=sha256:3a8e5c800492787f33a4114ef54444546c340df428974ebba17e0fd47e11af2f \
|
||||||
|
--hash=sha256:fd7f4dfa8190e014013b9f08f3e973f4e4c26ffd179a204895533a7254813a70
|
||||||
|
# via
|
||||||
|
# pip-api
|
||||||
|
# pip-tools
|
||||||
|
setuptools==84.2.1 \
|
||||||
|
--hash=sha256:3ff8b3b5492a0aa66960f4419365a86b84e013070d4313e4d4a6894f5d64d6bc \
|
||||||
|
--hash=sha256:ca140b45d5325a9478f880e351484f62fef15f336462e449ca0c03c873d589c2
|
||||||
|
# via pip-tools
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Direct dependencies of MAX. Pinned versions with hashes are in requirements.txt,
|
||||||
|
# generated by `make lock-deps` (see README).
|
||||||
|
Flask>=3.1,<4
|
||||||
|
gunicorn>=26,<27
|
||||||
|
psycopg2>=2.9,<3
|
||||||
+199
-3
@@ -1,3 +1,199 @@
|
|||||||
Flask==3.1.3
|
#
|
||||||
psycopg2==2.9.13
|
# This file is autogenerated by pip-compile with Python 3.15
|
||||||
gunicorn==26.2.0
|
# by the following command:
|
||||||
|
#
|
||||||
|
# pip-compile --allow-unsafe --generate-hashes --strip-extras requirements.in
|
||||||
|
#
|
||||||
|
blinker==1.9.0 \
|
||||||
|
--hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \
|
||||||
|
--hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc
|
||||||
|
# via flask
|
||||||
|
click==8.5.0 \
|
||||||
|
--hash=sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360 \
|
||||||
|
--hash=sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34
|
||||||
|
# via flask
|
||||||
|
flask==3.1.3 \
|
||||||
|
--hash=sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb \
|
||||||
|
--hash=sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c
|
||||||
|
# via -r requirements.in
|
||||||
|
gunicorn==26.2.0 \
|
||||||
|
--hash=sha256:62b864895d9ebff0b2f9867ba04fe811c93121596540830c9c916d0769668447 \
|
||||||
|
--hash=sha256:bd249d0b3f7972f7432f0a6b6ff3b3ee2d129f70cd1ff6c09a9dd9e29a2b88e3
|
||||||
|
# via -r requirements.in
|
||||||
|
itsdangerous==2.2.0 \
|
||||||
|
--hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \
|
||||||
|
--hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173
|
||||||
|
# via flask
|
||||||
|
jinja2==3.1.6 \
|
||||||
|
--hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \
|
||||||
|
--hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67
|
||||||
|
# via flask
|
||||||
|
markupsafe==3.0.4 \
|
||||||
|
--hash=sha256:007e1ffd9bf65bb6ee96df7b258fc632a4868dd5566037986c64781f35a36e98 \
|
||||||
|
--hash=sha256:02fa4acbc6a3fc5c693c34d4dd8c1130b7fe99cc915181b0ddd6f72aeb296002 \
|
||||||
|
--hash=sha256:03470d1a8268e692ecf79ecd565593e59d44219377a7ead61f1f1b94c1f7ff6b \
|
||||||
|
--hash=sha256:04e7902ba80ee4bac1d50a549606527a1dcf0476cd81403db41099d3b60ec653 \
|
||||||
|
--hash=sha256:051417f74bcaaefa316276e0ff723f541616ca51043d070da00249d9bddd3e3c \
|
||||||
|
--hash=sha256:05295589e619b9bed252a86b532b8e27350abc372d18ba89b59375325e91ec1e \
|
||||||
|
--hash=sha256:06de8ef6331f6e822c28d577dc8bf43fe398800477c49498f38fc38b67ff33fc \
|
||||||
|
--hash=sha256:0764a13d34cae40db7bbf3a09b7e9b491bf4603e20b263a7a9d6b8e324975d0a \
|
||||||
|
--hash=sha256:077293e425f28ec737dbcad442a71752e28f8ae27cde3d68acd1fb212091cd92 \
|
||||||
|
--hash=sha256:0930db9bdc62d22944e10b066448bb65dc9abe9112880c7cab8da54db4284d5f \
|
||||||
|
--hash=sha256:0cee7cb0f9a1b6892ea482237d9403b3d1b4603aee057d0ff01f0fac2d019a97 \
|
||||||
|
--hash=sha256:0d9c47709875fdb321452056622e930c52afbc07a7d780762fbb8b4d91ce6fa4 \
|
||||||
|
--hash=sha256:11935df9bf455ed0c04eb87bcd720f02b1fe5e02128a9430f23aed6f93336fc7 \
|
||||||
|
--hash=sha256:12a606a492de952afcb43b59a14aaaaad120e708d3663dd0fdf2d738d427a691 \
|
||||||
|
--hash=sha256:14bd2d845d62ab678eaf81da89d7b621b51756c72346745c1a594c09d49207a2 \
|
||||||
|
--hash=sha256:15ba9e28640feef770374b116a6f019c21f52404aeabe516aa7f800587b98cfc \
|
||||||
|
--hash=sha256:18a801868a884f216e784d7d14db2a4077143ce7610440aee2ce8f734e7cfcde \
|
||||||
|
--hash=sha256:1c0df495a977d10460a94941799c72d5b5ab03d3858d949b55b5a66c8f371c99 \
|
||||||
|
--hash=sha256:1caa2fa5a6184fb233153b35f654e6687bd555476f6170f29d8ee9be1a8b0af9 \
|
||||||
|
--hash=sha256:1e1451fab512d1bcc3dc26988ec1edb0b82c2db909132872cd9356070a6b63df \
|
||||||
|
--hash=sha256:1f1f9477e174582b0a1b583d60b66e1f2cf5d3fe12cee985e4aedf44766600e5 \
|
||||||
|
--hash=sha256:2628d3a8cb648ecebb3c5d6b0a1052d400e4d8b7ac0fb786be8d285b50040d17 \
|
||||||
|
--hash=sha256:26e9867520db70d37f7fb421a7f0d8adb40171011fb84ce869afa1a83370dfa8 \
|
||||||
|
--hash=sha256:2a6ef68ae94aed8721934072b27a3b654ea2100b97e4ab864cf1489c90926fbc \
|
||||||
|
--hash=sha256:2b2b1e18af909b448bb3cf9e3433366f7a8726271fc214e8b10e0f62a78c724b \
|
||||||
|
--hash=sha256:2cb3dd71fc6be918ad4264346a8ed69485f9b7ed7bf35495d8e22807cd6b8bea \
|
||||||
|
--hash=sha256:2d1b7d9308288661f56672b1b157d75fc536714d3638487bbea17b6318a78248 \
|
||||||
|
--hash=sha256:2dad610540cb2e6272855c178f08ae9a1c7ac258a7fb71660553a5f104b42741 \
|
||||||
|
--hash=sha256:2e5a7cd7fdd14fcb1ae5d7d8bf23d24fbd1daefd1fbca2580132e1ea75f098b5 \
|
||||||
|
--hash=sha256:2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6 \
|
||||||
|
--hash=sha256:340cbb1957ba99929cbf19a75626d36ba1ae21d1730b287d1cf7f824a20c4fc7 \
|
||||||
|
--hash=sha256:34bdde374c5932765d7dc685c4a1d191a3207852d67e8e0a9eb6ea85156181f1 \
|
||||||
|
--hash=sha256:353bd63081912ab8cfa6a0c7d185934cdf8426f04c618bba6bc4b394f2069b67 \
|
||||||
|
--hash=sha256:387d8cd30e69b3f0a72877b9ae717033396404e19095b17fe89753a981fda44f \
|
||||||
|
--hash=sha256:3882fb412298575bae3b9c46868251f15cc69307359f87bb1b382e53d6e5a2c9 \
|
||||||
|
--hash=sha256:38fc55594dab834470b6733dead2ee9e3f657fb0608c769dcafa0ba5ab52f45c \
|
||||||
|
--hash=sha256:396ec4e65cc889f69786b3b89478b471cee5a3bcf468b9d9bb03e1a30fb291fc \
|
||||||
|
--hash=sha256:39dbacefc411633db5b4378b066a9aca70a3d7e2922c9e578d825f844026eeba \
|
||||||
|
--hash=sha256:3a93d9616ddecfb393727a0041a562cf0b15a244e20f2bd25efc7949be4c4f17 \
|
||||||
|
--hash=sha256:3d23795802fc8bd72534836d64489bbf0f67c088959091bdb22e10735a5107bf \
|
||||||
|
--hash=sha256:434139499bb20b502ed3baa1f169e618f924a97e7a777fea1a49446d80106cf6 \
|
||||||
|
--hash=sha256:436e3ffc6310d3c41878c601db29098102fe5d8a467c49da4a4125254e0980f2 \
|
||||||
|
--hash=sha256:489505b03f692c3f376394e49194fa7a7f9e8558d6e293a7056a0032b0c38163 \
|
||||||
|
--hash=sha256:4a540e2d3192792fc84eced57bef37851ccb2b41f73291bb17408eea77bcd278 \
|
||||||
|
--hash=sha256:4a7cdc2a420ca01058182da4253329764d4bfa055564d1eced90e6ba1e8b1d3d \
|
||||||
|
--hash=sha256:4bced6e2a6dba6a28f7dd3c6ce14df1b2dd495923f16ea484cad03decd463b2b \
|
||||||
|
--hash=sha256:4cf3468d5ec187ffffcaca8e61929a37448f215dafc1386a12c750a72fe53634 \
|
||||||
|
--hash=sha256:4e2c4809c14559aa7ef426f27fb35afbb38104c349a903bf8f3600456764bb38 \
|
||||||
|
--hash=sha256:4ed644d75aa94a2baf7ec3a96eaa160ea58c742eb9d27c6506053c5c40fc84ed \
|
||||||
|
--hash=sha256:4f6e0852a0283b1b1fd776eeb7b766a5f440b3e2bd31ab51af3b400585f3965c \
|
||||||
|
--hash=sha256:5066b244f576f91afc8ee3ba029a89f99d39c79b1853fe9d39bea9f0afbec148 \
|
||||||
|
--hash=sha256:5086f9975abb1ab531ee6afca1761e4b59a19b446f3f6522ed776963228cfe5a \
|
||||||
|
--hash=sha256:50b5bedc9ed8a94fc8857a42ef4f84a81ea88f8d4f05dc8705fb23ee6d8dcca7 \
|
||||||
|
--hash=sha256:52704c5d36eb6dda8866493decd61111fff86244c9b1ad225ca01b9e91e5970f \
|
||||||
|
--hash=sha256:55ffd6ce583d97dc71dc92e930324c8c0d25aea7e3ade6ae54ef77cedb096811 \
|
||||||
|
--hash=sha256:569d65055d367e3dcdf30c3f41119467b73d9ee9faf332bdf40402644f5ac08e \
|
||||||
|
--hash=sha256:57f9947a7e57a081c1e3e0a2dd0d2dcf290a4531450e6f611e30084c222a7295 \
|
||||||
|
--hash=sha256:5989cb26b2e1efc6a42216a9f6b5ee495ce5ace2e5b352a9af489976b32d1ee2 \
|
||||||
|
--hash=sha256:5c22873ad1f0532ba40fa1727f3c0fc1bbbaab6d373d4cbe3f0dc74b2e2521c7 \
|
||||||
|
--hash=sha256:5e8b3d0b18fd623afa12ecb2ce8d8becef69f9b5440c6330c7972200e0bb84b0 \
|
||||||
|
--hash=sha256:61631e08084be9e21a8967ec3139c7616ed7c5e9368e05c86d1b39562c8a57b6 \
|
||||||
|
--hash=sha256:64511c54db4e4987aef4c41923235927428729e8174c5dba488429be70a998ed \
|
||||||
|
--hash=sha256:6669c1bf34080161ce49c589cc512ef24d4c704ac9d2b2d3667f519c60418378 \
|
||||||
|
--hash=sha256:672d207103e6b16ca098611b0f9efad6bc00afd47c03d6ef62186495ca677dc0 \
|
||||||
|
--hash=sha256:6768d67d1bce64270e0fdc2e69309d68b9b18ae56ddf6c711d168e9d051c2cac \
|
||||||
|
--hash=sha256:6a45c3d514f2436064db00d7fc8778d888f0236ebfed649b53d13a59e69ad51b \
|
||||||
|
--hash=sha256:6bd9e1788e15bfcf6a9082de42e30387e7b85d211ab21e57a939bb8cfaaf8d96 \
|
||||||
|
--hash=sha256:6d2a9efe686f9de00d0d1ea32a4a5a86d558a2277501bd78d964214eab625e59 \
|
||||||
|
--hash=sha256:6da83a088f8ef93b2d483a8232a4dbf4d69d3d8496b568a03c56becac43e1808 \
|
||||||
|
--hash=sha256:7018d4af1cd272e847aa5917983ab5e83e4f6579f9dbfecd4a79c0ca80b144c2 \
|
||||||
|
--hash=sha256:71f88e749ea29f67f21f3b36433c1dc54c7729ed2a6d9e2da2e0d9e0d7b224eb \
|
||||||
|
--hash=sha256:737c9c3981998eba27f11786f84fddcbabc74068b72a4a1f454ea02094b57b65 \
|
||||||
|
--hash=sha256:73e77980c7207854f00fc4e71fb1626868d5740ab4012623d55c7a99ad122a72 \
|
||||||
|
--hash=sha256:799c39bdf5e2f1292fedd3009f7b3c9e760f10b2420cb9638d56920840ff6db8 \
|
||||||
|
--hash=sha256:7a83aa6e4805df46fed18e989d3d16f86ef60cb50bbc8d9ce3a6be89165fbf6e \
|
||||||
|
--hash=sha256:7d3391b2188d18737cb2fa147028b1096236eaa7e156446c650a489fa2cadc91 \
|
||||||
|
--hash=sha256:7e1636da3d8dfc220b6dd10264db5f2b165e4888c4518594898fbe381049af8a \
|
||||||
|
--hash=sha256:805c8b84534fa10891890f0e4be39f3a99e94615d93e8836bf9fa1fdca2feeb2 \
|
||||||
|
--hash=sha256:811d02d5122171c1941357efd8f9bf4ffe907b7f0a1a4e729a880e4be3f46e3e \
|
||||||
|
--hash=sha256:8138eb83940ec7299024d92d4dee45f601b9e6c5ffde9d25f4e35e326203c707 \
|
||||||
|
--hash=sha256:83b3944fea42a8400edf92fd1770fb8d0d4f7de651353bd2d8525a92dba69a21 \
|
||||||
|
--hash=sha256:849dd2bb0e5e4ab2b71c7191726a4a8d5aa8a610daa584728cbee0b710ddc4ef \
|
||||||
|
--hash=sha256:8698d70a8081ee8c090dbb394768b5789a1da8b131b5499f89d071dd3cfaf6be \
|
||||||
|
--hash=sha256:8781a792a070cf2bd1b86d3aa943894115faaba6e88122a7bf32d62072742453 \
|
||||||
|
--hash=sha256:88d59b473bfb03259722600839af9bbd7fa13a2eb514beefeedb95997882f69a \
|
||||||
|
--hash=sha256:8909c2f1c6dd65e054ac4b573a91c8384d1492281e55d82d159d653f7a13adf6 \
|
||||||
|
--hash=sha256:8965520ac587c94a4ac48b729be3d8b8de00af39699b17585dfb599babe77977 \
|
||||||
|
--hash=sha256:8b5d563170ff8ba3181caa967c99a3c804d1dedb702c7cb93a6a7c32247da978 \
|
||||||
|
--hash=sha256:8e124f974786f831d6043728e38296969d3579db8896fe004682f5758e613581 \
|
||||||
|
--hash=sha256:8f0fac8b13d14bb06c68195f849371924ae53dd7b1c00fed24650f704383b692 \
|
||||||
|
--hash=sha256:9240187afb63d2f9ddc3e032c670356fe941f6e20662ea168a5dc3f1f317e1b3 \
|
||||||
|
--hash=sha256:925f929d6b59a8b3f8b8c6ac363cd0af7eecc81efb3071770b3c6717c450a369 \
|
||||||
|
--hash=sha256:9348cbb300d224fe3b89793262cb093504d4ae927004468463f745188a193e4a \
|
||||||
|
--hash=sha256:9388003072b95f2f1e3fd908604194d653ba21330d811961a78b7da1a77e9e36 \
|
||||||
|
--hash=sha256:9438a2648b2195980cb2dd8e53ed7b8df91319e2d0b70ae61a9e1d1bc8d3bec9 \
|
||||||
|
--hash=sha256:94e4c421742086aeee4c32a506eec8859d7634aad943f7e6aacf70f813478768 \
|
||||||
|
--hash=sha256:94f5407f7bc64fa6463906b896f9904beeeb7dd8dc116ee8e9056c8714ff9916 \
|
||||||
|
--hash=sha256:971a3bbb75d97ae4e2e8f7d4834236f86f85f0c85e04ab2e191db1123b04f80b \
|
||||||
|
--hash=sha256:9e227f3dbe6bde7491cf0a9965d00b88c6b1a4a95d11480ddf88bb96d397c19f \
|
||||||
|
--hash=sha256:9e25feb9e330b63edb0278a0acdf85e50d0cb0fbf49c3084abbe4e24ae195346 \
|
||||||
|
--hash=sha256:9f098115c247e11d138ab83a28fa0323c77015007ea2df73ba5fd714dfefd67c \
|
||||||
|
--hash=sha256:a18f38cafc329bac5e3c2b96c765b4c96d3d103421ed22ab7988c1e3fce27464 \
|
||||||
|
--hash=sha256:a4bbd2d87dd233b9fc5812160c3d0ffbe42edc22a26ce0469f58479ede633fe9 \
|
||||||
|
--hash=sha256:a5fcffb37e602b0b3c1638a97746b9b96125caa9bcf6fa41d337a9261de231ee \
|
||||||
|
--hash=sha256:a8e9f292fcda89b324f2f5c91d13f1424a153e40fc2756f38ee23b15835ff300 \
|
||||||
|
--hash=sha256:a9f54054101545a9a9cccefddf54316aa6e4491611fcbef9e91b3b6bebec04f6 \
|
||||||
|
--hash=sha256:aa2c838cc024642cc04c6854232f32b43e5e22833dd11119c1766c7873b8370d \
|
||||||
|
--hash=sha256:ac0c7c9f1609b0c4c114feb1d7a3409564c7fb77e360bed9e97e5d25dfeaf868 \
|
||||||
|
--hash=sha256:add96447a86d205ab616665d53b2950ee81083757f56e6ea833c8b2917646b46 \
|
||||||
|
--hash=sha256:ae9dcb8fbe244cb82f8a6458b455b927a03685e383d9bacf1ea5ce180b96dc97 \
|
||||||
|
--hash=sha256:b4a635a0487774f841cb1fb62e907e7195cc95bc761e053184b8acc3ceb20733 \
|
||||||
|
--hash=sha256:b4d12837e0203bbace818ff4a7461afdcd78bcd782351cea148139180d7bcffe \
|
||||||
|
--hash=sha256:b61687d0828e72bf5cda24a2690188f37170bd31c9359ac97e4e66569f120a16 \
|
||||||
|
--hash=sha256:b807e598953730f82e4eae3bd30f6a122cf6b31c398c6b504c0e04c13c170429 \
|
||||||
|
--hash=sha256:b8cd1f918b26fd7b1832ece557cc18f2d8747309ff8b3f0ef9d4250c5ad67a39 \
|
||||||
|
--hash=sha256:b91cc9d336957239ff200f30097e6fea2dc6d6fb3c81e853eaa09eac904fd894 \
|
||||||
|
--hash=sha256:bd3ce56ae2cbae3ba82b683bc425cd7e48d2ed8b10f3e818186b6f5646d9271c \
|
||||||
|
--hash=sha256:be6cb0c799abb0e2ba3e618e6d28ddddf7e485f6c2ce938dfa237daf3905072c \
|
||||||
|
--hash=sha256:befb4158af32106b9a93db8d6d1d1cbbd418c0d5aca0cabb7b1780abf0c89169 \
|
||||||
|
--hash=sha256:bf053da3c97a4bc5ecfbb218cdd2983febd91c617be8367d139882aa11e490aa \
|
||||||
|
--hash=sha256:c02e8f18bdedba082cef725942ac823b9b60656db07f7e265cb31618dfd00d77 \
|
||||||
|
--hash=sha256:c1bc67752d5f21013cfe430df4062441714eab79f65a6a05e01505957e9c35fe \
|
||||||
|
--hash=sha256:c61750fadcd119d0825bcb7d7d675dd264dcc89cc05292aab5be68ebdbb374ad \
|
||||||
|
--hash=sha256:c90d5b3d4e944e065a301d741b3c1d784f6bd1f503aa68b4967e32b2ba313d85 \
|
||||||
|
--hash=sha256:c9a7f43c0b202b334cc9184af09bb8f21d3a209e038efaf106936fb69e6b026e \
|
||||||
|
--hash=sha256:cb96e6e088d6cf71c1ea977510948320234824cf226e32f6f6e044f7a9c82b34 \
|
||||||
|
--hash=sha256:cf63c214fe879a65e69a386f915e36104fc84254ab141240f8854602d8e0be2a \
|
||||||
|
--hash=sha256:d1aca03ede943eb80ab3d63bb082c84b7aab85ea83bd0fd0c200260945fb49d9 \
|
||||||
|
--hash=sha256:d2e56fd3b00222722abfb3f5f0759ddbae4b90811b5ad4343c64030ad1bde70c \
|
||||||
|
--hash=sha256:d5f93ebbeb8032d47e349328ec8662d973d9b05a70b3c35df1f91fe419b84749 \
|
||||||
|
--hash=sha256:d882a373d8093c2941e01291b7ced96e9cbe4781da9a7751ca7e6c70385e5214 \
|
||||||
|
--hash=sha256:d920abdfa61279ba1a2ef9484aab07bf03331f8c08a10120fa332353d06e6932 \
|
||||||
|
--hash=sha256:da2af0d7aebfc2074080d72efa6ab8317c62481ef1f896f65d9999c1c01f4494 \
|
||||||
|
--hash=sha256:dd8ea6ebee7aedbf7c749fa80521d9ccf1ba473e0d1e14805caafbaad281c889 \
|
||||||
|
--hash=sha256:de8b364c423ef0a4bad9069657d617f9a5d2b2062457a89b1fa16ee199c399c1 \
|
||||||
|
--hash=sha256:df1ae86ff54725a01fa1a0510b914ca53a161b7050be74f6204e24aded5971d0 \
|
||||||
|
--hash=sha256:dff05cb7016dff1e9fd68f4122c127b65dfc59de5306cfb7ad92f956f230bee2 \
|
||||||
|
--hash=sha256:e1a622f13970d81f95d0c72f9dc090dce9085fccfa4c9f2174377ee32bd15786 \
|
||||||
|
--hash=sha256:e49fb0d1ce92cfa0cb198cc5b1b11cdf9d0638658e2a2db2687e39db7c87fc78 \
|
||||||
|
--hash=sha256:e5c802729725bd07e2bc3ab7b76dc7e0bbfc53129d8f1eb1c002c24cf774717e \
|
||||||
|
--hash=sha256:e841068dc0be4cb6dfb5c890eb88cbdcff2f4a332393c7ec94e8e618bd32c1a8 \
|
||||||
|
--hash=sha256:e916035e3e9930cbdfdd10abf48861340221857f45509565898e012263f7b289 \
|
||||||
|
--hash=sha256:eba154571c16e032112afac0dc2dfe9e63c2ceb7aedd07bb7eecf2ce26d4dd4c \
|
||||||
|
--hash=sha256:f03460ff076f70ab595bb45a0205ccea1971443575b6920c52e755dec2b3fbfe \
|
||||||
|
--hash=sha256:f0ec3b750b59375eab5b0fb2b9254810c00a3375be6d789899f1055a1d556237 \
|
||||||
|
--hash=sha256:f291bcf42ae98eb5107edb162c3c998b4a89648fd8e99ed4cbd12705292788cd \
|
||||||
|
--hash=sha256:f61efe1d2fe0de16158a5fe1d1cf3c14bdb6aecd54d8938fd26512c525c1f624 \
|
||||||
|
--hash=sha256:f68edfc67aabac33708941f26f22a7b8e9f81429bc0cf249fcf7d66b23af8d19 \
|
||||||
|
--hash=sha256:fa95848c929b6a75f6848d3c9793e59db365ee436776e57db835cdbfa79ba977 \
|
||||||
|
--hash=sha256:fd9f8797427910198f95bced71ddfed61130d7e349213bfb8466c9c99e2c46a8 \
|
||||||
|
--hash=sha256:fdb4ca07ab75ffadab4a8b135ad59cdbb3156b99310f3d565370da74a15d6bd3
|
||||||
|
# via
|
||||||
|
# flask
|
||||||
|
# jinja2
|
||||||
|
# werkzeug
|
||||||
|
psycopg2==2.9.13 \
|
||||||
|
--hash=sha256:0d2fc7eedfaca0586dcf1476454598428d0d8471d3b5cb55f92015a5f9d0af40 \
|
||||||
|
--hash=sha256:10f7408b34412e8c0d4f8b1565541f1d651b1d00447857e5d8561b38f5c1a738 \
|
||||||
|
--hash=sha256:165e25c1b0e616a1f28080c5c68bd2dc015051d83c90240b2171d3e76ca2b5ff \
|
||||||
|
--hash=sha256:7d48416f6a4823ada9b33771085331b842b553df88435701bff5ddb4469905de \
|
||||||
|
--hash=sha256:a6f54fd8e0024f35240866b5dfff9ead2a0dbd33b8096eec438bc6093412842d \
|
||||||
|
--hash=sha256:d16e7a5f5e400ac51ca953d42255804eff6c8a9650b1a2074f6ca6261d740382 \
|
||||||
|
--hash=sha256:d36784fc2dae69523ba4b79c7d1d1b4d6e83e87836874f111262f4db940b16a6
|
||||||
|
# via -r requirements.in
|
||||||
|
werkzeug==3.1.9 \
|
||||||
|
--hash=sha256:55ca7c70a75689be937aa27f8ff4b018f06ff4838fc73045560bf0f5a1291060 \
|
||||||
|
--hash=sha256:6392e50c78460ba618e5b21f08a71f59c99ce99cdc6cf6e3dd7e6ccca8754fab
|
||||||
|
# via flask
|
||||||
Reference in new issue
Block a user