Much more work

This commit is contained in:
Andrea Mistrali 2024-07-03 16:38:29 +02:00
parent c94f31d5ee
commit 44625ca5f0
17 changed files with 706 additions and 281 deletions

1
.gitignore vendored
View File

@ -265,3 +265,4 @@ tags
# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)
main.py
hsman/.flaskenv

View File

@ -2,3 +2,4 @@ from .apikeys import APIKey
from .nodes import Node
from .users import User
from .routes import Route
from .preauthkeys import PreAuthKey, v1ListPreAuthKeyRequest

View File

@ -5,14 +5,14 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
class APISettings(BaseSettings):
model_config = SettingsConfigDict(env_prefix='HS_')
model_config = SettingsConfigDict(env_prefix='HSAPI_')
server: str = "http://localhost:8080"
api_path: str = "/api/v1"
api_token: Union[str, None] = None
ssl_verify: Union[bool, str] = True
def refresh_api_token(self):
self.api_token = os.environ.get('HS_API_TOKEN', 'default')
self.api_token = os.environ.get('HSAPI_API_TOKEN', 'default')
class HTTPException(Exception):

View File

@ -2,19 +2,19 @@ from flask import Flask, render_template
from werkzeug.exceptions import HTTPException
from flask_mobility import Mobility
from flask_pyoidc import OIDCAuthentication
# from flask_pyoidc import OIDCAuthentication
from flask_pyoidc.provider_configuration import ProviderConfiguration, ClientMetadata
from . import filters
from .lib import OIDCAuthentication
import os
mobility = Mobility()
client_metadata = ClientMetadata(
client_id='***REMOVED***',
client_secret='***REMOVED***',
post_logout_redirect_uris=['https://example.com/logout'])
client_secret='***REMOVED***')
provider_config = ProviderConfiguration(issuer='***REMOVED***',
@ -55,8 +55,7 @@ def create_app(environment='development'):
filters.init_app(app)
app.config.update(
OIDC_REDIRECT_URI='http://localhost:5000/redirect',
SECRET_KEY="secreto"
OIDC_REDIRECT_URI='http://localhost:5000/auth',
)
auth.init_app(app)

View File

@ -1,21 +1,152 @@
from app import models
import os
from flask import request
import functools
from flask import request, abort, current_app
from flask import session as flask_session
from flask_pyoidc import OIDCAuthentication as _OIDCAuth
from flask_pyoidc.user_session import UserSession
from typing import Callable, List
import logging
log = logging.getLogger(__name__)
def remote_ip():
def remote_ip() -> str:
if 'HTTP_X_FORWARDED_FOR' in request.environ:
xff_parts = request.environ.get('HTTP_X_FORWARDED_FOR').split(',')
return xff_parts[0]
else:
return request.environ.get('REMOTE_ADDR')
return str(request.environ.get('REMOTE_ADDR'))
def webMode():
def webMode() -> bool:
is_gunicorn = "gunicorn" in os.environ.get('SERVER_SOFTWARE', '')
is_werkzeug = os.environ.get('WERKZEUG_RUN_MAIN', False) == "true"
return is_gunicorn or is_werkzeug
class OIDCAuthentication(_OIDCAuth):
def authorize(self, provider_name: str, authz_fn: Callable, **kwargs):
if provider_name not in self._provider_configurations:
raise ValueError(
f"Provider name '{provider_name}' not in configured providers: {
self._provider_configurations.keys()}."
)
# We save args with which we have been called
external_args = kwargs
# Decorator
def oidc_decorator(view_func):
@functools.wraps(view_func)
def wrapper(*args, **kwargs):
# Retrieve session and client
session = UserSession(flask_session, provider_name)
client = self.clients[session.current_provider]
# Check session validity
if session.should_refresh(client.session_refresh_interval_seconds):
log.debug('user auth will be refreshed "silently"')
return self._authenticate(client, interactive=False)
elif session.is_authenticated():
log.debug('user is already authenticated')
else:
log.debug('user not authenticated, start flow')
return self._authenticate(client)
# Call authorization function that must return true or false
authorized = authz_fn(session, **external_args)
if authorized:
return view_func(*args, **kwargs)
else:
return abort(403)
return wrapper
return oidc_decorator
def authorize_domains(self,
provider_name: str,
domains: List[str]):
"""
Authorize a user if the email domain is in a list of domains
"""
def _authz_fn(session, domains) -> bool:
email = session.userinfo.get('email', "")
domain = email.split('@')[-1]
if domain in domains:
return True
return False
return self.authorize(provider_name,
authz_fn=_authz_fn,
domains=domains)
def authorize_users(self, provider_name: str, users: List[str]):
"""
Authorize a user if the username of the user part of the email
is in a list of usernames
"""
def _authz_fn(session, users) -> bool:
username = session.userinfo.get('preferred_username', "")
email = session.userinfo.get('email', "")
email_user = email.split('@')[0]
if username in users or email_user in users:
return True
return False
return self.authorize(provider_name,
authz_fn=_authz_fn,
users=users)
def authorize_groups(self, provider_name: str, groups: List[str]):
"""
Authorize members of a list of groups
"""
def _authz_fn(session, groups) -> bool:
user_groups = session.userinfo.get('groups', [])
if len(set(groups).intersection(user_groups)):
return True
return False
return self.authorize(provider_name,
authz_fn=_authz_fn,
groups=groups)
def authorize_admins(self, provider_name: str):
"""
Authorize admins.
Admins are taken from the app config:
- members of groups in ADMIN_GROUPS
- users in ADMIN_USERS
"""
def _authz_fn(session) -> bool:
user_groups = session.userinfo.get('groups', [])
username = session.userinfo.get('preferred_username', "")
with current_app.app_context():
admin_groups = current_app.config.get('ADMIN_GROUPS', [])
admin_users = current_app.config.get('ADMIN_USERS', [])
authorized_groups = set(admin_groups).intersection(user_groups)
if len(authorized_groups):
log.debug(f"'{username}' is a member of {
authorized_groups}")
return True
if username in admin_users:
log.debug(f"'{username}' is an admin user")
return True
return False
return self.authorize(provider_name,
authz_fn=_authz_fn)

View File

@ -35,34 +35,38 @@ body > .container-fluid {
padding-right: 1rem;
}
.theme-icon {
width: 1.3em;
height: 1.3em;
margin-right: 10px;
}
a.plain:link {
a:link {
color: unset;
text-decoration-line: underline;
text-decoration-style: dotted;
/* text-decoration-style: dotted; */
/* text-decoration-style: dashed; */
}
a.plain:visited {
a:visited {
color: unset;
text-decoration-line: underline;
text-decoration-style: dotted;
/* text-decoration-style: dotted; */
/* text-decoration-style: dashed; */
}
a.plain:hover {
a:hover {
color: unset;
text-decoration: underline;
text-decoration-line: unset;
/* text-decoration: underline; */
/* text-decoration-style: dotted; */
}
a.plain:active {
a:active {
color: unset;
text-decoration: none;
}
a.nodeco, a.nav-link, a.navbar-brand, a.routeToggle {
text-decoration-line: none;
text-decoration-style: unset;
}
table.dataTable tbody tr:hover {
background-color: #444;
}
@ -71,7 +75,27 @@ table.dataTable tbody tr:hover > .sorting_1 {
background-color: #444;
}
.data:hover{
div.data:hover{
/* background-color: rgba(63, 140, 211, 0.808); */
background-color: #444;
}
/* primary route */
a.route.primary {
font-weight: bold;
}
/* route enabled */
a.route.True {
font-style: italic;
color: #bbbbbb;
}
/* route disabled */
a.route.False {
font-style: italic;
text-decoration-line: line-through underline;
color: #888;
}
div.dt-container div.dt-scroll-body {
border-bottom: none !important;
}

View File

@ -1,55 +1,30 @@
// custom javascript
var sun_icon = `
<div class="theme-icon">
<svg xmlns="http://www.w3.org/2000/svg" class="theme-icon" fill="none" viewBox="0 0 24 24" stroke="yellow" stroke-opacity="0.6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
</div>
`;
var moon_icon = `
<div class="theme-icon">
<svg xmlns="http://www.w3.org/2000/svg" class="theme-icon" fill="none" viewBox="0 0 18 24" stroke="grey" stroke-opacity="0.6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
</div>
`;
function getCookie(cookie) {
let name = cookie + "=";
let ca = document.cookie.split(";");
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == " ") {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
function renameNode(nodeId) {
var newName = $("#newName").val();
var url = "/node/" + nodeId + "/rename/" + newName;
$.ajax({
url: url,
xhrFields: {
withCredentials: true,
},
success: function (data) {
$("#renameModal").modal("hide");
$("#givenName").html(data.newName);
},
});
}
function setCookie(cname, cvalue) {
const d = new Date();
d.setTime(d.getTime() + 3650 * 24 * 60 * 60 * 1000); // 10 years cookie
let expires = "expires=" + d.toUTCString();
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
}
function toggleTheme(obj) {
console.log(getCookie("theme"));
console.log(document.cookie);
if (getCookie("theme") == "light") {
setCookie("theme", "dark");
obj.html(sun_icon);
$("body").addClass("bootstrap-dark");
} else {
// switch to light mode
setCookie("theme", "light");
obj.html(moon_icon);
$("body").removeClass("bootstrap-dark");
function copyToClipboard(obj) {
var span = $(obj);
var value = span.attr("data-original-title");
var original = span.html();
try {
navigator.clipboard.writeText(value);
span.html("copied!");
setTimeout(function () {
span.html(original);
}, 500);
} catch (error) {
span.html("error");
console.error(error);
}
}

View File

@ -22,6 +22,8 @@
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500;600&display=swap" rel="stylesheet">
<!-- Century gothic font -->
<link rel="stylesheet" href="https://use.typekit.net/oov2wcw.css">
<!-- fontawesome -->
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.0/css/all.css" integrity="sha384-lZN37f5QGtY3VHgisS14W3ExzMWZxybE1SJSEsQp9S+oqd12jhcu+A56Ebc1zFSJ" crossorigin="anonymous">
<link href="{{ url_for('static', filename='main.css') }}" rel="stylesheet" media="screen">
{% block links %}{% endblock %}
<link rel="apple-touch-icon" sizes="180x180" href="/static/favicon/apple-touch-icon.png">
@ -59,13 +61,14 @@
<a class="nav-link" href="/routes">routes</a>
</li>
</ul>
<!-- <ul class="navbar-nav">
<ul class="navbar-nav">
<li class="nav-item me-right">
<a href="#" id="themeSwitch">
<div class="theme-icon">{{ mc.theme_icon(theme)}}</div>
<a href="/logout" id="themeSwitch">
<i class="fas fa-sign-out-alt"></i>
<!-- <i class="fas fa-plug-circle-xmark"></i> -->
</a>
</li>
</ul> -->
</ul>
</div>
</nav>
</header>
@ -92,8 +95,8 @@
</div>
</footer>
<!-- scripts -->
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
<script src="https://code.jquery.com/jquery-3.7.1.min.js"
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
@ -104,11 +107,6 @@
<script src="{{ url_for('static', filename='main.js') }}" type="text/javascript"></script>
<script>
$(function () {
// themeSwitch = $('#themeSwitch');
// themeSwitch.click(function(event) {
// event.preventDefault();
// toggleTheme(themeSwitch);
// })
$('[data-toggle="tooltip"]').tooltip();
})
</script>

View File

@ -1,12 +1,71 @@
{% extends "base.html" %}
{% block content %}
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Egestas tellus rutrum tellus pellentesque eu tincidunt. Aenean et tortor at risus viverra adipiscing. Et malesuada fames ac turpis egestas sed tempus. Amet commodo nulla facilisi nullam vehicula ipsum a arcu. Quam viverra orci sagittis eu volutpat odio. Elit ut aliquam purus sit amet luctus. Vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt ornare massa. Ultrices gravida dictum fusce ut placerat orci nulla. Sed faucibus turpis in eu mi bibendum. Vel facilisis volutpat est velit egestas dui id ornare arcu. Scelerisque eu ultrices vitae auctor. Sem nulla pharetra diam sit amet. Lectus proin nibh nisl condimentum id venenatis a condimentum vitae.
</p>
<p></p>
<h3>Welcome, {{ session.userinfo.name }}</h3>
<hr>
<p>
Auctor elit sed vulputate mi sit amet mauris. Tincidunt eget nullam non nisi est. Leo vel orci porta non pulvinar neque laoreet suspendisse interdum. Volutpat commodo sed egestas egestas fringilla phasellus faucibus scelerisque. Ipsum dolor sit amet consectetur adipiscing elit pellentesque habitant morbi. Ultricies mi quis hendrerit dolor magna. Porta nibh venenatis cras sed felis eget. Quam vulputate dignissim suspendisse in. Pellentesque dignissim enim sit amet venenatis urna cursus eget nunc. Nunc lobortis mattis aliquam faucibus purus in.
</p>
<h4>authentication info</h4>
<div class="row data">
<div class="col col-2">
<strong>email</strong>
</div>
<div class="col col-6">
{{ session.userinfo.email }}
<!-- {{ session.userinfo.email_verified | fancyBool | safe }} -->
</div>
</div>
<div class="row data">
<div class="col col-2">
<strong>username</strong>
</div>
<div class="col col-6">
{{ session.userinfo.preferred_username }}
</div>
</div>
<div class="row data">
<div class="col col-2">
<strong>groups</strong>
</div>
<div class="col col-6">
<i class="fas fa-angle-right"></i>
{{ session.userinfo.groups[0]}}
</div>
</div>
{% for group in session.userinfo.groups[1:] |sort %}
<div class="row data">
<div class="col col-2">
&nbsp;
</div>
<div class="col col-6">
<i class="fas fa-angle-right"></i> {{ group }}
</div>
</div>
{% endfor %}
<hr>
<h4>your devices</h4>
<div class="row strong">
<div class="col col-2"><strong></strong></div>
<div class="col col-2"><strong>registered</strong></div>
<div class="col col-2"><strong>last event</strong></div>
<div class="col col-2"><strong>online</strong></div>
</div>
{% for node in userNodeList %}
<div class="row data">
<div class="col col-2">
{{ node.givenName}}
</div>
<div class="col col-2">
<span data-toggle="tooltip" data-placement="right" title="{{ node.createdAt | fmt_datetime }}">
{{node.createdAt | htime_dt }}
</span>
</div>
<div class="col col-2">
<span data-toggle="tooltip" data-placement="right" title="{{ node.lastSeen | fmt_datetime }}">
{{node.lastSeen | htime_dt }}
</span>
</div>
<div class="col col-2">
{{node.online | fancyBool | safe }}
</div>
</div>
{% endfor %}
{% endblock %}

View File

@ -6,7 +6,15 @@
<strong>name</strong>
</div>
<div class="col col-8 float-left">
{{ node.givenName }}
<span id="givenName">{{ node.givenName }}</span>
<a href="#" data-toggle="modal" data-target="#renameModal">
<span
data-toggle="tooltip"
data-placement="right"
title="rename node">
<i class="fas fa-edit"></i>
</span>
</a>
</div>
</div>
<div class="row">
@ -28,6 +36,11 @@
<span data-toggle="tooltip" data-placement="right" title="{{ node.expiry | fmt_datetime }}">
{{ node.expiry | htime_dt }}
</span>
<a href="/node/{{ node.id }}/expire">
<span data-toggle="tooltip" data-placement="right" title="expire/disconnect node">
<i class="fas fa-plug"></i>
</span>
</a>
</div>
</div>
@ -130,17 +143,17 @@
</div>
<p></p>
<div class="row">
<div class="col col-3 float-left">
<h5>
routes
<h5>routes
{% if isExitNode %}
<span class="small badge-pill badge-success">Exit Node</span>
{% endif %}
</h5>
</div>
</div>
{% if routes %}
<div class="row">
<div class="col col-3 float-left">
<strong>prefix</strong>
@ -165,4 +178,33 @@
</div>
</div>
{% endfor %}
{% else %}
<div class="row">
<div class="col col-9 text-center">
<h3>No routes announced</h3>
</div>
</div>
<!-- rename modal -->
<!-- Modal -->
<div class="modal fade" id="renameModal" tabindex="-1" role="dialog" aria-labelledby="renameModal" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="renameModalLabel">Rename node</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<input type="text" name="newName" id="newName" value="{{ node.givenName}}">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" onClick="renameNode(id={{node.id}})">Save changes</button>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}

View File

@ -10,9 +10,10 @@
<tr>
<th>name</th>
<th>user</th>
<th>registered on</th>
<th>last connect</th>
<th>registered</th>
<th>last event</th>
<th>online</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
@ -23,7 +24,11 @@
{{node.givenName}}
</a>
</td>
<td>{{node.user.name}}</td>
<td>
<a class="plain" href="{{ url_for('main.user', userName=node.user.name)}}">
{{node.user.name}}
</a>
</td>
<td data-order="{{ node.createdAt }}">
<span data-toggle="tooltip" data-placement="right" title="{{ node.createdAt | fmt_datetime }}">
{{node.createdAt | htime_dt }}
@ -37,6 +42,18 @@
<td data-filter="{{ node.online | fancyOnline }}">
{{node.online | fancyBool | safe}}
</td>
<td class="no-sort">
<span data-toggle="tooltip" data-placement="right" title="expire/disconnect">
<a class="nodeco" href="/node/{{node.id}}/list-expire">
<i class="fas fa-plug"></i>
</a>
</span>
<span data-toggle="tooltip" data-placement="right" title="delete">
<a class="nodeco" href="/node/{{node.id}}/delete">
<i class="fas fa-trash"></i>
</a>
</span>
</td>
</tr>
{% endfor %}
</tbody>
@ -55,6 +72,9 @@
fixedHeader: false,
select: false,
keys: false,
aoColumnDefs: [
{ 'bSortable': false, 'aTargets': [ -1 ] }
],
});
})
</script>

View File

@ -2,6 +2,15 @@
{% block content %}
<h5>
Routing table
<small class="text-muted">
click on the icon in <em>enabled</em> column to toggle route status
</small>
</h5>
<hr>
<p></p>
<div class="row">
<div class="col col-8">
<h5>Exit nodes</h5>
@ -31,7 +40,7 @@
<strong>enabled</strong>
</div>
<div class="col col-2 float-left">
<strong>primary</strong>
<strong>active</strong>
</div>
</div>
<div class="row data">
@ -39,23 +48,21 @@
{{ prefix}}
</div>
<div class="col col-2 float-left">
<span>
<strong>
<a class="plain" href="{{ url_for('main.node', nodeId=rts[0].node.id) }}">
<a class="plain route primary" href="{{ url_for('main.node', nodeId=rts[0].node.id) }}">
<span data-toggle="tooltip" data-placement="right"
title="{{ rts[0].node.ipAddresses | join('\n') }}">
{{ rts[0].node.givenName}}
</span>
</a>
</strong>
</span>
</div>
<div class="col col-2 float-left">
<a class="routeToggle" href="/routeToggle/{{rts[0].id}}">
{{ rts[0].enabled | fancyBool | safe}}
</a>
</div>
<div class="col col-2 float-left">
{{ rts[0].isPrimary | fancyBool | safe}}
</div>
<div class="col col-2 float-left">
{{ rts[0].enabled | fancyBool | safe}}
</div>
</div>
{% for rt in rts[1:] %}
<div class="row data">
@ -63,27 +70,17 @@
<span>&nbsp;</span>
</div>
<div class="col col-2">
{% if not rt.enabled %}
<s>
<a class="plain" href="{{ url_for('main.node', nodeId=rt.node.id) }}">
<a class="plain route {{rt.enabled}}" href="{{ url_for('main.node', nodeId=rt.node.id) }}">
<span data-toggle="tooltip" data-placement="right"
title="{{ rt.node.ipAddresses | join('\n') }}">
{{ rt.node.givenName}}
</span>
</a>
</s>
{% else %}
<em>
<a class="plain" href="{{ url_for('main.node', nodeId=rt.node.id) }}">
<span data-toggle="tooltip" data-placement="right"
title="{{ rt.node.ipAddresses | join('\n') }}">
{{ rt.node.givenName}}
</a>
</em>
{% endif %}
</div>
<div class="col col-2 float-left">
<a class="routeToggle" href="/routeToggle/{{rt.id}}">
{{ rt.enabled | fancyBool | safe}}
</a>
</div>
<div class="col col-2 float-left">
{{ rt.isPrimary | fancyBool | safe}}

View File

@ -1,4 +1,8 @@
{% extends "base.html" %}
{% block links %}
<!-- Datatables -->
<link href="https://cdn.datatables.net/v/bs4/dt-2.0.8/b-3.0.2/b-colvis-3.0.2/b-html5-3.0.2/cr-2.0.3/sr-1.4.1/datatables.min.css" rel="stylesheet">
{% endblock %}
{% block content %}
@ -21,40 +25,130 @@
</div>
</div>
<p></p>
<div class="row">
<div class="col col-3 float-left">
<h5>nodes</h5>
</div>
</div>
<div class="row">
<div class="col col-3 float-left">
<strong>name</strong>
</div>
<div class="col col-3">
<strong>last connect</strong>
</div>
<div class="col col-3">
<strong>online</strong>
</div>
</div>
<h5>nodes</h5>
<table id="nodes" class="display" style="width:100%">
<thead>
<tr>
<th>name</th>
<th>last connect</th>
<th>online</th>
</tr>
</thead>
<tbody>
{% for node in userNodeList %}
<div class="row data">
<div class="col col-3 float-left">
<tr>
<td>
<a href="{{ url_for('main.node', nodeId=node.id) }}" class="plain">
{{ node.givenName }}
</a>
</div>
<div class="col col-3">
</td>
<td>
<span data-toggle="tooltip" data-placement="right" title="{{ node.lastSeen | fmt_datetime }}">
{{node.lastSeen | htime_dt }}
</span>
</div>
<div class="col col-3">
</td>
<td>
{{node.online | fancyBool | safe}}
</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<p></p>
<h5>pre auth keys</h5>
<table id="paks" class="display" style="width:100%">
<thead>
<tr>
<th>name</th>
<th>created</th>
<th>expiration</th>
<th>attributes</th>
<!-- <th>valid</th> -->
</tr>
</thead>
<tbody>
{% for key in preauthKeys %}
<tr>
<td>
<span data-toggle="tooltip"
data-placement="right"
title="{{ key.key }}"
class="pak_copy">{{ key.key[:10] }}&hellip;</span>
</td>
<td>
<span data-toggle="tooltip"
data-placement="right"
title="{{ key.createdAt | fmt_datetime }}">
{{key.createdAt | htime_dt }}
</span>
</td>
<td>
<span data-toggle="tooltip"
data-placement="right"
title="{{ key.expiration | fmt_datetime }}">
{{key.expiration | htime_dt }}
</span>
</td>
<td>
{% if key.ephemeral %}
<span class="badge badge-pill badge-primary">ephemereal</span>
{% endif %}
{% if key.reusable %}
<span class="badge badge-pill badge-primary">reusable</span>
{% endif %}
{% if key.used %}
<span class="badge badge-pill badge-primary">used</span>
{% endif %}
</td>
<!-- <td>
{{(not key.expired) | fancyBool | safe}}
</td> -->
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
{% block scripts %}
<script src="https://cdn.datatables.net/v/bs4/dt-2.0.8/b-3.0.2/b-colvis-3.0.2/b-html5-3.0.2/cr-2.0.3/sr-1.4.1/datatables.min.js"></script>
<script>
$(function () {
$('.pak_copy').on('click', function() {
copyToClipboard(this)
})
new DataTable('#nodes', {
scrollY: 130,
scrollCollapse: true,
paging: false,
// lengthMenu: [5, 10, 30, 50, { label: 'All', value: -1 }],
// pageLength: 10,
fixedHeader: {
header: true,
footer: false
},
info: false,
searching: false,
select: false,
keys: false,
});
new DataTable('#paks', {
scrollY: 130,
scrollCollapse: true,
paging: false,
// lengthMenu: [5, 10, 30, 50, { label: 'All', value: -1 }],
// pageLength: 10,
fixedHeader: {
header: true,
footer: false
},
info: false,
searching: false,
select: false,
keys: false,
});
})
</script>
{% endblock %}

View File

@ -11,6 +11,7 @@
<th>name</th>
<th>registered on</th>
<th>online</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
@ -29,6 +30,13 @@
<td data-filter="{{ online[user.name] | fancyOnline }}">
{{online[user.name] | fancyBool | safe }}
</td>
<td class="no-sort">
<span data-toggle="tooltip" data-placement="right" title="delete">
<a class="nodeco" href="/user/{{user.name}}/delete">
<i class="fas fa-trash"></i>
</a>
</span>
</td>
</tr>
{% endfor %}
</tbody>
@ -47,6 +55,9 @@
fixedHeader: false,
select: false,
keys: false,
aoColumnDefs: [
{ 'bSortable': false, 'aTargets': [ -1 ] }
],
});
})
</script>

View File

@ -1,11 +1,13 @@
from flask import render_template, Blueprint, current_app, g
from flask import request, after_this_request, redirect, session
from flask import render_template, Blueprint
from flask import redirect, session, url_for
from app import auth
from flask import jsonify
from flask_pyoidc.user_session import UserSession
from hsapi import Node, User, Route
from hsapi import Node, User, Route, PreAuthKey, v1ListPreAuthKeyRequest
from .lib import remote_ip
import logging
log = logging.getLogger()
@ -13,18 +15,28 @@ log = logging.getLogger()
main_blueprint = Blueprint('main', __name__)
# @main_blueprint.before_request
# def set_theme():
# g.theme = request.cookies.get('theme', 'light')
@main_blueprint.route('/', methods=['GET', 'POST'])
@auth.oidc_auth('default')
@auth.access_control('default')
def index():
user_session = UserSession(session)
return jsonify(access_token=user_session.access_token,
id_token=user_session.id_token,
userinfo=user_session.userinfo)
hs_user = user_session.userinfo['email'].split('@')[0]
userNodeList = [n for n in Node().list().nodes if n.user.name == hs_user]
return render_template('index.html',
userNodeList=userNodeList,
session=user_session)
@main_blueprint.route('/token', methods=['GET', 'POST'])
@auth.access_control('default')
def token():
user_session = UserSession(session)
return jsonify(user_session.userinfo)
@main_blueprint.route('/call', methods=['GET', 'POST'])
@auth.access_control('default')
def call():
return "CALL OK"
@main_blueprint.route('/logout')
@ -34,16 +46,15 @@ def logout():
@main_blueprint.route('/nodes', methods=['GET'])
@auth.authorize_admins('default')
def nodes():
nodelist = Node().list()
log.debug(nodelist)
return render_template('nodes.html',
nodes=nodelist.nodes)
@main_blueprint.route('/node/<int:nodeId>', methods=['GET'])
@auth.authorize_admins('default')
def node(nodeId):
# There is a bug in HS api with retrieving a single node
# and we added a workaround to hsapi, so node.get() returns a
@ -52,24 +63,21 @@ def node(nodeId):
node = Node().get(nodeId)
routes = Node().routes(nodeId)
isExitNode = any((r for r in routes.routes if r.prefix.endswith('/0')))
log.debug(node)
return render_template("node.html",
routes=routes.routes,
isExitNode=isExitNode,
node=node.node)
node=node)
@main_blueprint.route('/users', methods=['GET'])
@auth.authorize_admins('default')
def users():
userList = User().list()
# Get online status of devices of the user
online = {}
nodeList = Node().list()
for user in userList.users:
log.debug(user)
userNodeList = [n for n in nodeList.nodes if n.user.name == user.name]
log.debug(userNodeList)
online[user.name] = any(map(lambda x: x.online, userNodeList))
return render_template('users.html',
@ -78,23 +86,25 @@ def users():
@main_blueprint.route('/user/<userName>', methods=['GET'])
@auth.authorize_admins('default')
def user(userName):
# There is a bug in HS api with retrieving a single node
# and we added a workaround to hsapi, so node.get() returns a
# v1Node object instead of v1NodeResponse, so we access directly
# `node`, instead of `node.node`
user = User().get(userName)
log.debug(user)
userNodeList = [n for n in Node().list().nodes if n.user.name == userName]
preauthkeyreq = v1ListPreAuthKeyRequest(user=userName)
preauthKeys = PreAuthKey().list(preauthkeyreq)
validpak = [k for k in preauthKeys.preAuthKeys if not k.expired]
return render_template("user.html",
user=user.user,
preauthKeys=validpak,
userNodeList=userNodeList)
@main_blueprint.route('/routes', methods=['GET'])
@auth.authorize_admins('default')
def routes():
routes = Route().list()
log.debug(routes)
prefixes = set(
(r.prefix for r in routes.routes if not r.prefix.endswith('/0')))
@ -105,7 +115,59 @@ def routes():
for prefix in prefixes:
rrp = [x for x in routes.routes if x.prefix == prefix]
final[prefix] = sorted(rrp, key=lambda x: x.isPrimary, reverse=True)
log.debug(final)
return render_template("routes.html",
exitNodes=exitNodes,
routes=final)
@main_blueprint.route('/routeToggle/<int:routeId>', methods=['GET'])
@auth.authorize_admins('default')
def routeToggle(routeId: int):
routes = Route().list()
route = [r for r in routes.routes if r.id == routeId]
if route:
route = route[0]
if route.enabled:
Route().disable(routeId)
else:
Route().enable(routeId)
return redirect(url_for("main.routes"))
@main_blueprint.route('/node/<int:nodeId>/expire', methods=['GET'])
@auth.authorize_admins('default')
def expireNode(nodeId: int):
Node().expire(nodeId)
return redirect(url_for("main.node", nodeId=nodeId))
@main_blueprint.route('/node/<int:nodeId>/list-expire', methods=['GET'])
@auth.authorize_admins('default')
def expireNodeList(nodeId: int):
Node().expire(nodeId)
return redirect(url_for("main.nodes"))
@main_blueprint.route('/node/<int:nodeId>/delete', methods=['GET'])
@auth.authorize_admins('default')
def deleteNode(nodeId: int):
Node().delete(nodeId)
return redirect(url_for("main.nodes"))
@main_blueprint.route('/node/<int:nodeId>/rename/<newName>', methods=['GET'])
@auth.authorize_admins('default')
def renameNode(nodeId: int, newName: str):
Node().rename(nodeId, newName)
return jsonify(dict(newName=newName))
@main_blueprint.route('/user/<userName>/delete', methods=['GET'])
@auth.authorize_admins('default')
def deleteUser(userName: str):
nodes = Node().byUser(userName)
for node in nodes.nodes:
Node().expire(node.id)
Node().delete(node.id)
User().delete(userName)
return redirect(url_for("main.users"))

186
hsman/poetry.lock generated
View File

@ -447,11 +447,11 @@ tornado = ["tornado (>=0.2)"]
[[package]]
name = "hsapi"
version = "0.9.0"
description = "Headscale API module"
description = "Headscale API client"
optional = false
python-versions = ">=3.11,<4.0"
files = [
{file = "hsapi-0.9.0-py3-none-any.whl", hash = "sha256:71697fca037bbebf6b6daf233b1ee7c43c52732f5780a9543a2dac154aef556b"},
{file = "hsapi-0.9.0-py3-none-any.whl", hash = "sha256:51b0a2424d6a93451710129f6232aa1552a83402f59e3e9a2cb7525000886d54"},
]
[package.dependencies]
@ -851,109 +851,121 @@ files = [
[[package]]
name = "pydantic"
version = "2.7.4"
version = "2.8.0"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic-2.7.4-py3-none-any.whl", hash = "sha256:ee8538d41ccb9c0a9ad3e0e5f07bf15ed8015b481ced539a1759d8cc89ae90d0"},
{file = "pydantic-2.7.4.tar.gz", hash = "sha256:0c84efd9548d545f63ac0060c1e4d39bb9b14db8b3c0652338aecc07b5adec52"},
{file = "pydantic-2.8.0-py3-none-any.whl", hash = "sha256:ead4f3a1e92386a734ca1411cb25d94147cf8778ed5be6b56749047676d6364e"},
{file = "pydantic-2.8.0.tar.gz", hash = "sha256:d970ffb9d030b710795878940bd0489842c638e7252fc4a19c3ae2f7da4d6141"},
]
[package.dependencies]
annotated-types = ">=0.4.0"
pydantic-core = "2.18.4"
typing-extensions = ">=4.6.1"
pydantic-core = "2.20.0"
typing-extensions = [
{version = ">=4.12.2", markers = "python_version >= \"3.13\""},
{version = ">=4.6.1", markers = "python_version < \"3.13\""},
]
[package.extras]
email = ["email-validator (>=2.0.0)"]
[[package]]
name = "pydantic-core"
version = "2.18.4"
version = "2.20.0"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic_core-2.18.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f76d0ad001edd426b92233d45c746fd08f467d56100fd8f30e9ace4b005266e4"},
{file = "pydantic_core-2.18.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:59ff3e89f4eaf14050c8022011862df275b552caef8082e37b542b066ce1ff26"},
{file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a55b5b16c839df1070bc113c1f7f94a0af4433fcfa1b41799ce7606e5c79ce0a"},
{file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4d0dcc59664fcb8974b356fe0a18a672d6d7cf9f54746c05f43275fc48636851"},
{file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8951eee36c57cd128f779e641e21eb40bc5073eb28b2d23f33eb0ef14ffb3f5d"},
{file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4701b19f7e3a06ea655513f7938de6f108123bf7c86bbebb1196eb9bd35cf724"},
{file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e00a3f196329e08e43d99b79b286d60ce46bed10f2280d25a1718399457e06be"},
{file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:97736815b9cc893b2b7f663628e63f436018b75f44854c8027040e05230eeddb"},
{file = "pydantic_core-2.18.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6891a2ae0e8692679c07728819b6e2b822fb30ca7445f67bbf6509b25a96332c"},
{file = "pydantic_core-2.18.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bc4ff9805858bd54d1a20efff925ccd89c9d2e7cf4986144b30802bf78091c3e"},
{file = "pydantic_core-2.18.4-cp310-none-win32.whl", hash = "sha256:1b4de2e51bbcb61fdebd0ab86ef28062704f62c82bbf4addc4e37fa4b00b7cbc"},
{file = "pydantic_core-2.18.4-cp310-none-win_amd64.whl", hash = "sha256:6a750aec7bf431517a9fd78cb93c97b9b0c496090fee84a47a0d23668976b4b0"},
{file = "pydantic_core-2.18.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:942ba11e7dfb66dc70f9ae66b33452f51ac7bb90676da39a7345e99ffb55402d"},
{file = "pydantic_core-2.18.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b2ebef0e0b4454320274f5e83a41844c63438fdc874ea40a8b5b4ecb7693f1c4"},
{file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a642295cd0c8df1b86fc3dced1d067874c353a188dc8e0f744626d49e9aa51c4"},
{file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f09baa656c904807e832cf9cce799c6460c450c4ad80803517032da0cd062e2"},
{file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98906207f29bc2c459ff64fa007afd10a8c8ac080f7e4d5beff4c97086a3dabd"},
{file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19894b95aacfa98e7cb093cd7881a0c76f55731efad31073db4521e2b6ff5b7d"},
{file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fbbdc827fe5e42e4d196c746b890b3d72876bdbf160b0eafe9f0334525119c8"},
{file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f85d05aa0918283cf29a30b547b4df2fbb56b45b135f9e35b6807cb28bc47951"},
{file = "pydantic_core-2.18.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e85637bc8fe81ddb73fda9e56bab24560bdddfa98aa64f87aaa4e4b6730c23d2"},
{file = "pydantic_core-2.18.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2f5966897e5461f818e136b8451d0551a2e77259eb0f73a837027b47dc95dab9"},
{file = "pydantic_core-2.18.4-cp311-none-win32.whl", hash = "sha256:44c7486a4228413c317952e9d89598bcdfb06399735e49e0f8df643e1ccd0558"},
{file = "pydantic_core-2.18.4-cp311-none-win_amd64.whl", hash = "sha256:8a7164fe2005d03c64fd3b85649891cd4953a8de53107940bf272500ba8a788b"},
{file = "pydantic_core-2.18.4-cp311-none-win_arm64.whl", hash = "sha256:4e99bc050fe65c450344421017f98298a97cefc18c53bb2f7b3531eb39bc7805"},
{file = "pydantic_core-2.18.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6f5c4d41b2771c730ea1c34e458e781b18cc668d194958e0112455fff4e402b2"},
{file = "pydantic_core-2.18.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2fdf2156aa3d017fddf8aea5adfba9f777db1d6022d392b682d2a8329e087cef"},
{file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4748321b5078216070b151d5271ef3e7cc905ab170bbfd27d5c83ee3ec436695"},
{file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847a35c4d58721c5dc3dba599878ebbdfd96784f3fb8bb2c356e123bdcd73f34"},
{file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c40d4eaad41f78e3bbda31b89edc46a3f3dc6e171bf0ecf097ff7a0ffff7cb1"},
{file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:21a5e440dbe315ab9825fcd459b8814bb92b27c974cbc23c3e8baa2b76890077"},
{file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01dd777215e2aa86dfd664daed5957704b769e726626393438f9c87690ce78c3"},
{file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4b06beb3b3f1479d32befd1f3079cc47b34fa2da62457cdf6c963393340b56e9"},
{file = "pydantic_core-2.18.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:564d7922e4b13a16b98772441879fcdcbe82ff50daa622d681dd682175ea918c"},
{file = "pydantic_core-2.18.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0eb2a4f660fcd8e2b1c90ad566db2b98d7f3f4717c64fe0a83e0adb39766d5b8"},
{file = "pydantic_core-2.18.4-cp312-none-win32.whl", hash = "sha256:8b8bab4c97248095ae0c4455b5a1cd1cdd96e4e4769306ab19dda135ea4cdb07"},
{file = "pydantic_core-2.18.4-cp312-none-win_amd64.whl", hash = "sha256:14601cdb733d741b8958224030e2bfe21a4a881fb3dd6fbb21f071cabd48fa0a"},
{file = "pydantic_core-2.18.4-cp312-none-win_arm64.whl", hash = "sha256:c1322d7dd74713dcc157a2b7898a564ab091ca6c58302d5c7b4c07296e3fd00f"},
{file = "pydantic_core-2.18.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:823be1deb01793da05ecb0484d6c9e20baebb39bd42b5d72636ae9cf8350dbd2"},
{file = "pydantic_core-2.18.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebef0dd9bf9b812bf75bda96743f2a6c5734a02092ae7f721c048d156d5fabae"},
{file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae1d6df168efb88d7d522664693607b80b4080be6750c913eefb77e34c12c71a"},
{file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9899c94762343f2cc2fc64c13e7cae4c3cc65cdfc87dd810a31654c9b7358cc"},
{file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99457f184ad90235cfe8461c4d70ab7dd2680e28821c29eca00252ba90308c78"},
{file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18f469a3d2a2fdafe99296a87e8a4c37748b5080a26b806a707f25a902c040a8"},
{file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7cdf28938ac6b8b49ae5e92f2735056a7ba99c9b110a474473fd71185c1af5d"},
{file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:938cb21650855054dc54dfd9120a851c974f95450f00683399006aa6e8abb057"},
{file = "pydantic_core-2.18.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:44cd83ab6a51da80fb5adbd9560e26018e2ac7826f9626bc06ca3dc074cd198b"},
{file = "pydantic_core-2.18.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:972658f4a72d02b8abfa2581d92d59f59897d2e9f7e708fdabe922f9087773af"},
{file = "pydantic_core-2.18.4-cp38-none-win32.whl", hash = "sha256:1d886dc848e60cb7666f771e406acae54ab279b9f1e4143babc9c2258213daa2"},
{file = "pydantic_core-2.18.4-cp38-none-win_amd64.whl", hash = "sha256:bb4462bd43c2460774914b8525f79b00f8f407c945d50881568f294c1d9b4443"},
{file = "pydantic_core-2.18.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:44a688331d4a4e2129140a8118479443bd6f1905231138971372fcde37e43528"},
{file = "pydantic_core-2.18.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a2fdd81edd64342c85ac7cf2753ccae0b79bf2dfa063785503cb85a7d3593223"},
{file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86110d7e1907ab36691f80b33eb2da87d780f4739ae773e5fc83fb272f88825f"},
{file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46387e38bd641b3ee5ce247563b60c5ca098da9c56c75c157a05eaa0933ed154"},
{file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:123c3cec203e3f5ac7b000bd82235f1a3eced8665b63d18be751f115588fea30"},
{file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dc1803ac5c32ec324c5261c7209e8f8ce88e83254c4e1aebdc8b0a39f9ddb443"},
{file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53db086f9f6ab2b4061958d9c276d1dbe3690e8dd727d6abf2321d6cce37fa94"},
{file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abc267fa9837245cc28ea6929f19fa335f3dc330a35d2e45509b6566dc18be23"},
{file = "pydantic_core-2.18.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a0d829524aaefdebccb869eed855e2d04c21d2d7479b6cada7ace5448416597b"},
{file = "pydantic_core-2.18.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:509daade3b8649f80d4e5ff21aa5673e4ebe58590b25fe42fac5f0f52c6f034a"},
{file = "pydantic_core-2.18.4-cp39-none-win32.whl", hash = "sha256:ca26a1e73c48cfc54c4a76ff78df3727b9d9f4ccc8dbee4ae3f73306a591676d"},
{file = "pydantic_core-2.18.4-cp39-none-win_amd64.whl", hash = "sha256:c67598100338d5d985db1b3d21f3619ef392e185e71b8d52bceacc4a7771ea7e"},
{file = "pydantic_core-2.18.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:574d92eac874f7f4db0ca653514d823a0d22e2354359d0759e3f6a406db5d55d"},
{file = "pydantic_core-2.18.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1f4d26ceb5eb9eed4af91bebeae4b06c3fb28966ca3a8fb765208cf6b51102ab"},
{file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77450e6d20016ec41f43ca4a6c63e9fdde03f0ae3fe90e7c27bdbeaece8b1ed4"},
{file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d323a01da91851a4f17bf592faf46149c9169d68430b3146dcba2bb5e5719abc"},
{file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43d447dd2ae072a0065389092a231283f62d960030ecd27565672bd40746c507"},
{file = "pydantic_core-2.18.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:578e24f761f3b425834f297b9935e1ce2e30f51400964ce4801002435a1b41ef"},
{file = "pydantic_core-2.18.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:81b5efb2f126454586d0f40c4d834010979cb80785173d1586df845a632e4e6d"},
{file = "pydantic_core-2.18.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ab86ce7c8f9bea87b9d12c7f0af71102acbf5ecbc66c17796cff45dae54ef9a5"},
{file = "pydantic_core-2.18.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:90afc12421df2b1b4dcc975f814e21bc1754640d502a2fbcc6d41e77af5ec312"},
{file = "pydantic_core-2.18.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:51991a89639a912c17bef4b45c87bd83593aee0437d8102556af4885811d59f5"},
{file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:293afe532740370aba8c060882f7d26cfd00c94cae32fd2e212a3a6e3b7bc15e"},
{file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48ece5bde2e768197a2d0f6e925f9d7e3e826f0ad2271120f8144a9db18d5c8"},
{file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eae237477a873ab46e8dd748e515c72c0c804fb380fbe6c85533c7de51f23a8f"},
{file = "pydantic_core-2.18.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:834b5230b5dfc0c1ec37b2fda433b271cbbc0e507560b5d1588e2cc1148cf1ce"},
{file = "pydantic_core-2.18.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e858ac0a25074ba4bce653f9b5d0a85b7456eaddadc0ce82d3878c22489fa4ee"},
{file = "pydantic_core-2.18.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2fd41f6eff4c20778d717af1cc50eca52f5afe7805ee530a4fbd0bae284f16e9"},
{file = "pydantic_core-2.18.4.tar.gz", hash = "sha256:ec3beeada09ff865c344ff3bc2f427f5e6c26401cc6113d77e372c3fdac73864"},
{file = "pydantic_core-2.20.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e9dcd7fb34f7bfb239b5fa420033642fff0ad676b765559c3737b91f664d4fa9"},
{file = "pydantic_core-2.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:649a764d9b0da29816889424697b2a3746963ad36d3e0968784ceed6e40c6355"},
{file = "pydantic_core-2.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7701df088d0b05f3460f7ba15aec81ac8b0fb5690367dfd072a6c38cf5b7fdb5"},
{file = "pydantic_core-2.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab760f17c3e792225cdaef31ca23c0aea45c14ce80d8eff62503f86a5ab76bff"},
{file = "pydantic_core-2.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb1ad5b4d73cde784cf64580166568074f5ccd2548d765e690546cff3d80937d"},
{file = "pydantic_core-2.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b81ec2efc04fc1dbf400647d4357d64fb25543bae38d2d19787d69360aad21c9"},
{file = "pydantic_core-2.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4a9732a5cad764ba37f3aa873dccb41b584f69c347a57323eda0930deec8e10"},
{file = "pydantic_core-2.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6dc85b9e10cc21d9c1055f15684f76fa4facadddcb6cd63abab702eb93c98943"},
{file = "pydantic_core-2.20.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:21d9f7e24f63fdc7118e6cc49defaab8c1d27570782f7e5256169d77498cf7c7"},
{file = "pydantic_core-2.20.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8b315685832ab9287e6124b5d74fc12dda31e6421d7f6b08525791452844bc2d"},
{file = "pydantic_core-2.20.0-cp310-none-win32.whl", hash = "sha256:c3dc8ec8b87c7ad534c75b8855168a08a7036fdb9deeeed5705ba9410721c84d"},
{file = "pydantic_core-2.20.0-cp310-none-win_amd64.whl", hash = "sha256:85770b4b37bb36ef93a6122601795231225641003e0318d23c6233c59b424279"},
{file = "pydantic_core-2.20.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:58e251bb5a5998f7226dc90b0b753eeffa720bd66664eba51927c2a7a2d5f32c"},
{file = "pydantic_core-2.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:78d584caac52c24240ef9ecd75de64c760bbd0e20dbf6973631815e3ef16ef8b"},
{file = "pydantic_core-2.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5084ec9721f82bef5ff7c4d1ee65e1626783abb585f8c0993833490b63fe1792"},
{file = "pydantic_core-2.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6d0f52684868db7c218437d260e14d37948b094493f2646f22d3dda7229bbe3f"},
{file = "pydantic_core-2.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1def125d59a87fe451212a72ab9ed34c118ff771e5473fef4f2f95d8ede26d75"},
{file = "pydantic_core-2.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b34480fd6778ab356abf1e9086a4ced95002a1e195e8d2fd182b0def9d944d11"},
{file = "pydantic_core-2.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d42669d319db366cb567c3b444f43caa7ffb779bf9530692c6f244fc635a41eb"},
{file = "pydantic_core-2.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53b06aea7a48919a254b32107647be9128c066aaa6ee6d5d08222325f25ef175"},
{file = "pydantic_core-2.20.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1f038156b696a1c39d763b2080aeefa87ddb4162c10aa9fabfefffc3dd8180fa"},
{file = "pydantic_core-2.20.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3f0f3a4a23717280a5ee3ac4fb1f81d6fde604c9ec5100f7f6f987716bb8c137"},
{file = "pydantic_core-2.20.0-cp311-none-win32.whl", hash = "sha256:316fe7c3fec017affd916a0c83d6f1ec697cbbbdf1124769fa73328e7907cc2e"},
{file = "pydantic_core-2.20.0-cp311-none-win_amd64.whl", hash = "sha256:2d06a7fa437f93782e3f32d739c3ec189f82fca74336c08255f9e20cea1ed378"},
{file = "pydantic_core-2.20.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d6f8c49657f3eb7720ed4c9b26624063da14937fc94d1812f1e04a2204db3e17"},
{file = "pydantic_core-2.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ad1bd2f377f56fec11d5cfd0977c30061cd19f4fa199bf138b200ec0d5e27eeb"},
{file = "pydantic_core-2.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed741183719a5271f97d93bbcc45ed64619fa38068aaa6e90027d1d17e30dc8d"},
{file = "pydantic_core-2.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d82e5ed3a05f2dcb89c6ead2fd0dbff7ac09bc02c1b4028ece2d3a3854d049ce"},
{file = "pydantic_core-2.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2ba34a099576234671f2e4274e5bc6813b22e28778c216d680eabd0db3f7dad"},
{file = "pydantic_core-2.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:879ae6bb08a063b3e1b7ac8c860096d8fd6b48dd9b2690b7f2738b8c835e744b"},
{file = "pydantic_core-2.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b0eefc7633a04c0694340aad91fbfd1986fe1a1e0c63a22793ba40a18fcbdc8"},
{file = "pydantic_core-2.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73deadd6fd8a23e2f40b412b3ac617a112143c8989a4fe265050fd91ba5c0608"},
{file = "pydantic_core-2.20.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:35681445dc85446fb105943d81ae7569aa7e89de80d1ca4ac3229e05c311bdb1"},
{file = "pydantic_core-2.20.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0f6dd3612a3b9f91f2e63924ea18a4476656c6d01843ca20a4c09e00422195af"},
{file = "pydantic_core-2.20.0-cp312-none-win32.whl", hash = "sha256:7e37b6bb6e90c2b8412b06373c6978d9d81e7199a40e24a6ef480e8acdeaf918"},
{file = "pydantic_core-2.20.0-cp312-none-win_amd64.whl", hash = "sha256:7d4df13d1c55e84351fab51383520b84f490740a9f1fec905362aa64590b7a5d"},
{file = "pydantic_core-2.20.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:d43e7ab3b65e4dc35a7612cfff7b0fd62dce5bc11a7cd198310b57f39847fd6c"},
{file = "pydantic_core-2.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b6a24d7b5893392f2b8e3b7a0031ae3b14c6c1942a4615f0d8794fdeeefb08b"},
{file = "pydantic_core-2.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b2f13c3e955a087c3ec86f97661d9f72a76e221281b2262956af381224cfc243"},
{file = "pydantic_core-2.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72432fd6e868c8d0a6849869e004b8bcae233a3c56383954c228316694920b38"},
{file = "pydantic_core-2.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d70a8ff2d4953afb4cbe6211f17268ad29c0b47e73d3372f40e7775904bc28fc"},
{file = "pydantic_core-2.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e49524917b8d3c2f42cd0d2df61178e08e50f5f029f9af1f402b3ee64574392"},
{file = "pydantic_core-2.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4f0f71653b1c1bad0350bc0b4cc057ab87b438ff18fa6392533811ebd01439c"},
{file = "pydantic_core-2.20.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:16197e6f4fdecb9892ed2436e507e44f0a1aa2cff3b9306d1c879ea2f9200997"},
{file = "pydantic_core-2.20.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:763602504bf640b3ded3bba3f8ed8a1cc2fc6a87b8d55c1c5689f428c49c947e"},
{file = "pydantic_core-2.20.0-cp313-none-win32.whl", hash = "sha256:a3f243f318bd9523277fa123b3163f4c005a3e8619d4b867064de02f287a564d"},
{file = "pydantic_core-2.20.0-cp313-none-win_amd64.whl", hash = "sha256:03aceaf6a5adaad3bec2233edc5a7905026553916615888e53154807e404545c"},
{file = "pydantic_core-2.20.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d6f2d8b8da1f03f577243b07bbdd3412eee3d37d1f2fd71d1513cbc76a8c1239"},
{file = "pydantic_core-2.20.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a272785a226869416c6b3c1b7e450506152d3844207331f02f27173562c917e0"},
{file = "pydantic_core-2.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efbb412d55a4ffe73963fed95c09ccb83647ec63b711c4b3752be10a56f0090b"},
{file = "pydantic_core-2.20.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1e4f46189d8740561b43655263a41aac75ff0388febcb2c9ec4f1b60a0ec12f3"},
{file = "pydantic_core-2.20.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d3df115f4a3c8c5e4d5acf067d399c6466d7e604fc9ee9acbe6f0c88a0c3cf"},
{file = "pydantic_core-2.20.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a340d2bdebe819d08f605e9705ed551c3feb97e4fd71822d7147c1e4bdbb9508"},
{file = "pydantic_core-2.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:616b9c2f882393d422ba11b40e72382fe975e806ad693095e9a3b67c59ea6150"},
{file = "pydantic_core-2.20.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25c46bb2ff6084859bbcfdf4f1a63004b98e88b6d04053e8bf324e115398e9e7"},
{file = "pydantic_core-2.20.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:23425eccef8f2c342f78d3a238c824623836c6c874d93c726673dbf7e56c78c0"},
{file = "pydantic_core-2.20.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:52527e8f223ba29608d999d65b204676398009725007c9336651c2ec2d93cffc"},
{file = "pydantic_core-2.20.0-cp38-none-win32.whl", hash = "sha256:1c3c5b7f70dd19a6845292b0775295ea81c61540f68671ae06bfe4421b3222c2"},
{file = "pydantic_core-2.20.0-cp38-none-win_amd64.whl", hash = "sha256:8093473d7b9e908af1cef30025609afc8f5fd2a16ff07f97440fd911421e4432"},
{file = "pydantic_core-2.20.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ee7785938e407418795e4399b2bf5b5f3cf6cf728077a7f26973220d58d885cf"},
{file = "pydantic_core-2.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0e75794883d635071cf6b4ed2a5d7a1e50672ab7a051454c76446ef1ebcdcc91"},
{file = "pydantic_core-2.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:344e352c96e53b4f56b53d24728217c69399b8129c16789f70236083c6ceb2ac"},
{file = "pydantic_core-2.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:978d4123ad1e605daf1ba5e01d4f235bcf7b6e340ef07e7122e8e9cfe3eb61ab"},
{file = "pydantic_core-2.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c05eaf6c863781eb834ab41f5963604ab92855822a2062897958089d1335dad"},
{file = "pydantic_core-2.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc7e43b4a528ffca8c9151b6a2ca34482c2fdc05e6aa24a84b7f475c896fc51d"},
{file = "pydantic_core-2.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:658287a29351166510ebbe0a75c373600cc4367a3d9337b964dada8d38bcc0f4"},
{file = "pydantic_core-2.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1dacf660d6de692fe351e8c806e7efccf09ee5184865893afbe8e59be4920b4a"},
{file = "pydantic_core-2.20.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3e147fc6e27b9a487320d78515c5f29798b539179f7777018cedf51b7749e4f4"},
{file = "pydantic_core-2.20.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c867230d715a3dd1d962c8d9bef0d3168994ed663e21bf748b6e3a529a129aab"},
{file = "pydantic_core-2.20.0-cp39-none-win32.whl", hash = "sha256:22b813baf0dbf612752d8143a2dbf8e33ccb850656b7850e009bad2e101fc377"},
{file = "pydantic_core-2.20.0-cp39-none-win_amd64.whl", hash = "sha256:3a7235b46c1bbe201f09b6f0f5e6c36b16bad3d0532a10493742f91fbdc8035f"},
{file = "pydantic_core-2.20.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cafde15a6f7feaec2f570646e2ffc5b73412295d29134a29067e70740ec6ee20"},
{file = "pydantic_core-2.20.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:2aec8eeea0b08fd6bc2213d8e86811a07491849fd3d79955b62d83e32fa2ad5f"},
{file = "pydantic_core-2.20.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:840200827984f1c4e114008abc2f5ede362d6e11ed0b5931681884dd41852ff1"},
{file = "pydantic_core-2.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ea1d8b7df522e5ced34993c423c3bf3735c53df8b2a15688a2f03a7d678800"},
{file = "pydantic_core-2.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5b8376a867047bf08910573deb95d3c8dfb976eb014ee24f3b5a61ccc5bee1b"},
{file = "pydantic_core-2.20.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d08264b4460326cefacc179fc1411304d5af388a79910832835e6f641512358b"},
{file = "pydantic_core-2.20.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:7a3639011c2e8a9628466f616ed7fb413f30032b891898e10895a0a8b5857d6c"},
{file = "pydantic_core-2.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05e83ce2f7eba29e627dd8066aa6c4c0269b2d4f889c0eba157233a353053cea"},
{file = "pydantic_core-2.20.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:603a843fea76a595c8f661cd4da4d2281dff1e38c4a836a928eac1a2f8fe88e4"},
{file = "pydantic_core-2.20.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac76f30d5d3454f4c28826d891fe74d25121a346c69523c9810ebba43f3b1cec"},
{file = "pydantic_core-2.20.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e3b1d4b1b3f6082849f9b28427ef147a5b46a6132a3dbaf9ca1baa40c88609"},
{file = "pydantic_core-2.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2761f71faed820e25ec62eacba670d1b5c2709bb131a19fcdbfbb09884593e5a"},
{file = "pydantic_core-2.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a0586cddbf4380e24569b8a05f234e7305717cc8323f50114dfb2051fcbce2a3"},
{file = "pydantic_core-2.20.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:b8c46a8cf53e849eea7090f331ae2202cd0f1ceb090b00f5902c423bd1e11805"},
{file = "pydantic_core-2.20.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b4a085bd04af7245e140d1b95619fe8abb445a3d7fdf219b3f80c940853268ef"},
{file = "pydantic_core-2.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:116b326ac82c8b315e7348390f6d30bcfe6e688a7d3f1de50ff7bcc2042a23c2"},
{file = "pydantic_core-2.20.0.tar.gz", hash = "sha256:366be8e64e0cb63d87cf79b4e1765c0703dd6313c729b22e7b9e378db6b96877"},
]
[package.dependencies]

View File

@ -11,7 +11,6 @@ logging.config.fileConfig(logconffile, disable_existing_loggers=True)
log = logging.getLogger(__name__)
app = create_app()
log.debug(f"Running in web mode: {lib.webMode()}")