Much more work
This commit is contained in:
parent
c94f31d5ee
commit
44625ca5f0
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
141
hsman/app/lib.py
141
hsman/app/lib.py
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
||||
</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 %}
|
||||
|
|
|
@ -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">×</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 %}
|
||||
|
|
|
@ -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> </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>
|
||||
|
|
|
@ -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) }}">
|
||||
<span data-toggle="tooltip" data-placement="right"
|
||||
title="{{ rts[0].node.ipAddresses | join('\n') }}">
|
||||
{{ rts[0].node.givenName}}
|
||||
</span>
|
||||
</a>
|
||||
</strong>
|
||||
</span>
|
||||
<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>
|
||||
</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> </span>
|
||||
</div>
|
||||
<div class="col col-2">
|
||||
{% if not rt.enabled %}
|
||||
<s>
|
||||
<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}}
|
||||
</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 %}
|
||||
<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>
|
||||
</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}}
|
||||
|
|
|
@ -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>
|
||||
{% for node in userNodeList %}
|
||||
<div class="row data">
|
||||
<div class="col col-3 float-left">
|
||||
<a href="{{ url_for('main.node', nodeId=node.id) }}" class="plain">
|
||||
{{ node.givenName }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col col-3">
|
||||
<span data-toggle="tooltip" data-placement="right" title="{{ node.lastSeen | fmt_datetime }}">
|
||||
{{node.lastSeen | htime_dt }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col col-3">
|
||||
{{node.online | fancyBool | safe}}
|
||||
</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 %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('main.node', nodeId=node.id) }}" class="plain">
|
||||
{{ node.givenName }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<span data-toggle="tooltip" data-placement="right" title="{{ node.lastSeen | fmt_datetime }}">
|
||||
{{node.lastSeen | htime_dt }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{{node.online | fancyBool | safe}}
|
||||
</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] }}…</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 %}
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
<th>name</th>
|
||||
<th>registered on</th>
|
||||
<th>online</th>
|
||||
<th> </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>
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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()}")
|
||||
|
|
Loading…
Reference in New Issue