feat: Add new updated version
All checks were successful
Build Docker / BuildImage (push) Successful in 1m0s
All checks were successful
Build Docker / BuildImage (push) Successful in 1m0s
This commit is contained in:
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.13
|
||||
69
main.py
69
main.py
@@ -1,14 +1,10 @@
|
||||
import time
|
||||
import sys
|
||||
import signal
|
||||
import threading
|
||||
import concurrent.futures
|
||||
from flask import Flask
|
||||
from server import app, node_check_executor
|
||||
import server
|
||||
from gunicorn.app.base import BaseApplication
|
||||
import os
|
||||
import dotenv
|
||||
import schedule
|
||||
|
||||
|
||||
class GunicornApp(BaseApplication):
|
||||
@@ -26,28 +22,25 @@ class GunicornApp(BaseApplication):
|
||||
return self.application
|
||||
|
||||
|
||||
def check():
|
||||
print('Checking nodes...', flush=True)
|
||||
server.check_nodes()
|
||||
|
||||
|
||||
def run_scheduler(stop_event):
|
||||
schedule.every(5).minutes.do(check)
|
||||
while not stop_event.is_set():
|
||||
schedule.run_pending()
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def run_gunicorn():
|
||||
workers = os.getenv('WORKERS', 1)
|
||||
threads = os.getenv('THREADS', 2)
|
||||
|
||||
try:
|
||||
workers = int(workers)
|
||||
except (ValueError, TypeError):
|
||||
workers = 1
|
||||
|
||||
try:
|
||||
threads = int(threads)
|
||||
except (ValueError, TypeError):
|
||||
threads = 2
|
||||
|
||||
options = {
|
||||
'bind': '0.0.0.0:5000',
|
||||
'workers': workers,
|
||||
'threads': threads,
|
||||
'timeout': 120,
|
||||
}
|
||||
|
||||
gunicorn_app = GunicornApp(server.app, options)
|
||||
@@ -57,39 +50,41 @@ def run_gunicorn():
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
print("Shutting down gracefully...", flush=True)
|
||||
stop_event.set()
|
||||
|
||||
# Shutdown the node check executor
|
||||
# Shutdown the scheduler
|
||||
if server.scheduler.running:
|
||||
print("Stopping scheduler...", flush=True)
|
||||
server.scheduler.shutdown()
|
||||
|
||||
# Shutdown the node check executors
|
||||
print("Shutting down thread pools...", flush=True)
|
||||
node_check_executor.shutdown(wait=False)
|
||||
server.node_check_executor.shutdown(wait=False)
|
||||
server.sub_check_executor.shutdown(wait=False)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
dotenv.load_dotenv()
|
||||
|
||||
stop_event = threading.Event()
|
||||
# Register signal handlers
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
# Start the scheduler
|
||||
scheduler_future = executor.submit(run_scheduler, stop_event)
|
||||
# Start the scheduler from server.py
|
||||
# This ensures we use the robust APScheduler defined there instead of the custom loop
|
||||
print("Starting background scheduler...", flush=True)
|
||||
with server.app.app_context():
|
||||
server.start_scheduler()
|
||||
|
||||
# Run an immediate check in a background thread so we don't block startup
|
||||
startup_thread = threading.Thread(target=server.scheduled_node_check)
|
||||
startup_thread.daemon = True
|
||||
startup_thread.start()
|
||||
|
||||
try:
|
||||
# Run the Gunicorn server
|
||||
run_gunicorn()
|
||||
except KeyboardInterrupt:
|
||||
print("Shutting down server...", flush=True)
|
||||
finally:
|
||||
stop_event.set()
|
||||
scheduler_future.cancel()
|
||||
|
||||
# Make sure to shut down node check executor
|
||||
node_check_executor.shutdown(wait=False)
|
||||
|
||||
try:
|
||||
scheduler_future.result(timeout=5)
|
||||
except concurrent.futures.CancelledError:
|
||||
print("Scheduler stopped.")
|
||||
except Exception as e:
|
||||
print(f"Scheduler did not stop cleanly: {e}")
|
||||
signal_handler(signal.SIGINT, None)
|
||||
|
||||
24
pyproject.toml
Normal file
24
pyproject.toml
Normal file
@@ -0,0 +1,24 @@
|
||||
[project]
|
||||
name = "hnsdoh-status"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"apscheduler>=3.9.1",
|
||||
"brotli>=1.2.0",
|
||||
"dnslib>=0.9.26",
|
||||
"dnspython>=2.8.0",
|
||||
"flask>=3.1.2",
|
||||
"flask-caching>=2.3.1",
|
||||
"gunicorn>=23.0.0",
|
||||
"python-dateutil>=2.9.0.post0",
|
||||
"python-dotenv>=1.2.1",
|
||||
"requests>=2.32.5",
|
||||
"schedule>=1.2.2",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"ruff>=0.14.5",
|
||||
]
|
||||
227
server.py
227
server.py
@@ -1,10 +1,9 @@
|
||||
from collections import defaultdict
|
||||
from functools import cache, wraps
|
||||
from functools import wraps
|
||||
import json
|
||||
from flask import (
|
||||
Flask,
|
||||
make_response,
|
||||
redirect,
|
||||
request,
|
||||
jsonify,
|
||||
render_template,
|
||||
@@ -12,7 +11,6 @@ from flask import (
|
||||
send_file,
|
||||
)
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
import dns.resolver
|
||||
import dns.message
|
||||
@@ -21,25 +19,23 @@ import dns.name
|
||||
import dns.rdatatype
|
||||
import ssl
|
||||
import dnslib
|
||||
import dnslib.dns
|
||||
import socket
|
||||
from datetime import datetime
|
||||
from dateutil import relativedelta
|
||||
import dotenv
|
||||
import time
|
||||
import logging
|
||||
import signal
|
||||
import sys
|
||||
import signal
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
from apscheduler.events import EVENT_JOB_ERROR, EVENT_JOB_EXECUTED
|
||||
from flask_caching import Cache
|
||||
import functools
|
||||
import io
|
||||
import brotli
|
||||
from io import BytesIO
|
||||
import concurrent.futures
|
||||
from threading import Lock
|
||||
import gc
|
||||
|
||||
# Set up logging BEFORE attempting imports that might fail
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
@@ -48,9 +44,13 @@ logger = logging.getLogger(__name__)
|
||||
# Set up ThreadPoolExecutor for parallel node checking
|
||||
# Use a reasonable number of workers based on CPU cores
|
||||
node_check_executor = concurrent.futures.ThreadPoolExecutor(
|
||||
max_workers=min(32, os.cpu_count() * 4) # Max 32 workers or 4x CPU cores
|
||||
max_workers=min(32, (os.cpu_count() or 1) * 4) # Max 32 workers or 4x CPU cores
|
||||
)
|
||||
|
||||
# Shared executor for sub-checks (DNS, DoH, DoT, Certs) to prevent thread churn
|
||||
# Increased to 200 to accommodate all sub-tasks when max_workers (32) are active in the main pool
|
||||
sub_check_executor = concurrent.futures.ThreadPoolExecutor(max_workers=200)
|
||||
|
||||
# Create a lock for thread safety when updating cache
|
||||
cache_lock = Lock()
|
||||
|
||||
@@ -71,17 +71,17 @@ cache = Cache(app)
|
||||
scheduler = BackgroundScheduler(daemon=True, job_defaults={'coalesce': True, 'max_instances': 1})
|
||||
|
||||
node_names = {
|
||||
"18.169.98.42": "Easy HNS",
|
||||
"172.233.46.92": "Nathan.Woodburn/",
|
||||
"194.50.5.27": "Nathan.Woodburn/",
|
||||
"18.169.98.42": "Easy HNS",
|
||||
"172.233.46.92": "Marioo",
|
||||
"139.177.195.185": "HNSCanada",
|
||||
"172.105.120.203": "Nathan.Woodburn/",
|
||||
"172.105.120.203": "NameGuardian/",
|
||||
"173.233.72.88": "Zorro"
|
||||
}
|
||||
node_locations = {
|
||||
"194.50.5.27": "Australia",
|
||||
"18.169.98.42": "England",
|
||||
"172.233.46.92": "Netherlands",
|
||||
"194.50.5.27": "Australia",
|
||||
"139.177.195.185": "Canada",
|
||||
"172.105.120.203": "Singapore",
|
||||
"173.233.72.88": "United States"
|
||||
@@ -110,7 +110,7 @@ else:
|
||||
sent_notifications = json.load(file)
|
||||
|
||||
if (os.getenv("NODES")):
|
||||
manual_nodes = os.getenv("NODES").split(",")
|
||||
manual_nodes = os.getenv("NODES","").split(",")
|
||||
|
||||
print(f"Log directory: {log_dir}", flush=True)
|
||||
|
||||
@@ -179,10 +179,14 @@ def faviconPNG():
|
||||
|
||||
@app.route("/.well-known/<path:path>")
|
||||
def wellknown(path):
|
||||
req = requests.get(f"https://nathan.woodburn.au/.well-known/{path}")
|
||||
try:
|
||||
req = requests.get(f"https://nathan.woodburn.au/.well-known/{path}", timeout=10)
|
||||
return make_response(
|
||||
req.content, 200, {"Content-Type": req.headers["Content-Type"]}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching well-known path {path}: {e}")
|
||||
return make_response("Error fetching resource", 500)
|
||||
|
||||
|
||||
# endregion
|
||||
@@ -268,6 +272,19 @@ def build_dns_query(domain: str, qtype: str = "A"):
|
||||
return q.pack()
|
||||
|
||||
|
||||
def send_down_notification(node):
|
||||
"""
|
||||
Send a notification that a node is down.
|
||||
This is a stub implementation to prevent crashes if the function is missing.
|
||||
"""
|
||||
try:
|
||||
# Implement your notification logic here (e.g. email, discord, slack)
|
||||
# For now, we just log it to avoid crashing
|
||||
logger.warning(f"Node DOWN: {node.get('name')} ({node.get('ip')})")
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending notification: {e}")
|
||||
|
||||
|
||||
@retry(max_attempts=3, delay_seconds=2)
|
||||
def check_doh(ip: str) -> dict:
|
||||
status = False
|
||||
@@ -351,12 +368,12 @@ def check_doh(ip: str) -> dict:
|
||||
if ssock:
|
||||
try:
|
||||
ssock.close()
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
if sock and sock != ssock:
|
||||
try:
|
||||
sock.close()
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {"status": status, "server": server_name}
|
||||
@@ -435,12 +452,12 @@ def verify_cert(ip: str, port: int) -> dict:
|
||||
if ssock:
|
||||
try:
|
||||
ssock.close()
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
if sock and sock != ssock:
|
||||
try:
|
||||
sock.close()
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {"valid": valid, "expires": expires, "expiry_date": expiry_date_str}
|
||||
@@ -482,12 +499,19 @@ def format_last_check(last_log: datetime) -> str:
|
||||
return "less than a minute ago"
|
||||
|
||||
|
||||
def check_nodes() -> list:
|
||||
def check_nodes(force=False) -> list:
|
||||
global nodes, _node_status_cache, _node_status_cache_time
|
||||
if last_log > datetime.now() - relativedelta.relativedelta(minutes=1):
|
||||
|
||||
# Skip check if done recently, unless forced
|
||||
if not force and last_log > datetime.now() - relativedelta.relativedelta(minutes=1):
|
||||
# Load the last log
|
||||
try:
|
||||
with open(f"{log_dir}/node_status.json", "r") as file:
|
||||
data = json.load(file)
|
||||
except (json.JSONDecodeError, FileNotFoundError) as e:
|
||||
logger.error(f"Error reading node_status.json: {e}")
|
||||
data = []
|
||||
|
||||
newest = {
|
||||
"date": datetime.now() - relativedelta.relativedelta(years=1),
|
||||
"nodes": [],
|
||||
@@ -542,6 +566,7 @@ def check_nodes() -> list:
|
||||
|
||||
# Send notifications if any nodes are down
|
||||
for node in node_status:
|
||||
try:
|
||||
if (
|
||||
not node["plain_dns"]
|
||||
or not node["doh"]
|
||||
@@ -567,6 +592,8 @@ def check_nodes() -> list:
|
||||
send_down_notification(node)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing certificate expiry for {node['ip']}: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in notification loop for {node.get('ip')}: {e}")
|
||||
|
||||
return node_status
|
||||
|
||||
@@ -582,13 +609,12 @@ def check_single_node(ip):
|
||||
cert_result = {"valid": False, "expires": "ERROR", "expiry_date": "ERROR"}
|
||||
cert_853_result = {"valid": False, "expires": "ERROR", "expiry_date": "ERROR"}
|
||||
|
||||
# Use timeout to limit time spent on each check
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
||||
future_plain_dns = executor.submit(check_plain_dns, ip)
|
||||
future_doh = executor.submit(check_doh, ip)
|
||||
future_dot = executor.submit(check_dot, ip)
|
||||
future_cert = executor.submit(verify_cert, ip, 443)
|
||||
future_cert_853 = executor.submit(verify_cert, ip, 853)
|
||||
# Use shared executor to avoid creating/destroying thread pools constantly
|
||||
future_plain_dns = sub_check_executor.submit(check_plain_dns, ip)
|
||||
future_doh = sub_check_executor.submit(check_doh, ip)
|
||||
future_dot = sub_check_executor.submit(check_dot, ip)
|
||||
future_cert = sub_check_executor.submit(verify_cert, ip, 443)
|
||||
future_cert_853 = sub_check_executor.submit(verify_cert, ip, 853)
|
||||
|
||||
# Collect results with timeout
|
||||
try:
|
||||
@@ -658,8 +684,12 @@ def log_status(node_status: list):
|
||||
# Check if the file exists
|
||||
filename = f"{log_dir}/node_status.json"
|
||||
if os.path.isfile(filename):
|
||||
try:
|
||||
with open(filename, "r") as file:
|
||||
data = json.load(file)
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f"Corrupted JSON in {filename}, starting fresh")
|
||||
data = []
|
||||
else:
|
||||
data = []
|
||||
|
||||
@@ -758,13 +788,13 @@ def summarize_history(history: list) -> dict:
|
||||
# Update counts and last downtime
|
||||
for key in ["plain_dns", "doh", "dot"]:
|
||||
status = node.get(key, "up")
|
||||
if status == False:
|
||||
if not status:
|
||||
total_counts[ip][key]["down"] += 1
|
||||
total_counts[ip][key]["total"] += 1
|
||||
|
||||
# Update last downtime for each key
|
||||
for key in ["plain_dns", "doh", "dot"]:
|
||||
if node.get(key) == False:
|
||||
if not node.get(key):
|
||||
# Check if the last downtime is more recent
|
||||
if nodes_status[ip][key]["last_down"] == "never":
|
||||
nodes_status[ip][key]["last_down"] = date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
@@ -907,7 +937,7 @@ def api_history():
|
||||
if "days" in request.args:
|
||||
try:
|
||||
history_days = int(request.args["days"])
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
history = get_history(history_days)
|
||||
history_summary = summarize_history(history)
|
||||
@@ -929,12 +959,12 @@ def api_all():
|
||||
if "history" in request.args:
|
||||
try:
|
||||
history_days = int(request.args["history"])
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
if "days" in request.args:
|
||||
try:
|
||||
history_days = int(request.args["days"])
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
history = get_history(history_days)
|
||||
return jsonify(history)
|
||||
@@ -942,7 +972,7 @@ def api_all():
|
||||
|
||||
@app.route("/api/refresh")
|
||||
def api_refresh():
|
||||
node_status = check_nodes()
|
||||
node_status = check_nodes(force=True)
|
||||
return jsonify(node_status)
|
||||
|
||||
@app.route("/api/latest")
|
||||
@@ -1161,7 +1191,7 @@ def index():
|
||||
if "history" in request.args:
|
||||
try:
|
||||
history_days = int(request.args["history"])
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
history = get_history(history_days)
|
||||
history_summary = summarize_history(history)
|
||||
@@ -1183,18 +1213,8 @@ def index():
|
||||
datetime.strptime(history_summary["overall"][key]["last_down"], "%Y-%m-%d %H:%M:%S")
|
||||
)
|
||||
|
||||
history_summary["nodes"] = convert_nodes_to_dict(history_summary["nodes"])
|
||||
|
||||
last_check = format_last_check(last_log)
|
||||
|
||||
# Replace true/false with up/down
|
||||
for node in node_status:
|
||||
for key in ["plain_dns", "doh", "dot"]:
|
||||
if node[key]:
|
||||
node[key] = "Up"
|
||||
else:
|
||||
node[key] = "Down"
|
||||
|
||||
return render_template(
|
||||
"index.html",
|
||||
nodes=node_status,
|
||||
@@ -1259,11 +1279,16 @@ def scheduled_node_check():
|
||||
nodes = [] # Reset node list to force refresh
|
||||
|
||||
# Run the check (which now uses ThreadPoolExecutor)
|
||||
node_status = check_nodes()
|
||||
# Force the check to bypass the 1-minute debounce
|
||||
node_status = check_nodes(force=True)
|
||||
# Clear relevant caches
|
||||
cache.delete_memoized(api_nodes)
|
||||
cache.delete_memoized(api_errors)
|
||||
cache.delete_memoized(index)
|
||||
|
||||
# Force garbage collection to prevent memory leaks over long periods
|
||||
gc.collect()
|
||||
|
||||
logger.info("Completed scheduled node check and updated caches")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in scheduled node check: {e}")
|
||||
@@ -1307,14 +1332,29 @@ def signal_handler(sig, frame):
|
||||
if scheduler.running:
|
||||
scheduler.shutdown()
|
||||
logger.info("Scheduler shut down")
|
||||
|
||||
# Shutdown thread pools
|
||||
logger.info("Shutting down thread pools...")
|
||||
node_check_executor.shutdown(wait=False)
|
||||
sub_check_executor.shutdown(wait=False)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
# Register the signal handlers
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
# Initialize the scheduler when the app starts without relying on @before_first_request
|
||||
# which is deprecated in newer Flask versions
|
||||
with app.app_context():
|
||||
# Only start scheduler if we are the main process or not in debug mode reloader
|
||||
if os.environ.get("WERKZEUG_RUN_MAIN") == "true" or __name__ == "__main__":
|
||||
with app.app_context():
|
||||
start_scheduler()
|
||||
# Run an immediate check
|
||||
scheduled_node_check()
|
||||
# Run an immediate check in a separate thread to not block startup
|
||||
import threading
|
||||
startup_thread = threading.Thread(target=scheduled_node_check)
|
||||
startup_thread.daemon = True
|
||||
startup_thread.start()
|
||||
|
||||
# Custom Brotli compression for responses
|
||||
@app.after_request
|
||||
@@ -1362,7 +1402,7 @@ def add_compression(response):
|
||||
|
||||
def check_nodes_from_log():
|
||||
"""Read the most recent node status from the log file."""
|
||||
global _node_status_cache, _node_status_cache_time
|
||||
global _node_status_cache, _node_status_cache_time, last_log
|
||||
|
||||
# Return cached result if it's less than 2 minutes old (increased from 60s)
|
||||
with cache_lock:
|
||||
@@ -1387,13 +1427,17 @@ def check_nodes_from_log():
|
||||
newest = entry
|
||||
newest["date"] = entry_date
|
||||
|
||||
# Update global last_log from the file data so the UI shows the correct time
|
||||
if isinstance(newest["date"], datetime) and newest["date"] > last_log:
|
||||
last_log = newest["date"]
|
||||
|
||||
# Update the cache
|
||||
with cache_lock:
|
||||
_node_status_cache = newest["nodes"]
|
||||
_node_status_cache_time = datetime.now()
|
||||
|
||||
return newest["nodes"]
|
||||
except Exception as e:
|
||||
except (json.JSONDecodeError, FileNotFoundError, Exception) as e:
|
||||
logger.error(f"Error reading node status from log: {e}")
|
||||
# If we can't read from the log, run a fresh check
|
||||
return check_nodes()
|
||||
@@ -1434,86 +1478,7 @@ def quick_status():
|
||||
logger.error(f"Error getting quick status: {e}")
|
||||
return jsonify({"status": "error", "message": str(e)})
|
||||
|
||||
# Optimize check_single_node with shorter timeouts
|
||||
def check_single_node(ip):
|
||||
"""Check a single node and return its status."""
|
||||
logger.info(f"Checking node {ip}")
|
||||
try:
|
||||
# Add timeout handling for individual checks
|
||||
plain_dns_result = False
|
||||
doh_result = {"status": False, "server": []}
|
||||
dot_result = False
|
||||
cert_result = {"valid": False, "expires": "ERROR", "expiry_date": "ERROR"}
|
||||
cert_853_result = {"valid": False, "expires": "ERROR", "expiry_date": "ERROR"}
|
||||
|
||||
# Use timeout to limit time spent on each check
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
||||
future_plain_dns = executor.submit(check_plain_dns, ip)
|
||||
future_doh = executor.submit(check_doh, ip)
|
||||
future_dot = executor.submit(check_dot, ip)
|
||||
future_cert = executor.submit(verify_cert, ip, 443)
|
||||
future_cert_853 = executor.submit(verify_cert, ip, 853)
|
||||
|
||||
# Collect results with timeout
|
||||
try:
|
||||
plain_dns_result = future_plain_dns.result(timeout=5)
|
||||
except (concurrent.futures.TimeoutError, Exception) as e:
|
||||
logger.warning(f"Plain DNS check timed out for {ip}: {str(e)}")
|
||||
|
||||
try:
|
||||
doh_result = future_doh.result(timeout=5)
|
||||
except (concurrent.futures.TimeoutError, Exception) as e:
|
||||
logger.warning(f"DoH check timed out for {ip}: {str(e)}")
|
||||
|
||||
try:
|
||||
dot_result = future_dot.result(timeout=5)
|
||||
except (concurrent.futures.TimeoutError, Exception) as e:
|
||||
logger.warning(f"DoT check timed out for {ip}: {str(e)}")
|
||||
|
||||
try:
|
||||
cert_result = future_cert.result(timeout=5)
|
||||
except (concurrent.futures.TimeoutError, Exception) as e:
|
||||
logger.warning(f"Cert check timed out for {ip}: {str(e)}")
|
||||
|
||||
try:
|
||||
cert_853_result = future_cert_853.result(timeout=5)
|
||||
except (concurrent.futures.TimeoutError, Exception) as e:
|
||||
logger.warning(f"Cert 853 check timed out for {ip}: {str(e)}")
|
||||
|
||||
node_status = {
|
||||
"ip": ip,
|
||||
"name": node_names[ip] if ip in node_names else ip,
|
||||
"location": (
|
||||
node_locations[ip] if ip in node_locations else "Unknown"
|
||||
),
|
||||
"plain_dns": plain_dns_result,
|
||||
"doh": doh_result["status"],
|
||||
"doh_server": doh_result["server"],
|
||||
"dot": dot_result,
|
||||
"cert": cert_result,
|
||||
"cert_853": cert_853_result,
|
||||
}
|
||||
logger.info(f"Node {ip} check complete")
|
||||
return node_status
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking node {ip}: {e}")
|
||||
# Add a failed entry for this node to ensure it's still included
|
||||
return {
|
||||
"ip": ip,
|
||||
"name": node_names[ip] if ip in node_names else ip,
|
||||
"location": (
|
||||
node_locations[ip] if ip in node_locations else "Unknown"
|
||||
),
|
||||
"plain_dns": False,
|
||||
"doh": False,
|
||||
"doh_server": [],
|
||||
"dot": False,
|
||||
"cert": {"valid": False, "expires": "ERROR", "expiry_date": "ERROR"},
|
||||
"cert_853": {"valid": False, "expires": "ERROR", "expiry_date": "ERROR"},
|
||||
}
|
||||
|
||||
# Run the app with threading enabled
|
||||
if __name__ == "__main__":
|
||||
# The scheduler is already started in the app context above
|
||||
# Run the Flask app with threading for better concurrency
|
||||
app.run(debug=True, port=5000, host="0.0.0.0", threaded=True)
|
||||
# Disable debug mode for production reliability to prevent double execution of scheduler
|
||||
app.run(debug=False, port=5000, host="0.0.0.0", threaded=True)
|
||||
|
||||
@@ -4,17 +4,63 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Status | HNS DoH</title>
|
||||
<link rel="icon" href="/assets/img/HNS.png" type="image/png">
|
||||
<link rel="stylesheet" href="/assets/css/404.css">
|
||||
<title>Page Not Found - HNSDoH Status</title>
|
||||
<link rel="stylesheet" href="/assets/style.css">
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
<style>
|
||||
.error-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
text-align: center;
|
||||
}
|
||||
.error-code {
|
||||
font-size: 6rem;
|
||||
font-weight: 800;
|
||||
color: var(--bg-card-hover);
|
||||
line-height: 1;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.error-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.error-msg {
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 2rem;
|
||||
max-width: 400px;
|
||||
}
|
||||
.btn {
|
||||
display: inline-block;
|
||||
background-color: var(--accent);
|
||||
color: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 600;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.btn:hover {
|
||||
opacity: 0.9;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="spacer"></div>
|
||||
<div class="centre">
|
||||
<h1>404 | Page not found</h1>
|
||||
<p>Sorry, the page you are looking for does not exist.</p>
|
||||
<p><a href="/">Go back to the homepage</a></p>
|
||||
<div class="container">
|
||||
<div class="error-container">
|
||||
<div class="error-code">404</div>
|
||||
<div class="error-title">Page Not Found</div>
|
||||
<div class="error-msg">The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.</div>
|
||||
<a href="/" class="btn">Return Home</a>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>Powered by <a href="https://nathan.woodburn.au">Nathan.Woodburn/</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -4,42 +4,101 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>API | HNS DoH</title>
|
||||
<link rel="icon" href="/assets/img/HNS.png" type="image/png">
|
||||
<link rel="stylesheet" href="/assets/css/api.css">
|
||||
<title>API Documentation - HNSDoH Status</title>
|
||||
<link rel="stylesheet" href="/assets/style.css">
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
<style>
|
||||
.endpoint-card {
|
||||
background-color: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.endpoint-method {
|
||||
display: inline-block;
|
||||
background-color: rgba(59, 130, 246, 0.15);
|
||||
color: var(--accent);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-weight: 700;
|
||||
font-size: 0.75rem;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
.endpoint-route {
|
||||
font-family: monospace;
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-main);
|
||||
}
|
||||
.endpoint-desc {
|
||||
margin-top: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.param-table {
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.param-table th { background-color: rgba(0,0,0,0.2); }
|
||||
.param-table td, .param-table th { padding: 0.75rem; border-bottom: 1px solid var(--border); }
|
||||
.param-table tr:last-child td { border-bottom: none; }
|
||||
.back-link { display: inline-block; margin-bottom: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="spacer"></div>
|
||||
<div class="centre">
|
||||
{% if endpoints %}
|
||||
<h1 style="font-size: xx-large;">API Info</h1>
|
||||
<p>Available endpoints:</p>
|
||||
<ul style="width: fit-content;margin: auto;">
|
||||
{% for endpoint in endpoints %}
|
||||
<li class="top">
|
||||
<strong>{{ endpoint.route }}</strong> - {{ endpoint.description }}
|
||||
{% if endpoint.parameters %}
|
||||
<ul>
|
||||
<li><em>Parameters:</em></li>
|
||||
{% for param in endpoint.parameters %}
|
||||
<li>
|
||||
<strong>{{ param.name }}:</strong>
|
||||
({{ param.type }}) - {{ param.description }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="container">
|
||||
<header>
|
||||
<div>
|
||||
<h1>API Documentation</h1>
|
||||
<div class="meta">Programmatic access to status data</div>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/" class="back-link">← Back to Dashboard</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% else %}
|
||||
<h1>404 | Page not found</h1>
|
||||
<p>Sorry, the page you are looking for does not exist.</p>
|
||||
<p><a href="/">Go back to the homepage</a></p>
|
||||
<div class="endpoints-list">
|
||||
{% for endpoint in endpoints %}
|
||||
<div class="endpoint-card">
|
||||
<div>
|
||||
<span class="endpoint-method">GET</span>
|
||||
<a href="{{ endpoint.route }}" class="endpoint-route">{{ endpoint.route }}</a>
|
||||
</div>
|
||||
<div class="endpoint-desc">{{ endpoint.description }}</div>
|
||||
|
||||
{% if endpoint.parameters %}
|
||||
<div class="param-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Parameter</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for param in endpoint.parameters %}
|
||||
<tr>
|
||||
<td><code>{{ param.name }}</code></td>
|
||||
<td><code>{{ param.type }}</code></td>
|
||||
<td>{{ param.description }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>Powered by <a href="https://nathan.woodburn.au">Nathan.Woodburn/</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
133
templates/assets/style.css
Normal file
133
templates/assets/style.css
Normal file
@@ -0,0 +1,133 @@
|
||||
:root {
|
||||
--bg-body: #0f172a;
|
||||
--bg-card: #1e293b;
|
||||
--bg-card-hover: #334155;
|
||||
--text-main: #f1f5f9;
|
||||
--text-muted: #94a3b8;
|
||||
--border: #334155;
|
||||
--accent: #3b82f6;
|
||||
--success: #22c55e;
|
||||
--error: #ef4444;
|
||||
--warning: #f59e0b;
|
||||
--shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background-color: var(--bg-body);
|
||||
color: var(--text-main);
|
||||
line-height: 1.5;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
header {
|
||||
margin-bottom: 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
h1 { font-size: 1.5rem; font-weight: 700; }
|
||||
h2 { font-size: 1.25rem; margin-bottom: 1rem; color: var(--text-main); }
|
||||
|
||||
.meta { color: var(--text-muted); font-size: 0.875rem; }
|
||||
|
||||
/* Alerts */
|
||||
.alerts-section { margin-bottom: 2rem; }
|
||||
.alert-box {
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.alert-box.error { background-color: rgba(239, 68, 68, 0.1); border: 1px solid var(--error); color: #fca5a5; }
|
||||
.alert-box.warning { background-color: rgba(245, 158, 11, 0.1); border: 1px solid var(--warning); color: #fcd34d; }
|
||||
|
||||
/* Grid */
|
||||
.node-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--shadow);
|
||||
transition: transform 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: var(--text-muted);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.node-name { font-weight: 600; font-size: 1.125rem; }
|
||||
.node-location { font-size: 0.875rem; color: var(--text-muted); }
|
||||
.node-ip { font-family: monospace; font-size: 0.75rem; color: var(--text-muted); background: rgba(0,0,0,0.2); padding: 2px 6px; border-radius: 4px; }
|
||||
|
||||
.status-list { display: flex; flex-direction: column; gap: 0.75rem; }
|
||||
.status-item { display: flex; justify-content: space-between; align-items: center; font-size: 0.875rem; }
|
||||
|
||||
.badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.badge.up { background-color: rgba(34, 197, 94, 0.15); color: var(--success); }
|
||||
.badge.down { background-color: rgba(239, 68, 68, 0.15); color: var(--error); }
|
||||
|
||||
.cert-info { font-size: 0.75rem; color: var(--text-muted); margin-top: 0.25rem; text-align: right; }
|
||||
|
||||
/* History Table */
|
||||
.table-container {
|
||||
background-color: var(--bg-card);
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid var(--border);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
|
||||
th, td { padding: 1rem; text-align: left; border-bottom: 1px solid var(--border); }
|
||||
th { background-color: rgba(0,0,0,0.2); font-weight: 600; color: var(--text-muted); }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
tr:hover td { background-color: var(--bg-card-hover); }
|
||||
|
||||
.uptime-bar {
|
||||
height: 6px;
|
||||
background-color: var(--bg-body);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
width: 100px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.uptime-fill { height: 100%; background-color: var(--success); }
|
||||
.uptime-fill.warn { background-color: var(--warning); }
|
||||
.uptime-fill.bad { background-color: var(--error); }
|
||||
|
||||
.footer { margin-top: 4rem; text-align: center; color: var(--text-muted); font-size: 0.875rem; }
|
||||
a { color: var(--accent); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
@@ -1,174 +1,128 @@
|
||||
<!DOCTYPE html>
|
||||
<html data-bs-theme="dark" lang="en-au">
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||
<title>Status | HNS DoH</title>
|
||||
<meta name="twitter:image" content="https://status.hnsdoh.com/assets/img/HNS.png">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:description" content="Access Handshake Domains with DNS over HTTPS">
|
||||
<meta property="og:title" content="Status | HNS DoH">
|
||||
<meta name="description" content="Access Handshake Domains with DNS over HTTPS">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:description" content="Access Handshake Domains with DNS over HTTPS">
|
||||
<meta name="twitter:title" content="Status | HNS DoH">
|
||||
<meta property="og:image" content="https://status.hnsdoh.com/assets/img/HNS.png">
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "http://schema.org",
|
||||
"@type": "WebSite",
|
||||
"name": "Status | HNS DoH",
|
||||
"url": "https://status.hnsdoh.com"
|
||||
}
|
||||
</script>
|
||||
<link rel="icon" type="image/png" sizes="670x700" href="assets/img/HNS.png">
|
||||
<link rel="icon" type="image/png" sizes="670x700" href="assets/img/HNSW.png" media="(prefers-color-scheme: dark)">
|
||||
<link rel="icon" type="image/png" sizes="670x700" href="assets/img/HNS.png">
|
||||
<link rel="icon" type="image/png" sizes="670x700" href="assets/img/HNSW.png" media="(prefers-color-scheme: dark)">
|
||||
<link rel="icon" type="image/png" sizes="670x700" href="assets/img/HNS.png">
|
||||
<link rel="icon" type="image/png" sizes="670x700" href="assets/img/HNS.png">
|
||||
<link rel="icon" type="image/png" sizes="670x700" href="assets/img/HNS.png">
|
||||
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="assets/css/bs-theme-overrides.css">
|
||||
<link rel="stylesheet" href="assets/css/Navbar-Right-Links-Dark-icons.css">
|
||||
<link rel="stylesheet" href="assets/css/Team-images.css">
|
||||
<link rel="stylesheet" href="assets/css/index.css">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
|
||||
<script async src="https://umami.woodburn.au/script.js"
|
||||
data-website-id="7e0ed7e4-3858-4124-a574-b57ac05aaad1"></script>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HNSDoH Status</title>
|
||||
<link rel="stylesheet" href="/assets/style.css">
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-md fixed-top bg-dark py-3" data-bs-theme="dark">
|
||||
<div class="container-fluid"><a class="navbar-brand d-flex align-items-center" href="https://welcome.hnsdoh.com"><span
|
||||
class="bs-icon-sm bs-icon-rounded bs-icon-primary d-flex justify-content-center align-items-center me-2 bs-icon"><img
|
||||
src="assets/img/HNSW.png" width="20px"></span><span>HNS DoH</span></a></div>
|
||||
</nav>
|
||||
<div>
|
||||
<h1>HNSDoH Status</h1>
|
||||
<div class="meta">Handshake DNS over HTTPS/TLS</div>
|
||||
</div>
|
||||
<div class="meta">
|
||||
Last checked: {{ last_check }}
|
||||
</div>
|
||||
</header>
|
||||
<div style="margin: 100px;"></div>
|
||||
<section id="intro">
|
||||
<div class="text-center">
|
||||
<h1 class="text-center" style="font-size: 60px;">HNS DoH Status</h1>
|
||||
<div class="errors">
|
||||
<!-- Check if errors is empty -->
|
||||
{% if alerts %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<h4 class="alert-heading">Alert</h4>
|
||||
|
||||
{% if alerts or warnings %}
|
||||
<div class="alerts-section">
|
||||
{% for alert in alerts %}
|
||||
<p>{{ alert }}</p>
|
||||
<div class="alert-box error">{{ alert }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
<div class="warnings">
|
||||
<!-- Check if warnings is empty -->
|
||||
{% if warnings %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<h4 class="alert-heading">Warning</h4>
|
||||
{% for warning in warnings %}
|
||||
<p>{{ warning }}</p>
|
||||
<div class="alert-box warning">{{ warning }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="status"></section>
|
||||
<div class="text-center" style="width: fit-content;margin: auto;max-width: 100%;">
|
||||
<span style="font-size: smaller;margin-bottom: 10px;">Last check: {{last_check}}</span>
|
||||
<div class="spacer"></div>
|
||||
<div class="node" style="display: block;">
|
||||
<div>
|
||||
<h2>Overall Stats</h2>
|
||||
</div>
|
||||
<div class="node-info">
|
||||
<p>Plain DNS: {{history.overall.plain_dns.percentage}}% uptime (last down
|
||||
{{history.overall.plain_dns.last_down}})</p>
|
||||
<p>DNS over HTTPS: {{history.overall.doh.percentage}}% uptime (last down
|
||||
{{history.overall.doh.last_down}})</p>
|
||||
<p>DNS over TLS: {{history.overall.dot.percentage}}% uptime (last down
|
||||
{{history.overall.dot.last_down}})</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<h2>Node Status</h2>
|
||||
<div class="node-grid">
|
||||
{% for node in nodes %}
|
||||
<div class="node {{node.class}}">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h2>{{node.location}}</h2>
|
||||
</div>
|
||||
<div class="node-info">
|
||||
<h5>Current Status</h5>
|
||||
<p>Plain DNS: {{node.plain_dns}}</p>
|
||||
<p>DNS over HTTPS: {{node.doh}}</p>
|
||||
<p>DNS over TLS: {{node.dot}}</p>
|
||||
<p>Certificate: {% if node.cert.valid %} Valid {% else %} Invalid {% endif %} (expires
|
||||
{{node.cert.expires}})</p>
|
||||
</div>
|
||||
<div class="node-info">
|
||||
<h5>Stats</h5>
|
||||
<p>Plain DNS: {{history.nodes[node.ip].plain_dns.percentage}}% uptime (last down
|
||||
{{history.nodes[node.ip].plain_dns.last_down}})</p>
|
||||
<p>DNS over HTTPS: {{history.nodes[node.ip].doh.percentage}}% uptime (last down
|
||||
{{history.nodes[node.ip].doh.last_down}})</p>
|
||||
<p>DNS over TLS: {{history.nodes[node.ip].dot.percentage}}% uptime (last down
|
||||
{{history.nodes[node.ip].dot.last_down}})</p>
|
||||
</div>
|
||||
<div class="node-info">
|
||||
<p style="font-weight: bold;">{{node.name}}: {{node.ip}}</p>
|
||||
<div class="node-name">{{ node.name }}</div>
|
||||
<div class="node-location">{{ node.location }}</div>
|
||||
</div>
|
||||
<div class="node-ip">{{ node.ip }}</div>
|
||||
</div>
|
||||
|
||||
<div class="status-list">
|
||||
<div class="status-item">
|
||||
<span>Plain DNS</span>
|
||||
<span class="badge {{ 'up' if node.plain_dns else 'down' }}">
|
||||
{{ 'UP' if node.plain_dns else 'DOWN' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="status-item">
|
||||
<span>DoH (443)</span>
|
||||
<span class="badge {{ 'up' if node.doh else 'down' }}">
|
||||
{{ 'UP' if node.doh else 'DOWN' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="status-item">
|
||||
<span>DoT (853)</span>
|
||||
<span class="badge {{ 'up' if node.dot else 'down' }}">
|
||||
{{ 'UP' if node.dot else 'DOWN' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="status-item">
|
||||
<span>Certificate</span>
|
||||
<div style="text-align: right;">
|
||||
<span class="badge {{ 'up' if node.cert.valid else 'down' }}">
|
||||
{{ 'VALID' if node.cert.valid else 'INVALID' }}
|
||||
</span>
|
||||
{% if node.cert.valid %}
|
||||
<div class="cert-info">Exp: {{ node.cert.expires }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="setup"
|
||||
style="min-height: 400px;padding-top: 10vh;text-align: center;margin-right: 10%;margin-left: 10%;"
|
||||
data-bs-target="#navcol-5" data-bs-smooth-scroll="true">
|
||||
<h3 class="display-1">Setup</h3>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<div>
|
||||
<h5 class="display-5">DNS over HTTPS</h5>
|
||||
<p>DNS over HTTPS is supported by most browsers. To add HNSDoH to your revolvers add this URL to
|
||||
your Secure DNS setting<br><code>https://hnsdoh.com/dns-query</code></p>
|
||||
<h2>30-Day History</h2>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Node</th>
|
||||
<th style="text-align: center;">Plain DNS</th>
|
||||
<th style="text-align: center;">DoH</th>
|
||||
<th style="text-align: center;">DoT</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for node in history.nodes %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="node-name">{{ node.name }}</div>
|
||||
<div class="node-location">{{ node.location }}</div>
|
||||
</td>
|
||||
{% for type in ['plain_dns', 'doh', 'dot'] %}
|
||||
<td style="text-align: center;">
|
||||
<div>{{ node[type].percentage }}%</div>
|
||||
<div class="uptime-bar" style="margin: 4px auto 0;">
|
||||
<div class="uptime-fill {% if node[type].percentage < 90 %}warn{% endif %} {% if node[type].percentage < 70 %}bad{% endif %}"
|
||||
style="width: {{ node[type].percentage }}%"></div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<div>
|
||||
<h5 class="display-5">DNS over TLS</h5>
|
||||
<p>DNS over TLS is the best option for mobile phones. Simply set Private DNS to the
|
||||
hostname <br><code>hnsdoh.com</code></p>
|
||||
{% if node[type].last_down != 'never' %}
|
||||
<div class="cert-info" style="text-align: center;">Last outage: {{ node[type].last_down }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<div>
|
||||
<h5 class="display-5">Plain DNS</h5>
|
||||
<p>As a last resort you can use any of plain DNS below (best to chose 2 IPs from different
|
||||
people)<br><br>- 194.50.5.27 (powered by Nathan.Woodburn/)<br>- 139.177.195.185 (powered by
|
||||
HNS Canada)<br>- 172.233.46.92 (powered by Nathan.Woodburn/)<br>- 172.105.120.203 (powered
|
||||
by Nathan.Woodburn/)<br>- 18.169.98.42 (powered by Easy HNS)<br><br>Alternative Providers (Not
|
||||
running the HNSDoH software configuration)<br><br>- 194.50.5.26 (powered by
|
||||
Nathan.Woodburn/)<br>- 194.50.5.28 (powered by Nathan.Woodburn/)<br>- 139.144.68.241
|
||||
(powered by HNS DNS)<br>- 139.144.68.242 (powered by HNS DNS)<br>- 2a01:7e01:e002:c300::
|
||||
(powered by HNS DNS)<br>- 2a01:7e01:e002:c500:: (powered by HNS DNS)</p>
|
||||
|
||||
<div class="footer">
|
||||
<p>Powered by <a href="https://nathan.woodburn.au">Nathan.Woodburn/</a></p>
|
||||
<p><a href="/api">API Access</a></p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<footer class="text-center bg-dark">
|
||||
<div class="container text-white py-4 py-lg-5">
|
||||
<p class="text-muted mb-0">Copyright © 2024 HNSDoH</p>
|
||||
</div>
|
||||
</footer>
|
||||
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
|
||||
<script src="assets/js/bs-init.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
446
uv.lock
generated
Normal file
446
uv.lock
generated
Normal file
@@ -0,0 +1,446 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "apscheduler"
|
||||
version = "3.11.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "tzlocal" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/81/192db4f8471de5bc1f0d098783decffb1e6e69c4f8b4bc6711094691950b/apscheduler-3.11.1.tar.gz", hash = "sha256:0db77af6400c84d1747fe98a04b8b58f0080c77d11d338c4f507a9752880f221", size = 108044, upload-time = "2025-10-31T18:55:42.819Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/58/9f/d3c76f76c73fcc959d28e9def45b8b1cc3d7722660c5003b19c1022fd7f4/apscheduler-3.11.1-py3-none-any.whl", hash = "sha256:6162cb5683cb09923654fa9bdd3130c4be4bfda6ad8990971c9597ecd52965d2", size = 64278, upload-time = "2025-10-31T18:55:41.186Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blinker"
|
||||
version = "1.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "1.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/d4/4ad5432ac98c73096159d9ce7ffeb82d151c2ac84adcc6168e476bb54674/brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", size = 861523, upload-time = "2025-11-05T18:38:34.67Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/9f/9cc5bd03ee68a85dc4bc89114f7067c056a3c14b3d95f171918c088bf88d/brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", size = 444289, upload-time = "2025-11-05T18:38:35.6Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/b6/fe84227c56a865d16a6614e2c4722864b380cb14b13f3e6bef441e73a85a/brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", size = 1528076, upload-time = "2025-11-05T18:38:36.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/de/de4ae0aaca06c790371cf6e7ee93a024f6b4bb0568727da8c3de112e726c/brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", size = 1626880, upload-time = "2025-11-05T18:38:37.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/16/a1b22cbea436642e071adcaf8d4b350a2ad02f5e0ad0da879a1be16188a0/brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", size = 1419737, upload-time = "2025-11-05T18:38:38.729Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/63/c968a97cbb3bdbf7f974ef5a6ab467a2879b82afbc5ffb65b8acbb744f95/brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", size = 1484440, upload-time = "2025-11-05T18:38:39.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/9d/102c67ea5c9fc171f423e8399e585dabea29b5bc79b05572891e70013cdd/brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", size = 1593313, upload-time = "2025-11-05T18:38:41.24Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/4a/9526d14fa6b87bc827ba1755a8440e214ff90de03095cacd78a64abe2b7d/brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", size = 1487945, upload-time = "2025-11-05T18:38:42.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/e8/3fe1ffed70cbef83c5236166acaed7bb9c766509b157854c80e2f766b38c/brotli-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a", size = 334368, upload-time = "2025-11-05T18:38:43.345Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/91/e739587be970a113b37b821eae8097aac5a48e5f0eca438c22e4c7dd8648/brotli-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8", size = 369116, upload-time = "2025-11-05T18:38:44.609Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/e1/298c2ddf786bb7347a1cd71d63a347a79e5712a7c0cba9e3c3458ebd976f/brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", size = 863080, upload-time = "2025-11-05T18:38:45.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/0c/aac98e286ba66868b2b3b50338ffbd85a35c7122e9531a73a37a29763d38/brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", size = 445453, upload-time = "2025-11-05T18:38:46.433Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/f1/0ca1f3f99ae300372635ab3fe2f7a79fa335fee3d874fa7f9e68575e0e62/brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", size = 1528168, upload-time = "2025-11-05T18:38:47.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/a6/2ebfc8f766d46df8d3e65b880a2e220732395e6d7dc312c1e1244b0f074a/brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", size = 1627098, upload-time = "2025-11-05T18:38:48.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/2f/0976d5b097ff8a22163b10617f76b2557f15f0f39d6a0fe1f02b1a53e92b/brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", size = 1419861, upload-time = "2025-11-05T18:38:49.372Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/97/d76df7176a2ce7616ff94c1fb72d307c9a30d2189fe877f3dd99af00ea5a/brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", size = 1484594, upload-time = "2025-11-05T18:38:50.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/93/14cf0b1216f43df5609f5b272050b0abd219e0b54ea80b47cef9867b45e7/brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", size = 1593455, upload-time = "2025-11-05T18:38:51.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/73/3183c9e41ca755713bdf2cc1d0810df742c09484e2e1ddd693bee53877c1/brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", size = 1488164, upload-time = "2025-11-05T18:38:53.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/6a/0c78d8f3a582859236482fd9fa86a65a60328a00983006bcf6d83b7b2253/brotli-1.2.0-cp314-cp314-win32.whl", hash = "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d", size = 339280, upload-time = "2025-11-05T18:38:54.02Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/10/56978295c14794b2c12007b07f3e41ba26acda9257457d7085b0bb3bb90c/brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", size = 375639, upload-time = "2025-11-05T18:38:55.67Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cachelib"
|
||||
version = "0.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/69/0b5c1259e12fbcf5c2abe5934b5c0c1294ec0f845e2b4b2a51a91d79a4fb/cachelib-0.13.0.tar.gz", hash = "sha256:209d8996e3c57595bee274ff97116d1d73c4980b2fd9a34c7846cd07fd2e1a48", size = 34418, upload-time = "2024-04-13T14:18:27.782Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/42/960fc9896ddeb301716fdd554bab7941c35fb90a1dc7260b77df3366f87f/cachelib-0.13.0-py3-none-any.whl", hash = "sha256:8c8019e53b6302967d4e8329a504acf75e7bc46130291d30188a6e4e58162516", size = 20914, upload-time = "2024-04-13T14:18:26.361Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.11.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dnslib"
|
||||
version = "0.9.26"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/71/269f74ef9bc8ca453af2e1768d4f4c8e7ef5f894d058d27fd1b69c754d7f/dnslib-0.9.26.tar.gz", hash = "sha256:be56857534390b2fbd02935270019bacc5e6b411d156cb3921ac55a7fb51f1a8", size = 82901, upload-time = "2025-03-03T09:17:32.606Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/16/93/167364f10194e374119b211e475ed8763d7591ae4f7001b304282dde5825/dnslib-0.9.26-py3-none-any.whl", hash = "sha256:e68719e633d761747c7e91bd241019ef5a2b61a63f56025939e144c841a70e0d", size = 64161, upload-time = "2025-03-03T09:17:31.47Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dnspython"
|
||||
version = "2.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flask"
|
||||
version = "3.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "blinker" },
|
||||
{ name = "click" },
|
||||
{ name = "itsdangerous" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "markupsafe" },
|
||||
{ name = "werkzeug" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flask-caching"
|
||||
version = "2.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cachelib" },
|
||||
{ name = "flask" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e2/80/74846c8af58ed60972d64f23a6cd0c3ac0175677d7555dff9f51bf82c294/flask_caching-2.3.1.tar.gz", hash = "sha256:65d7fd1b4eebf810f844de7de6258254b3248296ee429bdcb3f741bcbf7b98c9", size = 67560, upload-time = "2025-02-23T01:34:40.207Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/00/bb/82daa5e2fcecafadcc8659ce5779679d0641666f9252a4d5a2ae987b0506/Flask_Caching-2.3.1-py3-none-any.whl", hash = "sha256:d3efcf600e5925ea5a2fcb810f13b341ae984f5b52c00e9d9070392f3ca10761", size = 28916, upload-time = "2025-02-23T01:34:37.749Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gunicorn"
|
||||
version = "23.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "packaging" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/34/72/9614c465dc206155d93eff0ca20d42e1e35afc533971379482de953521a4/gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec", size = 375031, upload-time = "2024-08-10T20:25:27.378Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/7d/6dac2a6e1eba33ee43f318edbed4ff29151a49b5d37f080aad1e6469bca4/gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", size = 85029, upload-time = "2024-08-10T20:25:24.996Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hnsdoh-status"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "apscheduler" },
|
||||
{ name = "brotli" },
|
||||
{ name = "dnslib" },
|
||||
{ name = "dnspython" },
|
||||
{ name = "flask" },
|
||||
{ name = "flask-caching" },
|
||||
{ name = "gunicorn" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "requests" },
|
||||
{ name = "schedule" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "apscheduler", specifier = ">=3.9.1" },
|
||||
{ name = "brotli", specifier = ">=1.2.0" },
|
||||
{ name = "dnslib", specifier = ">=0.9.26" },
|
||||
{ name = "dnspython", specifier = ">=2.8.0" },
|
||||
{ name = "flask", specifier = ">=3.1.2" },
|
||||
{ name = "flask-caching", specifier = ">=2.3.1" },
|
||||
{ name = "gunicorn", specifier = ">=23.0.0" },
|
||||
{ name = "python-dateutil", specifier = ">=2.9.0.post0" },
|
||||
{ name = "python-dotenv", specifier = ">=1.2.1" },
|
||||
{ name = "requests", specifier = ">=2.32.5" },
|
||||
{ name = "schedule", specifier = ">=1.2.2" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "ruff", specifier = ">=0.14.5" }]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itsdangerous"
|
||||
version = "2.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "25.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "six" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.14.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/fa/fbb67a5780ae0f704876cb8ac92d6d76da41da4dc72b7ed3565ab18f2f52/ruff-0.14.5.tar.gz", hash = "sha256:8d3b48d7d8aad423d3137af7ab6c8b1e38e4de104800f0d596990f6ada1a9fc1", size = 5615944, upload-time = "2025-11-13T19:58:51.155Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/68/31/c07e9c535248d10836a94e4f4e8c5a31a1beed6f169b31405b227872d4f4/ruff-0.14.5-py3-none-linux_armv6l.whl", hash = "sha256:f3b8248123b586de44a8018bcc9fefe31d23dda57a34e6f0e1e53bd51fd63594", size = 13171630, upload-time = "2025-11-13T19:57:54.894Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/5c/283c62516dca697cd604c2796d1487396b7a436b2f0ecc3fd412aca470e0/ruff-0.14.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f7a75236570318c7a30edd7f5491945f0169de738d945ca8784500b517163a72", size = 13413925, upload-time = "2025-11-13T19:57:59.181Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/f3/aa319f4afc22cb6fcba2b9cdfc0f03bbf747e59ab7a8c5e90173857a1361/ruff-0.14.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6d146132d1ee115f8802356a2dc9a634dbf58184c51bff21f313e8cd1c74899a", size = 12574040, upload-time = "2025-11-13T19:58:02.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/7f/cb5845fcc7c7e88ed57f58670189fc2ff517fe2134c3821e77e29fd3b0c8/ruff-0.14.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2380596653dcd20b057794d55681571a257a42327da8894b93bbd6111aa801f", size = 13009755, upload-time = "2025-11-13T19:58:05.172Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/d2/bcbedbb6bcb9253085981730687ddc0cc7b2e18e8dc13cf4453de905d7a0/ruff-0.14.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2d1fa985a42b1f075a098fa1ab9d472b712bdb17ad87a8ec86e45e7fa6273e68", size = 12937641, upload-time = "2025-11-13T19:58:08.345Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/58/e25de28a572bdd60ffc6bb71fc7fd25a94ec6a076942e372437649cbb02a/ruff-0.14.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88f0770d42b7fa02bbefddde15d235ca3aa24e2f0137388cc15b2dcbb1f7c7a7", size = 13610854, upload-time = "2025-11-13T19:58:11.419Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/24/43bb3fd23ecee9861970978ea1a7a63e12a204d319248a7e8af539984280/ruff-0.14.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3676cb02b9061fee7294661071c4709fa21419ea9176087cb77e64410926eb78", size = 15061088, upload-time = "2025-11-13T19:58:14.551Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/44/a022f288d61c2f8c8645b24c364b719aee293ffc7d633a2ca4d116b9c716/ruff-0.14.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b595bedf6bc9cab647c4a173a61acf4f1ac5f2b545203ba82f30fcb10b0318fb", size = 14734717, upload-time = "2025-11-13T19:58:17.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/81/5c6ba44de7e44c91f68073e0658109d8373b0590940efe5bd7753a2585a3/ruff-0.14.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f55382725ad0bdb2e8ee2babcbbfb16f124f5a59496a2f6a46f1d9d99d93e6e2", size = 14028812, upload-time = "2025-11-13T19:58:20.533Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/ef/41a8b60f8462cb320f68615b00299ebb12660097c952c600c762078420f8/ruff-0.14.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7497d19dce23976bdaca24345ae131a1d38dcfe1b0850ad8e9e6e4fa321a6e19", size = 13825656, upload-time = "2025-11-13T19:58:23.345Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/00/207e5de737fdb59b39eb1fac806904fe05681981b46d6a6db9468501062e/ruff-0.14.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:410e781f1122d6be4f446981dd479470af86537fb0b8857f27a6e872f65a38e4", size = 13959922, upload-time = "2025-11-13T19:58:26.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/7e/fa1f5c2776db4be405040293618846a2dece5c70b050874c2d1f10f24776/ruff-0.14.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c01be527ef4c91a6d55e53b337bfe2c0f82af024cc1a33c44792d6844e2331e1", size = 12932501, upload-time = "2025-11-13T19:58:29.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/d8/d86bf784d693a764b59479a6bbdc9515ae42c340a5dc5ab1dabef847bfaa/ruff-0.14.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f66e9bb762e68d66e48550b59c74314168ebb46199886c5c5aa0b0fbcc81b151", size = 12927319, upload-time = "2025-11-13T19:58:32.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/de/ee0b304d450ae007ce0cb3e455fe24fbcaaedae4ebaad6c23831c6663651/ruff-0.14.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d93be8f1fa01022337f1f8f3bcaa7ffee2d0b03f00922c45c2207954f351f465", size = 13206209, upload-time = "2025-11-13T19:58:35.952Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/aa/193ca7e3a92d74f17d9d5771a765965d2cf42c86e6f0fd95b13969115723/ruff-0.14.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c135d4b681f7401fe0e7312017e41aba9b3160861105726b76cfa14bc25aa367", size = 13953709, upload-time = "2025-11-13T19:58:39.002Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/f1/7119e42aa1d3bf036ffc9478885c2e248812b7de9abea4eae89163d2929d/ruff-0.14.5-py3-none-win32.whl", hash = "sha256:c83642e6fccfb6dea8b785eb9f456800dcd6a63f362238af5fc0c83d027dd08b", size = 12925808, upload-time = "2025-11-13T19:58:42.779Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/9d/7c0a255d21e0912114784e4a96bf62af0618e2190cae468cd82b13625ad2/ruff-0.14.5-py3-none-win_amd64.whl", hash = "sha256:9d55d7af7166f143c94eae1db3312f9ea8f95a4defef1979ed516dbb38c27621", size = 14331546, upload-time = "2025-11-13T19:58:45.691Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/80/69756670caedcf3b9be597a6e12276a6cf6197076eb62aad0c608f8efce0/ruff-0.14.5-py3-none-win_arm64.whl", hash = "sha256:4b700459d4649e2594b31f20a9de33bc7c19976d4746d8d0798ad959621d64a4", size = 13433331, upload-time = "2025-11-13T19:58:48.434Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schedule"
|
||||
version = "1.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0c/91/b525790063015759f34447d4cf9d2ccb52cdee0f1dd6ff8764e863bcb74c/schedule-1.2.2.tar.gz", hash = "sha256:15fe9c75fe5fd9b9627f3f19cc0ef1420508f9f9a46f45cd0769ef75ede5f0b7", size = 26452, upload-time = "2024-06-18T20:03:14.633Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/a7/84c96b61fd13205f2cafbe263cdb2745965974bdf3e0078f121dfeca5f02/schedule-1.2.2-py3-none-any.whl", hash = "sha256:5bef4a2a0183abf44046ae0d164cadcac21b1db011bdd8102e4a0c1e91e06a7d", size = 12220, upload-time = "2024-05-25T18:41:59.121Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2025.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzlocal"
|
||||
version = "5.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "werkzeug"
|
||||
version = "3.1.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925, upload-time = "2024-11-08T15:52:18.093Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" },
|
||||
]
|
||||
Reference in New Issue
Block a user