add email and password verification to client-side vpn

This commit is contained in:
arthur 2025-10-19 23:31:07 +07:00
parent 10f8e81d92
commit 693930cd7b
5 changed files with 442 additions and 204 deletions

192
cli.py Executable file
View File

@ -0,0 +1,192 @@
#!/usr/bin/env python3
"""
VPN Access Server - Main Entry Point
Unified command-line interface for all VPN Access Server operations.
Supports authentication, session management, database operations, and testing.
"""
import sys
import os
import argparse
from pathlib import Path
# Add the access module to the Python path
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'access'))
def run_auth():
"""Run authentication module."""
from access.auth import main as auth_main
auth_main()
def run_session():
"""Run session management module."""
from access.session import main as session_main
session_main()
def run_init_db():
"""Initialize database schema."""
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'scripts'))
from scripts.init_db import main as init_db_main
init_db_main()
def run_seed_data():
"""Seed database with sample data."""
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'scripts'))
from scripts.seed_data import main as seed_data_main
seed_data_main()
def run_tests():
"""Run unit tests."""
import subprocess
try:
result = subprocess.run([
sys.executable, '-m', 'pytest', 'tests/', '-v'
], cwd=os.path.dirname(os.path.abspath(__file__)))
sys.exit(result.returncode)
except FileNotFoundError:
print("pytest not found. Install test dependencies with: uv add --group test pytest pytest-cov")
sys.exit(1)
def health_check():
"""Check system health and connectivity."""
from access.config import config
from access.db import db
from access.utils import setup_logging
logger = setup_logging(log_level='INFO')
print("🔍 VPN Access Server Health Check")
print("=" * 50)
# Configuration check
print("📋 Configuration...")
if config.validate():
print("✅ Configuration is valid")
else:
print("❌ Configuration validation failed")
return False
# Database check
print("🗄️ Database connectivity...")
if db.health_check():
print("✅ Database connection successful")
else:
print("❌ Database connection failed")
return False
# Test utilities
print("🔧 Utility functions...")
from access.utils import validate_mac_address, normalize_mac_address
test_mac = "00:11:22:33:44:55"
if validate_mac_address(test_mac) and normalize_mac_address("001122334455") == test_mac:
print("✅ Utility functions working")
else:
print("❌ Utility functions failed")
return False
print("=" * 50)
print("✅ All health checks passed!")
return True
def show_status():
"""Show system status and configuration."""
from access.config import config
print("📊 VPN Access Server Status")
print("=" * 50)
print(f"Database Host: {config.database.host}:{config.database.port}")
print(f"Database Name: {config.database.database}")
print(f"Database User: {config.database.username}")
print(f"Log Level: {config.server.log_level}")
print(f"Log File: {config.server.log_file}")
print(f"Default Session Limit: {config.server.default_session_limit}s ({config.server.default_session_limit//3600}h)")
print(f"Max Session Limit: {config.server.max_session_limit}s ({config.server.max_session_limit//3600}h)")
print("=" * 50)
from util.client.generate_client import generate_client_config
def run_gen_client(username: str):
"""Generate a client .ovpn file."""
generate_client_config(username)
def main():
"""Main entry point with command-line interface."""
parser = argparse.ArgumentParser(
description="VPN Access Server - OpenVPN authentication and session management",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s auth # Run authentication (for OpenVPN)
%(prog)s session # Run session management (for OpenVPN)
%(prog)s init-db # Initialize database schema
%(prog)s seed-data # Add sample data for testing
%(prog)s test # Run unit tests
%(prog)s health-check # Check system health
%(prog)s status # Show configuration status
%(prog)s gen-client <user> # Generate a client .ovpn file
Environment Variables:
DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD
LOG_LEVEL, DEFAULT_SESSION_LIMIT, MAX_SESSION_LIMIT
See .env.example for full configuration options.
"""
)
subparsers = parser.add_subparsers(dest='command', help='Commands')
subparsers.add_parser('auth', help='Run authentication (for OpenVPN)')
subparsers.add_parser('session', help='Run session management (for OpenVPN)')
subparsers.add_parser('init-db', help='Initialize database schema')
subparsers.add_parser('seed-data', help='Add sample data for testing')
subparsers.add_parser('test', help='Run unit tests')
subparsers.add_parser('health-check', help='Check system health')
subparsers.add_parser('status', help='Show configuration status')
gen_client_parser = subparsers.add_parser('gen-client', help='Generate a client .ovpn file')
gen_client_parser.add_argument('username', help='Username for the client config')
parser.add_argument(
'--version',
action='version',
version='VPN Access Server 1.0.0'
)
if len(sys.argv) < 2:
parser.print_help()
sys.exit(1)
args = parser.parse_args()
try:
if args.command == 'auth':
run_auth()
elif args.command == 'session':
run_session()
elif args.command == 'init-db':
run_init_db()
elif args.command == 'seed-data':
run_seed_data()
elif args.command == 'test':
run_tests()
elif args.command == 'health-check':
if not health_check():
sys.exit(1)
elif args.command == 'status':
show_status()
elif args.command == 'gen-client':
run_gen_client(args.username)
except KeyboardInterrupt:
print("\n⚠️ Operation cancelled by user")
sys.exit(1)
except Exception as e:
print(f"❌ Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

217
main.py Executable file → Normal file
View File

@ -1,192 +1,51 @@
#!/usr/bin/env python3
"""
VPN Access Server - Main Entry Point
Unified command-line interface for all VPN Access Server operations.
Supports authentication, session management, database operations, and testing.
FastAPI server for the VPN Access Server.
"""
import sys
import uvicorn
import os
import argparse
from pathlib import Path
# Add the access module to the Python path
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'access'))
def run_auth():
"""Run authentication module."""
from access.auth import main as auth_main
auth_main()
def run_session():
"""Run session management module."""
from access.session import main as session_main
session_main()
def run_init_db():
"""Initialize database schema."""
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'scripts'))
from scripts.init_db import main as init_db_main
init_db_main()
def run_seed_data():
"""Seed database with sample data."""
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'scripts'))
from scripts.seed_data import main as seed_data_main
seed_data_main()
def run_tests():
"""Run unit tests."""
import subprocess
try:
result = subprocess.run([
sys.executable, '-m', 'pytest', 'tests/', '-v'
], cwd=os.path.dirname(os.path.abspath(__file__)))
sys.exit(result.returncode)
except FileNotFoundError:
print("pytest not found. Install test dependencies with: uv add --group test pytest pytest-cov")
sys.exit(1)
def health_check():
"""Check system health and connectivity."""
from access.config import config
from access.db import db
from access.utils import setup_logging
logger = setup_logging(log_level='INFO')
print("🔍 VPN Access Server Health Check")
print("=" * 50)
# Configuration check
print("📋 Configuration...")
if config.validate():
print("✅ Configuration is valid")
else:
print("❌ Configuration validation failed")
return False
# Database check
print("🗄️ Database connectivity...")
if db.health_check():
print("✅ Database connection successful")
else:
print("❌ Database connection failed")
return False
# Test utilities
print("🔧 Utility functions...")
from access.utils import validate_mac_address, normalize_mac_address
test_mac = "00:11:22:33:44:55"
if validate_mac_address(test_mac) and normalize_mac_address("001122334455") == test_mac:
print("✅ Utility functions working")
else:
print("❌ Utility functions failed")
return False
print("=" * 50)
print("✅ All health checks passed!")
return True
def show_status():
"""Show system status and configuration."""
from access.config import config
print("📊 VPN Access Server Status")
print("=" * 50)
print(f"Database Host: {config.database.host}:{config.database.port}")
print(f"Database Name: {config.database.database}")
print(f"Database User: {config.database.username}")
print(f"Log Level: {config.server.log_level}")
print(f"Log File: {config.server.log_file}")
print(f"Default Session Limit: {config.server.default_session_limit}s ({config.server.default_session_limit//3600}h)")
print(f"Max Session Limit: {config.server.max_session_limit}s ({config.server.max_session_limit//3600}h)")
print("=" * 50)
from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse
from pydantic import BaseModel
from util.client.generate_client import generate_client_config
app = FastAPI(
title="VPN Access Server API",
description="API for managing VPN clients and server operations.",
version="1.0.0",
)
def run_gen_client(username: str):
"""Generate a client .ovpn file."""
generate_client_config(username)
class ClientRequest(BaseModel):
username: str
email: str
@app.post("/api/client/generate", summary="Generate a new VPN client configuration")
def generate_client(request: ClientRequest):
"""
Generates a new OpenVPN client configuration (.ovpn) file.
def main():
"""Main entry point with command-line interface."""
parser = argparse.ArgumentParser(
description="VPN Access Server - OpenVPN authentication and session management",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s auth # Run authentication (for OpenVPN)
%(prog)s session # Run session management (for OpenVPN)
%(prog)s init-db # Initialize database schema
%(prog)s seed-data # Add sample data for testing
%(prog)s test # Run unit tests
%(prog)s health-check # Check system health
%(prog)s status # Show configuration status
%(prog)s gen-client <user> # Generate a client .ovpn file
Environment Variables:
DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD
LOG_LEVEL, DEFAULT_SESSION_LIMIT, MAX_SESSION_LIMIT
See .env.example for full configuration options.
"""
)
subparsers = parser.add_subparsers(dest='command', help='Commands')
subparsers.add_parser('auth', help='Run authentication (for OpenVPN)')
subparsers.add_parser('session', help='Run session management (for OpenVPN)')
subparsers.add_parser('init-db', help='Initialize database schema')
subparsers.add_parser('seed-data', help='Add sample data for testing')
subparsers.add_parser('test', help='Run unit tests')
subparsers.add_parser('health-check', help='Check system health')
subparsers.add_parser('status', help='Show configuration status')
gen_client_parser = subparsers.add_parser('gen-client', help='Generate a client .ovpn file')
gen_client_parser.add_argument('username', help='Username for the client config')
parser.add_argument(
'--version',
action='version',
version='VPN Access Server 1.0.0'
)
if len(sys.argv) < 2:
parser.print_help()
sys.exit(1)
args = parser.parse_args()
try:
if args.command == 'auth':
run_auth()
elif args.command == 'session':
run_session()
elif args.command == 'init-db':
run_init_db()
elif args.command == 'seed-data':
run_seed_data()
elif args.command == 'test':
run_tests()
elif args.command == 'health-check':
if not health_check():
sys.exit(1)
elif args.command == 'status':
show_status()
elif args.command == 'gen-client':
run_gen_client(args.username)
except KeyboardInterrupt:
print("\n⚠️ Operation cancelled by user")
sys.exit(1)
except Exception as e:
print(f"❌ Error: {e}")
sys.exit(1)
This endpoint will:
1. Trigger `easyrsa` to generate a new client certificate and key.
2. Assemble the `.ovpn` file with the new certificate/key and the server's CA and TA keys.
3. Save the file to the server's client configuration directory.
"""
success, message = generate_client_config(request.username)
if not success:
raise HTTPException(status_code=500, detail=message)
return {"message": message}
@app.get("/api/client/get-config/{username}", summary="Download a client configuration file")
def get_client_config(username: str, email: str):
"""
Downloads the .ovpn configuration file for a specific client.
The file is sought in the `generated-clients` directory.
"""
file_path = os.path.join("generated-clients", f"{username}.ovpn")
if not os.path.isfile(file_path):
raise HTTPException(status_code=404, detail="Configuration file not found for this user. Please generate it first.")
return FileResponse(path=file_path, filename=f"{username}.ovpn", media_type='application/octet-stream')
if __name__ == "__main__":
main()
uvicorn.run(app, host="0.0.0.0", port=8443)

View File

@ -8,6 +8,8 @@ dependencies = [
"db>=0.1.1",
"mysql-connector-python>=8.0.33",
"utils>=1.0.2",
"fastapi>=0.111.0",
"uvicorn>=0.20.0",
]
[project.optional-dependencies]
@ -17,7 +19,7 @@ test = [
]
[project.scripts]
vpn-access-server = "main:main"
vpn-access-server = "cli:main"
[build-system]
requires = ["hatchling"]

View File

@ -4,48 +4,50 @@ Utility for generating OpenVPN client configuration files.
import os
import subprocess
from typing import Tuple
def generate_client_config(username: str):
def generate_client_config(username: str, email: str) -> Tuple[bool, str]:
"""
Generates a .ovpn file for a given user.
Args:
username: The username for which to generate the config.
email: The email for which to generate the config and used as password
Returns:
A tuple containing a boolean indicating success and a message.
"""
easyrsa_dir = "/home/arthur/openvpn-ca/"
ca_path = "/home/arthur/openvpn-ca/pki/ca.crt"
ta_path = "/home/arthur/openvpn-ca/ta.key"
client_crt_path = f"/home/arthur/openvpn-ca/pki/issued/{username}.crt"
client_key_path = f"/home/arthur/openvpn-ca/pki/private/{username}.key"
output_path = f"/etc/openvpn/client/{username}.ovpn"
output_path = f"generated-clients/{username}.ovpn"
# config password into env
password = email
env = os.environ.copy()
env["EASYRSA_PASSOUT"] = f"pass:{password}"
# Step 1: Generate the client certificate
print(f"Generating certificate for user: {username}...")
try:
command = ["./easyrsa", "--batch", "build-client-full", username, "nopass"]
command = ["./easyrsa", "--batch", "build-client-full", username]
process = subprocess.run(
command,
cwd=easyrsa_dir,
env=env,
check=True,
capture_output=True,
text=True
)
print(process.stdout)
print("Certificate generated successfully.")
except FileNotFoundError:
print(f"Error: 'easyrsa' script not found in {easyrsa_dir}. Please check the path.")
return
return False, f"Error: 'easyrsa' script not found in {easyrsa_dir}. Please check the path."
except subprocess.CalledProcessError as e:
print(f"Error generating certificate for user: {username}")
print(f"Return code: {e.returncode}")
print(f"Stderr: {e.stderr}")
return
return False, f"Error generating certificate for user: {username}. Stderr: {e.stderr}"
# Step 2: Verify that all required files exist
for f in [ca_path, ta_path, client_crt_path, client_key_path]:
if not os.path.isfile(f):
print(f"Error: Cannot read file '{f}'. File not found after generation.")
return
return False, f"Error: Cannot read file '{f}'. File not found after generation."
# Step 3: Read the content of the files
try:
@ -58,8 +60,7 @@ def generate_client_config(username: str):
with open(ta_path, 'r') as f:
ta_content = f.read()
except IOError as e:
print(f"Error reading files: {e}")
return
return False, f"Error reading files: {e}"
# Step 4: Assemble the .ovpn configuration
ovpn_config = f"""
@ -98,22 +99,17 @@ key-direction 1
# Step 5: Write the configuration to the output file
try:
output_dir = os.path.dirname(output_path)
# Check if dir exists and if we have write permission
if not os.path.isdir(output_dir) or not os.access(output_dir, os.W_OK):
print(f"Error: Output directory '{output_dir}' does not exist or is not writable.")
print("Please ensure you have the correct permissions to write to this directory.")
# As a fallback, save to a local directory
local_output_dir = "generated-clients"
if not os.path.exists(local_output_dir):
os.makedirs(local_output_dir)
local_output_path = os.path.join(local_output_dir, f"{username}.ovpn")
with open(local_output_path, 'w') as f:
f.write(ovpn_config)
print(f"Could not write to server path. Saved config locally to: {local_output_path}")
return
return True, f"Successfully generated client config. Could not write to server path. Saved config locally to: {local_output_path}"
with open(output_path, 'w') as f:
f.write(ovpn_config)
print(f"Successfully generated client config: {output_path}")
return True, f"Successfully generated client config: {output_path}"
except IOError as e:
print(f"Error writing to file: {e}")
return False, f"Error writing to file: {e}"

189
uv.lock generated
View File

@ -2,12 +2,46 @@ version = 1
revision = 2
requires-python = ">=3.13"
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
]
[[package]]
name = "antiorm"
version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0b/f8/71baa4824d9666c1be51d117119579a97f461ddbded48b2e01a6ad0554b5/antiorm-1.2.1.tar.gz", hash = "sha256:96eb1841ce5163db4cf1dc13f4499ec2d7cffc190cf724b78ffdd3e6b7c4ff93", size = 171953, upload-time = "2016-06-28T22:52:03.354Z" }
[[package]]
name = "anyio"
version = "4.11.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
]
[[package]]
name = "click"
version = "8.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
@ -87,6 +121,38 @@ dependencies = [
]
sdist = { url = "https://files.pythonhosted.org/packages/a9/22/f65d64c83e63790b3273c6adb3bff338ad594f46d84b41bd1f94593b40a6/db-0.1.1.tar.gz", hash = "sha256:980e772f15c1161d3b287ffec4f144e40961b0b3e6d5102809577870bf6c5808", size = 3350, upload-time = "2014-12-13T05:13:58.159Z" }
[[package]]
name = "fastapi"
version = "0.119.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "starlette" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0a/f9/5c5bcce82a7997cc0eb8c47b7800f862f6b56adc40486ed246e5010d443b/fastapi-0.119.0.tar.gz", hash = "sha256:451082403a2c1f0b99c6bd57c09110ed5463856804c8078d38e5a1f1035dbbb7", size = 336756, upload-time = "2025-10-11T17:13:40.53Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/70/584c4d7cad80f5e833715c0a29962d7c93b4d18eed522a02981a6d1b6ee5/fastapi-0.119.0-py3-none-any.whl", hash = "sha256:90a2e49ed19515320abb864df570dd766be0662c5d577688f1600170f7f73cf2", size = 107095, upload-time = "2025-10-11T17:13:39.048Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[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 = "iniconfig"
version = "2.1.0"
@ -128,6 +194,70 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pydantic"
version = "2.12.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74", size = 819383, upload-time = "2025-10-17T15:04:21.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431, upload-time = "2025-10-17T15:04:19.346Z" },
]
[[package]]
name = "pydantic-core"
version = "2.41.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688, upload-time = "2025-10-14T10:20:54.448Z" },
{ url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807, upload-time = "2025-10-14T10:20:56.115Z" },
{ url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669, upload-time = "2025-10-14T10:20:57.874Z" },
{ url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629, upload-time = "2025-10-14T10:21:00.006Z" },
{ url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049, upload-time = "2025-10-14T10:21:01.801Z" },
{ url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409, upload-time = "2025-10-14T10:21:03.556Z" },
{ url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635, upload-time = "2025-10-14T10:21:05.385Z" },
{ url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284, upload-time = "2025-10-14T10:21:07.122Z" },
{ url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566, upload-time = "2025-10-14T10:21:08.981Z" },
{ url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809, upload-time = "2025-10-14T10:21:10.805Z" },
{ url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119, upload-time = "2025-10-14T10:21:12.583Z" },
{ url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c", size = 1981398, upload-time = "2025-10-14T10:21:14.584Z" },
{ url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4", size = 2030735, upload-time = "2025-10-14T10:21:16.432Z" },
{ url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564", size = 1973209, upload-time = "2025-10-14T10:21:18.213Z" },
{ url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324, upload-time = "2025-10-14T10:21:20.363Z" },
{ url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515, upload-time = "2025-10-14T10:21:22.339Z" },
{ url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819, upload-time = "2025-10-14T10:21:26.683Z" },
{ url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2", size = 1995866, upload-time = "2025-10-14T10:21:28.951Z" },
{ url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89", size = 1970034, upload-time = "2025-10-14T10:21:30.869Z" },
{ url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022, upload-time = "2025-10-14T10:21:32.809Z" },
{ url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495, upload-time = "2025-10-14T10:21:34.812Z" },
{ url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131, upload-time = "2025-10-14T10:21:36.924Z" },
{ url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236, upload-time = "2025-10-14T10:21:38.927Z" },
{ url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573, upload-time = "2025-10-14T10:21:41.574Z" },
{ url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467, upload-time = "2025-10-14T10:21:44.018Z" },
{ url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754, upload-time = "2025-10-14T10:21:46.466Z" },
{ url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754, upload-time = "2025-10-14T10:21:48.486Z" },
{ url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115, upload-time = "2025-10-14T10:21:50.63Z" },
{ url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400, upload-time = "2025-10-14T10:21:52.959Z" },
{ url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070, upload-time = "2025-10-14T10:21:55.419Z" },
{ url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277, upload-time = "2025-10-14T10:21:57.474Z" },
{ url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608, upload-time = "2025-10-14T10:21:59.557Z" },
{ url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614, upload-time = "2025-10-14T10:22:01.847Z" },
{ url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904, upload-time = "2025-10-14T10:22:04.062Z" },
{ url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538, upload-time = "2025-10-14T10:22:06.39Z" },
{ url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183, upload-time = "2025-10-14T10:22:08.812Z" },
{ url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542, upload-time = "2025-10-14T10:22:11.332Z" },
{ url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
@ -167,20 +297,77 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
[[package]]
name = "starlette"
version = "0.48.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949, upload-time = "2025-09-13T08:41:05.699Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "typing-inspection"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
]
[[package]]
name = "utils"
version = "1.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ad/1f/c196d21c2df061923154aecf24cab049a114394956e90c9bfbfdd398e27a/utils-1.0.2.tar.gz", hash = "sha256:f4d5157e27e9d434006b5b52a1ec951a34e53e7ecaa145d43a153ec452eb5d9e", size = 13203, upload-time = "2024-01-06T07:11:25.75Z" }
[[package]]
name = "uvicorn"
version = "0.38.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" },
]
[[package]]
name = "vpn-access-server"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "db" },
{ name = "fastapi" },
{ name = "mysql-connector-python" },
{ name = "utils" },
{ name = "uvicorn" },
]
[package.optional-dependencies]
@ -198,10 +385,12 @@ test = [
[package.metadata]
requires-dist = [
{ name = "db", specifier = ">=0.1.1" },
{ name = "fastapi", specifier = ">=0.111.0" },
{ name = "mysql-connector-python", specifier = ">=8.0.33" },
{ name = "pytest", marker = "extra == 'test'", specifier = ">=7.0.0" },
{ name = "pytest-cov", marker = "extra == 'test'", specifier = ">=4.0.0" },
{ name = "utils", specifier = ">=1.0.2" },
{ name = "uvicorn", specifier = ">=0.20.0" },
]
provides-extras = ["test"]