diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 00000000..ab1f4164
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,10 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Ignored default folder with query files
+/queries/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
+# Editor-based HTTP Client requests
+/httpRequests/
diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml
new file mode 100644
index 00000000..21646cce
--- /dev/null
+++ b/.idea/dictionaries/project.xml
@@ -0,0 +1,10 @@
+
+
+
+ fname
+ ilike
+ nexiosmodel
+ vickram
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 00000000..105ce2da
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 00000000..39379463
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 00000000..df1314e1
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/nexios.iml b/.idea/nexios.iml
new file mode 100644
index 00000000..32723422
--- /dev/null
+++ b/.idea/nexios.iml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 00000000..35eb1ddf
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/example.db b/example.db
new file mode 100644
index 00000000..cfc2509a
Binary files /dev/null and b/example.db differ
diff --git a/nexios.db b/nexios.db
new file mode 100644
index 00000000..498e45be
Binary files /dev/null and b/nexios.db differ
diff --git a/nexios/logging.py b/nexios/logging.py
index 149dd074..76f77258 100644
--- a/nexios/logging.py
+++ b/nexios/logging.py
@@ -78,8 +78,9 @@ def create_logger(
logger.setLevel(log_level)
console_handler = StreamHandler(sys.stderr)
+ formatter = "[%(asctime)s] %(levelname)s in %(module)s: %(message)s"
console_handler.setFormatter(
- Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s")
+ Formatter(formatter)
)
handlers: Tuple[Handler, ...] = (console_handler,)
@@ -89,7 +90,7 @@ def create_logger(
log_file, maxBytes=max_bytes, backupCount=backup_count
)
file_handler.setFormatter(
- Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s")
+ Formatter(formatter)
)
handlers += (file_handler,)
diff --git a/nexios/orm/__init__.py b/nexios/orm/__init__.py
new file mode 100644
index 00000000..6c037787
--- /dev/null
+++ b/nexios/orm/__init__.py
@@ -0,0 +1,13 @@
+from .fields import Field, FieldInfo
+from .relationships import Relationship, RelationshipType
+from .model import NexiosModel
+from .query.builder import Select
+
+
+__all__ = [
+ "Field",
+ "Relationship",
+ "RelationshipType",
+ "NexiosModel",
+ "Select"
+]
\ No newline at end of file
diff --git a/nexios/orm/benchmark/__init__.py b/nexios/orm/benchmark/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/nexios/orm/benchmark/mysql.py b/nexios/orm/benchmark/mysql.py
new file mode 100644
index 00000000..6b061560
--- /dev/null
+++ b/nexios/orm/benchmark/mysql.py
@@ -0,0 +1,409 @@
+import time
+import statistics
+import threading
+import matplotlib.pyplot as plt
+import pandas as pd
+from concurrent.futures import ThreadPoolExecutor, as_completed
+import logging
+import numpy as np
+from typing import Dict, Any, cast
+from mysql.connector import pooling as mysql_pooling
+import mysql.connector
+
+# Disable verbose logging for clean benchmark output
+logging.getLogger().setLevel(logging.ERROR)
+
+class MySQLConnectionPoolBenchmark:
+ """Comprehensive benchmark suite for connection pools"""
+
+ def __init__(self, **kwargs: Any):
+ self.kwargs = kwargs
+ self.results = {}
+
+ def benchmark_mysql_pool(self, pool_size: int, operations: int, concurrency: int):
+ """Benchmark mysql's built-in connection pool"""
+ print(f"Testing mysql pool: {pool_size} connections, {operations} ops, {concurrency} threads")
+
+ pool = mysql_pooling.MySQLConnectionPool(
+ pool_name="mysql_pool",
+ pool_size=pool_size,
+ **self.kwargs
+ )
+
+ times = []
+ errors = 0
+
+ def worker(worker_id: int):
+ worker_times = []
+ for i in range(operations // concurrency):
+ start_time = time.perf_counter()
+ try:
+ with pool.get_connection() as conn:
+ with conn.cursor() as cur:
+ cur.execute("SELECT 1") # Small sleep to simulate work
+ cur.fetchone()
+ worker_times.append(time.perf_counter() - start_time)
+ except Exception as e:
+ nonlocal errors
+ errors += 1
+ return worker_times
+
+ # Run benchmark
+ start_total = time.perf_counter()
+ futures = []
+ try:
+ with ThreadPoolExecutor(max_workers=concurrency) as executor:
+ exec = [executor.submit(worker, i) for i in range(concurrency)]
+ futures.extend(exec)
+ for future in as_completed(futures, timeout=60.0):
+ times.extend(future.result())
+ except TimeoutError as e:
+ print(f" WARNING: Benchmark timed out after 60 seconds: {e}")
+ # Collect whatever results we have
+ for future in futures:
+ if future.done():
+ try:
+ times.extend(future.result())
+ except Exception:
+ pass
+
+ end_total = time.perf_counter()
+
+ conn = pool.get_connection()
+ conn.close()
+
+ total_time = end_total - start_total
+ ops_per_sec = operations / total_time
+
+ self.results['mysql'] = {
+ 'total_time': total_time,
+ 'operations_per_second': ops_per_sec,
+ 'avg_latency_ms': statistics.mean(times) * 1000,
+ 'p95_latency_ms': np.percentile(times, 95) * 1000 if times else 0,
+ 'errors': errors,
+ 'min_size': pool_size,
+ 'max_size': pool_size
+ }
+
+ print(f" Results: {ops_per_sec:.1f} ops/sec, {statistics.mean(times)*1000:.1f}ms avg latency")
+ return self.results['mysql']
+
+
+ def benchmark_custom_pool(self, min_size: int, max_size: int, operations: int, concurrency: int):
+ """Benchmark your custom connection pool"""
+ print(f"Testing custom pool: {min_size}-{max_size} connections, {operations} ops, {concurrency} threads")
+
+ from nexios.orm.pool.base import PoolConfig
+ from nexios.orm.pool.connection_pool import ConnectionPool
+
+ config = PoolConfig(
+ min_size=min_size,
+ max_size=max_size,
+ connection_timeout=10.0,
+ health_check_interval=300,
+ max_lifetime=3600,
+ idle_timeout=1800
+ )
+
+ def create_conn():
+ from nexios.orm.dbapi.mysql.mysql_connector_ import MySQLConnectorConnection
+ conn = cast(mysql.connector.connection.MySQLConnection, mysql.connector.connect(**self.kwargs))
+ return MySQLConnectorConnection(conn)
+
+ pool = ConnectionPool(create_conn, config)
+
+ time.sleep(1) # Allow pool to initialize
+
+ times = []
+ errors = 0
+
+ def worker(worker_id: int):
+ worker_times = []
+ for i in range(operations // concurrency):
+ start_time = time.perf_counter()
+ try:
+ with pool.connection() as conn:
+ cur = conn.cursor()
+ cur.execute("SELECT 1")
+ cur.fetchone()
+ worker_times.append(time.perf_counter() - start_time)
+ except Exception as e:
+ nonlocal errors
+ errors += 1
+ return worker_times
+
+ # Run benchmark
+ start_total = time.perf_counter()
+ futures_custom = []
+ try:
+ with ThreadPoolExecutor(max_workers=concurrency) as executor:
+ exec_ = [executor.submit(worker, i) for i in range(concurrency)]
+ futures_custom.extend(exec_)
+ # futures_custom = [executor.submit(worker, i) for i in range(concurrency)]
+ for future in as_completed(futures_custom):
+ times.extend(future.result())
+ except TimeoutError as e:
+ print(f" WARNING: Benchmark timed out after 60 seconds: {e}")
+ # Collect whatever results we have
+ for future in futures_custom:
+ if future.done():
+ try:
+ times.extend(future.result())
+ except Exception:
+ pass
+
+ end_total = time.perf_counter()
+
+ pool.close()
+
+ total_time = end_total - start_total
+ ops_per_sec = operations / total_time
+
+ self.results['custom'] = {
+ 'total_time': total_time,
+ 'operations_per_second': ops_per_sec,
+ 'avg_latency_ms': statistics.mean(times) * 1000,
+ 'p95_latency_ms': np.percentile(times, 95) * 1000 if times else 0,
+ 'errors': errors,
+ 'min_size': min_size,
+ 'max_size': max_size
+ }
+
+ print(f" Results: {ops_per_sec:.1f} ops/sec, {statistics.mean(times)*1000:.1f}ms avg latency")
+ return self.results['custom']
+
+ def benchmark_under_load(self, pool_type: str, duration: int = 60):
+ """Benchmark under sustained load with varying concurrency"""
+ print(f"Testing {pool_type} pool under sustained load for {duration}s")
+
+ if pool_type == 'mysql':
+ pool = mysql_pooling.MySQLConnectionPool(pool_name="mysql_pool", pool_size=20, **self.kwargs)
+ else:
+ from nexios.orm.pool.base import PoolConfig
+ from nexios.orm.pool.connection_pool import ConnectionPool
+ from nexios.orm.dbapi.mysql.mysql_connector_ import MySQLConnectorConnection
+
+ config = PoolConfig(min_size=5, max_size=20)
+ conn = cast(mysql.connector.connection.MySQLConnection, mysql.connector.connect(**self.kwargs))
+
+ pool = ConnectionPool(lambda: MySQLConnectorConnection(conn), config) # type: ignore
+
+ metrics = {
+ 'throughput': [],
+ 'latency': [],
+ 'active_connections': [],
+ 'timestamp': []
+ }
+
+ stop_event = threading.Event()
+
+ def monitor_worker():
+ while not stop_event.is_set():
+ if hasattr(pool, 'get_stats'):
+ stats = pool.get_stats() # type: ignore
+ metrics['active_connections'].append(stats.get('in_use_connections', 0))
+ time.sleep(1)
+
+ def load_worker(worker_id: int):
+ worker_ops = 0
+ worker_times = []
+
+ while not stop_event.is_set():
+ start_time = time.perf_counter()
+ try:
+ if pool_type == 'mysql':
+ conn = pool.get_connection()
+ cur = conn.cursor()
+ cur.execute("SELECT 1")
+ cur.fetchone()
+ else:
+ conn = pool.get_connection()
+ cur = conn.cursor()
+ cur.execute("SELECT 1")
+ cur.fetchone()
+
+ worker_times.append(time.perf_counter() - start_time)
+ worker_ops += 1
+
+ except Exception:
+ pass
+
+ return worker_ops, worker_times
+
+ # Start monitoring
+ monitor_thread = threading.Thread(target=monitor_worker, daemon=True)
+ monitor_thread.start()
+
+ # Ramp up load
+ concurrency_levels = [10, 25, 50, 75, 100, 50, 25] # Varying load
+ current_concurrency = 0
+
+ start_time = time.perf_counter()
+ workers = []
+
+ for target_concurrency in concurrency_levels:
+ phase_duration = duration // len(concurrency_levels)
+ phase_end = time.perf_counter() + phase_duration
+
+ # Adjust concurrency
+ if target_concurrency > current_concurrency:
+ # Add workers
+ for i in range(current_concurrency, target_concurrency):
+ t = threading.Thread(target=load_worker, args=(i,), daemon=True)
+ t.start()
+ workers.append(t)
+ else:
+ # Let natural completion reduce workers
+ workers = workers[:target_concurrency]
+
+ current_concurrency = target_concurrency
+
+ # Collect metrics during this phase
+ while time.perf_counter() < phase_end:
+ time.sleep(1)
+ if hasattr(pool, 'get_stats'):
+ stats = pool.get_stats() # type: ignore
+ metrics['throughput'].append(stats.get('current_throughput', 0))
+ metrics['latency'].append(stats.get('avg_operation_time', 0) * 1000)
+ metrics['timestamp'].append(time.perf_counter() - start_time)
+
+ stop_event.set()
+
+ if pool_type == 'mysql':
+ conn = pool.get_connection()
+ conn.close()
+ else:
+ pool.close() # type: ignore
+
+ return metrics
+
+ def compare_pools(self, scenarios: Dict[str, Dict[str, int]]):
+ """Compare both pools across different scenarios"""
+ comparison_results = {}
+
+ for scenario_name, params in scenarios.items():
+ print(f"\n=== Scenario: {scenario_name} ===")
+ print(f"Parameters: {params}")
+
+ def mysql_params():
+ kw = params.copy()
+ kw.pop('min_size')
+ kw.update({'pool_size': kw.pop('max_size')})
+ return kw
+
+ mysql_kwargs = mysql_params()
+
+ print(f"Mysql kwargs: {mysql_kwargs}")
+
+ mysql_result = self.benchmark_mysql_pool(**mysql_kwargs)
+ custom_result = self.benchmark_custom_pool(**params)
+
+ comparison = {
+ 'mysql': mysql_result,
+ 'custom': custom_result,
+ 'custom_vs_mysql': {
+ 'throughput_ratio': custom_result['operations_per_second'] / mysql_result['operations_per_second'],
+ 'latency_ratio': custom_result['avg_latency_ms'] / mysql_result['avg_latency_ms'],
+ 'advantage': 'custom' if custom_result['operations_per_second'] > mysql_result['operations_per_second'] else 'mysql'
+ }
+ }
+
+ comparison_results[scenario_name] = comparison
+
+ print(f"Winner: {comparison['custom_vs_mysql']['advantage']}")
+ print(f"Throughput ratio: {comparison['custom_vs_mysql']['throughput_ratio']:.2f}x")
+
+ return comparison_results
+
+ def plot_results(self, comparison_results):
+ """Plot comparison results"""
+ scenarios = list(comparison_results.keys())
+ mysql_throughput = [comparison_results[s]['mysql']['operations_per_second'] for s in scenarios]
+ custom_throughput = [comparison_results[s]['custom']['operations_per_second'] for s in scenarios]
+
+ fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(15, 10))
+
+ # Throughput comparison
+ x = range(len(scenarios))
+ width = 0.35
+ ax1.bar([i - width/2 for i in x], mysql_throughput, width, label='mysql', alpha=0.7)
+ ax1.bar([i + width/2 for i in x], custom_throughput, width, label='Custom', alpha=0.7)
+ ax1.set_xlabel('Scenario')
+ ax1.set_ylabel('Operations/sec')
+ ax1.set_title('Throughput Comparison')
+ ax1.set_xticks(x)
+ ax1.set_xticklabels(scenarios, rotation=45)
+ ax1.legend()
+ ax1.grid(True, alpha=0.3)
+
+ # Latency comparison
+ mysql_latency = [comparison_results[s]['mysql']['avg_latency_ms'] for s in scenarios]
+ custom_latency = [comparison_results[s]['custom']['avg_latency_ms'] for s in scenarios]
+
+ ax2.bar([i - width/2 for i in x], mysql_latency, width, label='mysql', alpha=0.7)
+ ax2.bar([i + width/2 for i in x], custom_latency, width, label='Custom', alpha=0.7)
+ ax2.set_xlabel('Scenario')
+ ax2.set_ylabel('Average Latency (ms)')
+ ax2.set_title('Latency Comparison')
+ ax2.set_xticks(x)
+ ax2.set_xticklabels(scenarios, rotation=45)
+ ax2.legend()
+ ax2.grid(True, alpha=0.3)
+
+ # Performance ratio
+ ratios = [comparison_results[s]['custom_vs_mysql']['throughput_ratio'] for s in scenarios]
+ ax3.bar(x, ratios, color=['green' if r > 1 else 'red' for r in ratios], alpha=0.7)
+ ax3.axhline(y=1, color='black', linestyle='--', alpha=0.5)
+ ax3.set_xlabel('Scenario')
+ ax3.set_ylabel('Custom/mysql Throughput Ratio')
+ ax3.set_title('Performance Ratio (>1 = Custom is better)')
+ ax3.set_xticks(x)
+ ax3.set_xticklabels(scenarios, rotation=45)
+ ax3.grid(True, alpha=0.3)
+
+ # Error comparison
+ mysql_errors = [comparison_results[s]['mysql']['errors'] for s in scenarios]
+ custom_errors = [comparison_results[s]['custom']['errors'] for s in scenarios]
+
+ ax4.bar([i - width/2 for i in x], mysql_errors, width, label='mysql', alpha=0.7)
+ ax4.bar([i + width/2 for i in x], custom_errors, width, label='Custom', alpha=0.7)
+ ax4.set_xlabel('Scenario')
+ ax4.set_ylabel('Errors')
+ ax4.set_title('Error Comparison')
+ ax4.set_xticks(x)
+ ax4.set_xticklabels(scenarios, rotation=45)
+ ax4.legend()
+ ax4.grid(True, alpha=0.3)
+
+ plt.tight_layout()
+ plt.savefig('pool_comparison.png', dpi=300, bbox_inches='tight')
+ plt.show()
+
+ def generate_report(self, comparison_results):
+ """Generate a detailed comparison report"""
+ report = []
+ report.append("Connection Pool Benchmark Report")
+ report.append("=" * 50)
+
+ for scenario, results in comparison_results.items():
+ report.append(f"\nScenario: {scenario}")
+ report.append("-" * 30)
+
+ custom = results['custom']
+ mysql_ = results['mysql']
+ comparison = results['custom_vs_mysql']
+
+ report.append(f"mysql: {mysql_['operations_per_second']:.1f} ops/sec, {mysql_['avg_latency_ms']:.1f}ms latency")
+ report.append(f"Custom: {custom['operations_per_second']:.1f} ops/sec, {custom['avg_latency_ms']:.1f}ms latency")
+ report.append(f"Ratio: {comparison['throughput_ratio']:.2f}x throughput, {comparison['latency_ratio']:.2f}x latency")
+ report.append(f"Winner: {comparison['advantage'].upper()}")
+
+ # Overall summary
+ custom_wins = sum(1 for r in comparison_results.values()
+ if r['custom_vs_mysql']['advantage'] == 'custom')
+ total_scenarios = len(comparison_results)
+
+ report.append("\nOverall Summary:")
+ report.append(f"Custom pool won {custom_wins}/{total_scenarios} scenarios")
+
+ return "\n".join(report)
\ No newline at end of file
diff --git a/nexios/orm/benchmark/postgres.py b/nexios/orm/benchmark/postgres.py
new file mode 100644
index 00000000..130b6ccb
--- /dev/null
+++ b/nexios/orm/benchmark/postgres.py
@@ -0,0 +1,398 @@
+import logging
+import statistics
+import threading
+import time
+from concurrent.futures import ThreadPoolExecutor, as_completed
+from typing import Dict
+
+import matplotlib.pyplot as plt
+import numpy as np
+import psycopg
+from psycopg_pool import ConnectionPool as PsycopgPool
+
+# Disable verbose logging for clean benchmark output
+logging.getLogger().setLevel(logging.ERROR)
+
+class ConnectionPoolBenchmark:
+ """Comprehensive benchmark suite for connection pools"""
+
+ def __init__(self, dsn: str):
+ self.dsn = dsn
+ self.results = {}
+
+ def benchmark_psycopg3_pool(self, min_size: int, max_size: int, operations: int, concurrency: int):
+ """Benchmark psycopg3's built-in connection pool"""
+ print(f"Testing psycopg3 pool: {min_size}-{max_size} connections, {operations} ops, {concurrency} threads")
+
+ pool = PsycopgPool(
+ self.dsn,
+ min_size=min_size,
+ max_size=max_size,
+ timeout=10.0
+ )
+
+ times = []
+ errors = 0
+
+ def worker(worker_id: int):
+ worker_times = []
+ for i in range(operations // concurrency):
+ start_time = time.perf_counter()
+ try:
+ with pool.connection() as conn:
+ with conn.cursor() as cur:
+ cur.execute("SELECT 1") # Small sleep to simulate work
+ cur.fetchone()
+ worker_times.append(time.perf_counter() - start_time)
+ except Exception as e:
+ nonlocal errors
+ errors += 1
+ return worker_times
+
+ # Run benchmark
+ start_total = time.perf_counter()
+ futures = []
+ try:
+ with ThreadPoolExecutor(max_workers=concurrency) as executor:
+ exec = [executor.submit(worker, i) for i in range(concurrency)]
+ futures.extend(exec)
+ # futures = [executor.submit(worker, i) for i in range(concurrency)]
+ for future in as_completed(futures, timeout=60.0):
+ times.extend(future.result())
+ except TimeoutError as e:
+ print(f" WARNING: Benchmark timed out after 60 seconds: {e}")
+ # Collect whatever results we have
+ for future in futures:
+ if future.done():
+ try:
+ times.extend(future.result())
+ except Exception:
+ pass
+
+ end_total = time.perf_counter()
+
+ pool.close()
+
+ total_time = end_total - start_total
+ ops_per_sec = operations / total_time
+
+ self.results['psycopg3'] = {
+ 'total_time': total_time,
+ 'operations_per_second': ops_per_sec,
+ 'avg_latency_ms': statistics.mean(times) * 1000,
+ 'p95_latency_ms': np.percentile(times, 95) * 1000 if times else 0,
+ 'errors': errors,
+ 'min_size': min_size,
+ 'max_size': max_size
+ }
+
+ print(f" Results: {ops_per_sec:.1f} ops/sec, {statistics.mean(times)*1000:.1f}ms avg latency")
+ return self.results['psycopg3']
+
+
+ def benchmark_custom_pool(self, min_size: int, max_size: int, operations: int, concurrency: int):
+ """Benchmark your custom connection pool"""
+ print(f"Testing custom pool: {min_size}-{max_size} connections, {operations} ops, {concurrency} threads")
+
+ from nexios.orm.pool.base import PoolConfig
+ from nexios.orm.pool.connection_pool import ConnectionPool
+
+ config = PoolConfig(
+ min_size=min_size,
+ max_size=max_size,
+ connection_timeout=10.0,
+ health_check_interval=300,
+ max_lifetime=3600,
+ idle_timeout=1800
+ )
+
+ def create_conn():
+ from nexios.orm.dbapi.postgres.psycopg_ import PsycopgConnection
+ conn = psycopg.connect(self.dsn)
+ return PsycopgConnection(conn)
+
+ pool = ConnectionPool(create_conn, config)
+
+ time.sleep(1) # Allow pool to initialize
+
+ times = []
+ errors = 0
+
+ def worker(worker_id: int):
+ worker_times = []
+ for i in range(operations // concurrency):
+ start_time = time.perf_counter()
+ try:
+ with pool.connection() as conn:
+ cur = conn.cursor()
+ cur.execute("SELECT 1")
+ cur.fetchone()
+ worker_times.append(time.perf_counter() - start_time)
+ except Exception as e:
+ nonlocal errors
+ errors += 1
+ return worker_times
+
+ # Run benchmark
+ start_total = time.perf_counter()
+ futures_custom = []
+ try:
+ with ThreadPoolExecutor(max_workers=concurrency) as executor:
+ exec_ = [executor.submit(worker, i) for i in range(concurrency)]
+ futures_custom.extend(exec_)
+ # futures_custom = [executor.submit(worker, i) for i in range(concurrency)]
+ for future in as_completed(futures_custom):
+ times.extend(future.result())
+ except TimeoutError as e:
+ print(f" WARNING: Benchmark timed out after 60 seconds: {e}")
+ # Collect whatever results we have
+ for future in futures_custom:
+ if future.done():
+ try:
+ times.extend(future.result())
+ except:
+ pass
+
+ end_total = time.perf_counter()
+
+ pool.close()
+
+ total_time = end_total - start_total
+ ops_per_sec = operations / total_time
+
+ self.results['custom'] = {
+ 'total_time': total_time,
+ 'operations_per_second': ops_per_sec,
+ 'avg_latency_ms': statistics.mean(times) * 1000,
+ 'p95_latency_ms': np.percentile(times, 95) * 1000 if times else 0,
+ 'errors': errors,
+ 'min_size': min_size,
+ 'max_size': max_size
+ }
+
+ print(f" Results: {ops_per_sec:.1f} ops/sec, {statistics.mean(times)*1000:.1f}ms avg latency")
+ return self.results['custom']
+
+ def benchmark_under_load(self, pool_type: str, duration: int = 60):
+ """Benchmark under sustained load with varying concurrency"""
+ print(f"Testing {pool_type} pool under sustained load for {duration}s")
+
+ if pool_type == 'psycopg3':
+ pool = PsycopgPool(self.dsn, min_size=5, max_size=20, timeout=30.0)
+ else:
+ from nexios.orm.pool.base import PoolConfig
+ from nexios.orm.pool.connection_pool import ConnectionPool
+ from nexios.orm.dbapi.postgres.psycopg_ import PsycopgConnection
+ config = PoolConfig(min_size=5, max_size=20)
+ conn = psycopg.connect(self.dsn)
+
+ pool = ConnectionPool(lambda: PsycopgConnection(conn), config)
+
+ metrics = {
+ 'throughput': [],
+ 'latency': [],
+ 'active_connections': [],
+ 'timestamp': []
+ }
+
+ stop_event = threading.Event()
+
+ def monitor_worker():
+ while not stop_event.is_set():
+ if hasattr(pool, 'get_stats'):
+ stats = pool.get_stats() # type: ignore
+ metrics['active_connections'].append(stats.get('in_use_connections', 0))
+ time.sleep(1)
+
+ def load_worker(worker_id: int):
+ worker_ops = 0
+ worker_times = []
+
+ while not stop_event.is_set():
+ start_time = time.perf_counter()
+ try:
+ if pool_type == 'psycopg3':
+ with pool.connection() as conn:
+ cur = conn.cursor()
+ cur.execute("SELECT 1")
+ cur.fetchone()
+ else:
+ with pool.connection() as conn:
+ cur = conn.cursor()
+ cur.execute("SELECT 1")
+ cur.fetchone()
+
+ worker_times.append(time.perf_counter() - start_time)
+ worker_ops += 1
+
+ except Exception:
+ pass
+
+ return worker_ops, worker_times
+
+ # Start monitoring
+ monitor_thread = threading.Thread(target=monitor_worker, daemon=True)
+ monitor_thread.start()
+
+ # Ramp up load
+ concurrency_levels = [10, 25, 50, 75, 100, 50, 25] # Varying load
+ current_concurrency = 0
+
+ start_time = time.perf_counter()
+ workers = []
+
+ for target_concurrency in concurrency_levels:
+ phase_duration = duration // len(concurrency_levels)
+ phase_end = time.perf_counter() + phase_duration
+
+ # Adjust concurrency
+ if target_concurrency > current_concurrency:
+ # Add workers
+ for i in range(current_concurrency, target_concurrency):
+ t = threading.Thread(target=load_worker, args=(i,), daemon=True)
+ t.start()
+ workers.append(t)
+ else:
+ # Let natural completion reduce workers
+ workers = workers[:target_concurrency]
+
+ current_concurrency = target_concurrency
+
+ # Collect metrics during this phase
+ while time.perf_counter() < phase_end:
+ time.sleep(1)
+ if hasattr(pool, 'get_stats'):
+ stats = pool.get_stats() # type: ignore
+ metrics['throughput'].append(stats.get('current_throughput', 0))
+ metrics['latency'].append(stats.get('avg_operation_time', 0) * 1000)
+ metrics['timestamp'].append(time.perf_counter() - start_time)
+
+ stop_event.set()
+
+ if pool_type == 'psycopg3':
+ pool.close()
+ else:
+ pool.close()
+
+ return metrics
+
+ def compare_pools(self, scenarios: Dict[str, Dict[str, int]]):
+ """Compare both pools across different scenarios"""
+ comparison_results = {}
+
+ for scenario_name, params in scenarios.items():
+ print(f"\n=== Scenario: {scenario_name} ===")
+ print(f"Parameters: {params}")
+
+ psycopg_result = self.benchmark_psycopg3_pool(**params)
+ custom_result = self.benchmark_custom_pool(**params)
+
+ comparison = {
+ 'psycopg3': psycopg_result,
+ 'custom': custom_result,
+ 'custom_vs_psycopg3': {
+ 'throughput_ratio': custom_result['operations_per_second'] / psycopg_result['operations_per_second'],
+ 'latency_ratio': custom_result['avg_latency_ms'] / psycopg_result['avg_latency_ms'],
+ 'advantage': 'custom' if custom_result['operations_per_second'] > psycopg_result['operations_per_second'] else 'psycopg3'
+ }
+ }
+
+ comparison_results[scenario_name] = comparison
+
+ print(f"Winner: {comparison['custom_vs_psycopg3']['advantage']}")
+ print(f"Throughput ratio: {comparison['custom_vs_psycopg3']['throughput_ratio']:.2f}x")
+
+ return comparison_results
+
+ def plot_results(self, comparison_results):
+ """Plot comparison results"""
+ scenarios = list(comparison_results.keys())
+ psycopg_throughput = [comparison_results[s]['psycopg3']['operations_per_second'] for s in scenarios]
+ custom_throughput = [comparison_results[s]['custom']['operations_per_second'] for s in scenarios]
+
+ fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(15, 10))
+
+ # Throughput comparison
+ x = range(len(scenarios))
+ width = 0.35
+ ax1.bar([i - width/2 for i in x], psycopg_throughput, width, label='psycopg3', alpha=0.7)
+ ax1.bar([i + width/2 for i in x], custom_throughput, width, label='Custom', alpha=0.7)
+ ax1.set_xlabel('Scenario')
+ ax1.set_ylabel('Operations/sec')
+ ax1.set_title('Throughput Comparison')
+ ax1.set_xticks(x)
+ ax1.set_xticklabels(scenarios, rotation=45)
+ ax1.legend()
+ ax1.grid(True, alpha=0.3)
+
+ # Latency comparison
+ psycopg_latency = [comparison_results[s]['psycopg3']['avg_latency_ms'] for s in scenarios]
+ custom_latency = [comparison_results[s]['custom']['avg_latency_ms'] for s in scenarios]
+
+ ax2.bar([i - width/2 for i in x], psycopg_latency, width, label='psycopg3', alpha=0.7)
+ ax2.bar([i + width/2 for i in x], custom_latency, width, label='Custom', alpha=0.7)
+ ax2.set_xlabel('Scenario')
+ ax2.set_ylabel('Average Latency (ms)')
+ ax2.set_title('Latency Comparison')
+ ax2.set_xticks(x)
+ ax2.set_xticklabels(scenarios, rotation=45)
+ ax2.legend()
+ ax2.grid(True, alpha=0.3)
+
+ # Performance ratio
+ ratios = [comparison_results[s]['custom_vs_psycopg3']['throughput_ratio'] for s in scenarios]
+ ax3.bar(x, ratios, color=['green' if r > 1 else 'red' for r in ratios], alpha=0.7)
+ ax3.axhline(y=1, color='black', linestyle='--', alpha=0.5)
+ ax3.set_xlabel('Scenario')
+ ax3.set_ylabel('Custom/psycopg3 Throughput Ratio')
+ ax3.set_title('Performance Ratio (>1 = Custom is better)')
+ ax3.set_xticks(x)
+ ax3.set_xticklabels(scenarios, rotation=45)
+ ax3.grid(True, alpha=0.3)
+
+ # Error comparison
+ psycopg_errors = [comparison_results[s]['psycopg3']['errors'] for s in scenarios]
+ custom_errors = [comparison_results[s]['custom']['errors'] for s in scenarios]
+
+ ax4.bar([i - width/2 for i in x], psycopg_errors, width, label='psycopg3', alpha=0.7)
+ ax4.bar([i + width/2 for i in x], custom_errors, width, label='Custom', alpha=0.7)
+ ax4.set_xlabel('Scenario')
+ ax4.set_ylabel('Errors')
+ ax4.set_title('Error Comparison')
+ ax4.set_xticks(x)
+ ax4.set_xticklabels(scenarios, rotation=45)
+ ax4.legend()
+ ax4.grid(True, alpha=0.3)
+
+ plt.tight_layout()
+ plt.savefig('pool_comparison.png', dpi=300, bbox_inches='tight')
+ plt.show()
+
+ def generate_report(self, comparison_results):
+ """Generate a detailed comparison report"""
+ report = []
+ report.append("Connection Pool Benchmark Report")
+ report.append("=" * 50)
+
+ for scenario, results in comparison_results.items():
+ report.append(f"\nScenario: {scenario}")
+ report.append("-" * 30)
+
+ custom = results['custom']
+ psycopg = results['psycopg3']
+ comparison = results['custom_vs_psycopg3']
+
+ report.append(f"psycopg3: {psycopg['operations_per_second']:.1f} ops/sec, {psycopg['avg_latency_ms']:.1f}ms latency")
+ report.append(f"Custom: {custom['operations_per_second']:.1f} ops/sec, {custom['avg_latency_ms']:.1f}ms latency")
+ report.append(f"Ratio: {comparison['throughput_ratio']:.2f}x throughput, {comparison['latency_ratio']:.2f}x latency")
+ report.append(f"Winner: {comparison['advantage'].upper()}")
+
+ # Overall summary
+ custom_wins = sum(1 for r in comparison_results.values()
+ if r['custom_vs_psycopg3']['advantage'] == 'custom')
+ total_scenarios = len(comparison_results)
+
+ report.append(f"\nOverall Summary:")
+ report.append(f"Custom pool won {custom_wins}/{total_scenarios} scenarios")
+
+ return "\n".join(report)
\ No newline at end of file
diff --git a/nexios/orm/config.py b/nexios/orm/config.py
new file mode 100644
index 00000000..962415be
--- /dev/null
+++ b/nexios/orm/config.py
@@ -0,0 +1,1125 @@
+from __future__ import annotations
+
+import importlib
+import importlib.util
+import inspect
+import uuid
+from abc import ABC, abstractmethod
+from datetime import date, datetime, time, timedelta, timezone
+from decimal import Decimal
+from enum import Enum, StrEnum
+from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network
+from pathlib import Path
+from typing import (
+ Annotated,
+ Any,
+ Dict,
+ List,
+ Optional,
+ Tuple,
+ Type,
+ Union,
+ get_args,
+ get_origin,
+ Callable,
+)
+from urllib.parse import urlparse
+from uuid import UUID
+
+from pydantic_core import PydanticUndefined as Undefined
+from nexios.orm.fields import FieldInfo
+from nexios.orm.model import NexiosModel
+from nexios.orm.utils import InstanceOrType
+from nexios.orm.query.expressions import ColumnExpression
+
+
+class PostgreSQLDriver(StrEnum):
+ ASYNCPG = "asyncpg"
+ PG8000 = "pg8000"
+ PSYCOPG3 = "psycopg3"
+ PSYCOPG3_ASYNC = "psycopg3_async"
+ AIOPG = "aiopg"
+
+
+class MySQLDriver(StrEnum):
+ MYSQL_CONNECTOR = "mysql-connector"
+ PYMySQL = "pymysql"
+ AIOMYSQL = "aiomysql"
+ ASYNCMY = "asyncmy"
+ MARIADB = "mariadb"
+ MYSQL_CLIENT = "mysqlclient"
+
+
+class SQLiteDriver(StrEnum):
+ AIOSQLITE = "aiosqlite"
+ SQLITE3 = "sqlite3"
+ APSW = "apsw"
+
+
+class Dialect(ABC):
+ """Base class for database dialects"""
+
+ @abstractmethod
+ def get_type_mapping(
+ self,
+ max_digits: Optional[int] = None,
+ precision: Optional[int] = None,
+ scale: Optional[int] = None,
+ ) -> Dict[type, str]: ...
+
+ @abstractmethod
+ def auto_increment_keyword(self) -> str: ...
+
+ @abstractmethod
+ def quote_identifier(self, identifier: str) -> str: ...
+
+ @abstractmethod
+ def format_datetime_default(self, dt: datetime) -> str: ...
+
+ @abstractmethod
+ def current_timestamp(self) -> str: ...
+
+ @abstractmethod
+ def current_date(self) -> str: ...
+
+ @abstractmethod
+ def generate_uuid(self) -> str: ...
+
+ @abstractmethod
+ def get_limit_offset_sql(
+ self, limit: Optional[int], offset: Optional[int]
+ ) -> str: ...
+
+
+class SQLiteDialect(Dialect):
+ def current_timestamp(self) -> str:
+ return "(DATETIME('now'))"
+
+ def current_date(self) -> str:
+ return "(DATE('now'))"
+
+ def generate_uuid(self) -> str:
+ return f"'{uuid.uuid4()}'"
+
+ def format_datetime_default(self, dt: datetime) -> str:
+ return f"'{dt.isoformat()}'"
+
+ def get_type_mapping(
+ self,
+ max_digits: Optional[int] = None,
+ precision: Optional[int] = None,
+ scale: Optional[int] = None,
+ ) -> Dict[type, str]:
+ return {
+ int: "INTEGER",
+ str: "TEXT",
+ float: "REAL",
+ bool: "INTEGER",
+ bytes: "BLOB",
+ datetime: "TEXT",
+ date: "TEXT",
+ time: "TEXT",
+ timedelta: "INTEGER",
+ Decimal: "NUMERIC",
+ UUID: "TEXT",
+ dict: "TEXT",
+ list: "TEXT",
+ Enum: "TEXT",
+ object: "BLOB",
+ }
+
+ def auto_increment_keyword(self) -> str:
+ return "AUTOINCREMENT"
+
+ def quote_identifier(self, identifier: str) -> str:
+ return f'"{identifier}"'
+
+ def get_limit_offset_sql(self, limit: int | None, offset: int | None) -> str:
+ if limit is not None and offset is not None:
+ return f"LIMIT {limit} OFFSET {offset}"
+ elif limit is not None:
+ return f"LIMIT {limit}"
+ elif offset is not None:
+ return f"LIMIT -1 OFFSET {offset}"
+ else:
+ return ""
+
+
+class PostgreSQLDialect(Dialect):
+ def current_timestamp(self) -> str:
+ return "CURRENT_TIMESTAMP"
+
+ def format_datetime_default(self, dt: datetime) -> str:
+ return f"'{dt.isoformat()}'::TIMESTAMP"
+
+ def current_date(self) -> str:
+ return "CURRENT_DATE"
+
+ def generate_uuid(self) -> str:
+ return "gen_random_uuid()"
+
+ def get_type_mapping(
+ self,
+ max_digits: Optional[int] = None,
+ precision: Optional[int] = None,
+ scale: Optional[int] = None,
+ ) -> Dict[type, str]:
+ string_type = (
+ f"VARCHAR({max_digits})" if max_digits and max_digits <= 255 else "TEXT"
+ )
+ return {
+ int: "BIGINT",
+ str: string_type,
+ Path: "TEXT",
+ float: "DOUBLE PRECISION",
+ bool: "BOOLEAN",
+ bytes: "BYTEA",
+ datetime: "TIMESTAMP",
+ date: "DATE",
+ time: "TIME",
+ timedelta: "INTERVAL",
+ Decimal: f"NUMERIC({precision}, {scale})",
+ UUID: "UUID",
+ IPv4Address: "INET",
+ IPv6Address: "INET",
+ IPv4Network: "INET",
+ IPv6Network: "INET",
+ dict: "JSONB",
+ list: "JSONB",
+ Enum: "TEXT",
+ object: "BYTEA",
+ }
+
+ def auto_increment_keyword(self) -> str:
+ return "GENERATED BY DEFAULT AS IDENTITY"
+
+ def quote_identifier(self, identifier: str) -> str:
+ return f'"{identifier}"'
+
+ def get_limit_offset_sql(self, limit: int | None, offset: int | None) -> str:
+ parts = []
+ if limit is not None:
+ parts.append(f"LIMIT {limit}")
+ if offset is not None:
+ parts.append(f"OFFSET {offset}")
+ return " ".join(parts)
+
+
+class MySQLDialect(Dialect):
+ def format_datetime_default(self, dt: datetime) -> str:
+ return f"'{dt.strftime('%Y-%m-%d %H:%M:%S')}'"
+
+ def current_timestamp(self) -> str:
+ return "CURRENT_TIMESTAMP"
+
+ def current_date(self) -> str:
+ return "CURRENT()"
+
+ def generate_uuid(self) -> str:
+ return "UUID()"
+
+ def get_type_mapping(
+ self,
+ max_digits: Optional[int] = None,
+ precision: Optional[int] = None,
+ scale: Optional[int] = None,
+ ) -> Dict[type, str]:
+ def resolve_str() -> str:
+ if max_digits:
+ if max_digits <= 255:
+ return f"VARCHAR({max_digits})"
+ elif max_digits <= 65535:
+ return "TEXT"
+ elif max_digits <= 16777215:
+ return "MEDIUMTEXT"
+ else:
+ return "LONGTEXT"
+ else:
+ return "'TEXT"
+
+ string_type = resolve_str()
+
+ return {
+ int: "BIGINT",
+ str: string_type,
+ float: "DOUBLE",
+ bool: "BOOLEAN",
+ bytes: "BLOB",
+ datetime: "DATETIME",
+ date: "DATE",
+ time: "TIME",
+ timedelta: "BIGINT",
+ Decimal: f"DECIMAL({precision}, {scale})",
+ UUID: "CHAR(36)",
+ IPv4Address: "VARCHAR(45)",
+ IPv6Address: "VARCHAR(45)",
+ IPv4Network: "VARCHAR(45)",
+ IPv6Network: "VARCHAR(45)",
+ dict: "JSON",
+ list: "JSON",
+ Enum: "VARCHAR(255)",
+ object: "BLOB",
+ }
+
+ def auto_increment_keyword(self) -> str:
+ return "AUTO_INCREMENT"
+
+ def quote_identifier(self, identifier: str) -> str:
+ return f"`{identifier}`"
+
+ def get_limit_offset_sql(self, limit: int | None, offset: int | None) -> str:
+ if limit is None and offset is None:
+ return ""
+
+ if limit is not None and offset is not None:
+ return f"LIMIT {limit} OFFSET {offset}"
+ elif limit is not None:
+ return f"LIMIT {limit}"
+ elif offset is not None:
+ # Only offset - MySQL requires LIMIT
+ # Use a very large number as "unlimited"
+ # 18446744073709551615 is max for BIGINT UNSIGNED in MySQL
+ return f"LIMIT 18446744073709551615 OFFSET {offset}"
+
+ return ""
+
+
+def is_true(attr_value):
+ return attr_value is not Undefined and bool(attr_value)
+
+def _normalize_driver(driver: Any):
+ if isinstance(driver, StrEnum):
+ return driver
+
+ if isinstance(driver, str):
+ low = driver.lower()
+ for enum in (PostgreSQLDriver, MySQLDriver, SQLiteDriver):
+ for member in enum:
+ if low in (member.value, member.name.lower()):
+ return member
+
+ return driver
+
+def get_param_placeholder(
+ driver: str, index: int = 1, name: Optional[str] = None
+) -> str:
+ """
+ Returns the correct placeholder syntax based on the driver's paramstyle.
+ Args:
+ driver: Database driver (e.g., 'postgres', 'sqlite')
+ index: The 1-based position for numeric/dollar styles ($1, :1)
+ name: Named parameter name (optional)
+ Returns:
+ Placeholder syntax based on the driver's paramstyle
+ """
+ if isinstance(driver, PostgreSQLDriver):
+ if driver == PostgreSQLDriver.ASYNCPG:
+ if name:
+ raise ValueError("asyncpg doesn't support named parameters")
+ if index is None:
+ raise ValueError(f"{driver.value} requires index for positional params")
+ return f"${index}"
+ elif driver == PostgreSQLDriver.PG8000:
+ if name:
+ return f":{name}"
+ return "%s"
+ elif driver == PostgreSQLDriver.PSYCOPG3:
+ if name:
+ return f"%({name})s"
+ return "%s"
+ else:
+ return "%s"
+ elif isinstance(driver, MySQLDriver):
+ if name:
+ return f"%({name})s"
+ return "%s"
+ elif isinstance(driver, SQLiteDriver):
+ if name:
+ return f":{name}"
+ return "?"
+ else:
+ return "?"
+
+
+def generate_placeholders(driver: str, count, start_index: int = 1):
+ driver = _normalize_driver(driver)
+ placeholders = []
+ for i in range(count):
+ placeholders.append(get_param_placeholder(driver=driver, index=start_index + i))
+ return ", ".join(placeholders)
+
+
+class DDLGenerator:
+ """Generate DDL statements from ORM model"""
+
+ def __init__(self, dialect: Dialect, driver: str) -> None:
+ self.dialect = dialect or SQLiteDialect()
+ self.driver = _normalize_driver(driver or SQLiteDriver.SQLITE3)
+
+ def _get_tablename(self, model_class: InstanceOrType[NexiosModel]) -> str:
+ if not isinstance(model_class, type):
+ model_class = model_class.__class__
+
+ tbname = getattr(model_class, "__tablename__", None)
+
+ if tbname is None and hasattr(model_class, "__tablename__"):
+ tbname = model_class.__tablename__
+
+ return tbname if tbname else ""
+
+ def _get_primary_key(self, model_class: NexiosModel) -> Any:
+ return model_class.get_primary_key()
+
+ def create_table(self, model_class: Type[NexiosModel]) -> str:
+ """Generate CREATE TABLE statements"""
+ table_name = self._get_tablename(model_class)
+ columns = self._get_column_definitions(model_class)
+ constraints = self._get_table_constraints(model_class)
+
+ column_defs = ",\n ".join(columns + constraints)
+
+ return f"CREATE TABLE IF NOT EXISTS {self.dialect.quote_identifier(table_name)} (\n {column_defs}\n);"
+
+ def delete(self, model_class: NexiosModel) -> tuple[str, tuple[Any, ...]]:
+ """Delete statement
+ model_class: Instance of the BaseModel
+ name: is any field value that matches the condition
+ """
+ from nexios.orm.query.expressions import ColumnExpression, BinaryExpression
+
+ tablename = self._get_tablename(model_class)
+ primary_key = self._get_primary_key(model_class)
+
+ if isinstance(primary_key, ColumnExpression):
+ primary_key_field = primary_key.field_name
+ else:
+ primary_key_field = primary_key
+
+ primary_key_value = getattr(model_class, primary_key_field, None)
+
+ if primary_key_value is None:
+ raise ValueError(
+ f"Cannot delete {model_class.__name__} with null primary key"
+ )
+
+ col_expr = ColumnExpression(model_class.__class__, primary_key_field)
+ condition = BinaryExpression(col_expr, "=", primary_key_value)
+ sql_condition, params = condition.to_sql(get_param_placeholder(self.driver))
+ sql = f"DELETE FROM {self.dialect.quote_identifier(tablename)} WHERE {sql_condition}"
+ return sql, tuple(params)
+
+ def upsert(self, model_class: NexiosModel) -> Tuple[str, tuple]:
+ """Insert or update statement"""
+ insert_sql, params, returning_clause = self._insert(model_class)
+ update_clause = self._update(model_class)
+
+ if update_clause:
+ sql = insert_sql + " " + update_clause + returning_clause
+ else:
+ sql = insert_sql + returning_clause
+
+ return sql, params
+
+ def drop_table(self, model_class: Type[NexiosModel]) -> str:
+ """Generate DROP TABLE statement"""
+ table_name = self._get_tablename(model_class)
+ return f"DROP TABLE IF EXISTS {self.dialect.quote_identifier(table_name)};"
+
+ def create_indexes(self, model_class: Type[NexiosModel]) -> List[str]:
+ """Generate CREATE INDEX statements"""
+ indexes = []
+ table_name = self._get_tablename(model_class)
+
+ def _get_search_sql() -> str:
+ """MySQL uses CREATE FULLTEXT INDEX, postgres appends GIN and SQLite requires creation of virtual table"""
+ return f"USING GIN(to_tsvector('english', {table_name}))"
+
+ for field_name, field_info in model_class.get_fields().items():
+ index = getattr(field_info, "index", Undefined)
+
+ if isinstance(index, ColumnExpression):
+ index_field = index.field_name
+ else:
+ index_field = index
+
+ if is_true(index_field) and not field_info.unique:
+ index_name = f"idx_{table_name}_{field_name}"
+ indexes.append(
+ f"CREATE INDEX {self.dialect.quote_identifier(index_name)} "
+ f"ON {self.dialect.quote_identifier(table_name)} "
+ f"({self.dialect.quote_identifier(field_name)})"
+ )
+
+ return indexes
+
+ def _insert(self, model_instance: NexiosModel) -> tuple[str, tuple[Any, ...], str]:
+ """Generate INSERT statement"""
+ from nexios.orm.query.expressions import ColumnExpression
+
+ model_class = model_instance.__class__
+ fields_to_save: Dict[str, Any] = {}
+ tablename = self._get_tablename(model_class)
+ fields = model_instance.get_fields()
+
+ primary_key = self._get_primary_key(model_instance)
+ if isinstance(primary_key, ColumnExpression):
+ primary_key_field = primary_key.field_name
+ else:
+ primary_key_field = primary_key
+
+ for field_name, field_info in fields.items():
+ value = getattr(model_instance, field_name, None)
+
+ auto_increment = getattr(field_info, "auto_increment", False)
+ is_primary_key = is_true(primary_key)
+
+ if is_primary_key and auto_increment:
+ if value is None or isinstance(value, int) and value <= 0:
+ continue # Let database generate the auto_increment
+
+ else:
+ fields_to_save[field_name] = value
+ continue
+
+ if value is not None:
+ fields_to_save[field_name] = value
+
+ field_names = list(fields_to_save.keys())
+ field_count = len(field_names)
+
+ placeholders = generate_placeholders(self.driver, field_count, 1)
+ field_names_str = ", ".join(field_names)
+ sql = f"INSERT INTO {self.dialect.quote_identifier(tablename)} ({field_names_str}) VALUES ({placeholders})"
+
+ field_info = fields.get(primary_key_field)
+ auto_increment = getattr(field_info, "auto_increment", False)
+
+ returning_clause = ""
+
+ if auto_increment and isinstance(self.dialect, PostgreSQLDialect):
+ returning_clause = " RETURNING " + self.dialect.quote_identifier(
+ primary_key_field
+ )
+
+ params = tuple(getattr(model_instance, fname) for fname in field_names)
+
+ return sql, params, returning_clause
+
+ def _update(self, model_instance: NexiosModel):
+ fields = model_instance.get_fields()
+ primary_key = self._get_primary_key(model_instance)
+
+ fields_to_update = []
+
+ for field_name in fields.keys():
+ if isinstance(primary_key, ColumnExpression):
+ is_pk = field_name == primary_key.field_name
+ else:
+ is_pk = field_name == primary_key
+
+ # is_primary_key = is_true(primary_key)
+ if not is_pk:
+ fields_to_update.append(field_name)
+
+ if isinstance(self.dialect, PostgreSQLDialect):
+ update_parts = []
+ for field in fields_to_update:
+ update_parts.append(f"{field} = excluded.{field}")
+ if update_parts:
+ return f"ON CONFLICT ({primary_key}) DO UPDATE SET {','.join(update_parts)}"
+ return ""
+
+ elif isinstance(self.dialect, MySQLDialect):
+ update_parts = []
+
+ for field in fields_to_update:
+ update_parts.append(f"{field} = VALUES({field})")
+
+ if update_parts:
+ return f"ON DUPLICATE KEY UPDATE {', '.join(update_parts)}"
+
+ return ""
+
+ else:
+ # SQLite: field = excluded.field (same as PostgreSQL syntax)
+ update_parts = []
+ for field in fields_to_update:
+ update_parts.append(f"{field} = excluded.{field}")
+ if update_parts:
+ return f"ON CONFLICT ({primary_key}) DO UPDATE SET {', '.join(update_parts)}"
+ return ""
+
+ def _get_column_definitions(self, model_class: Type[NexiosModel]) -> List[str]:
+ """Generate column definitions for CREATE TABLE"""
+ columns = []
+ fields = model_class.get_fields()
+
+ for field_name, field_info in fields.items():
+ column_def = self._build_column_definition(
+ field_name, field_info, model_class
+ )
+ columns.append(column_def)
+
+ return columns
+
+ def _build_column_definition(
+ self, field_name: str, field_info: FieldInfo, model_class: Type[NexiosModel]
+ ) -> str:
+ """Build a single column definition"""
+ max_digits = getattr(field_info, "max_length", 255)
+ precision = getattr(field_info, "precision", 0) # precision is not in FieldInfo
+ scale = getattr(field_info, "decimal_places", 1)
+ parts = [self.dialect.quote_identifier(field_name)]
+
+ field_type = self._get_field_type(model_class, field_name)
+ db_type = self._map_python_type(field_type, max_digits, precision, scale)
+
+ parts.append(db_type)
+
+ is_primary_key = is_true(getattr(field_info, "primary_key", Undefined))
+
+ if is_primary_key:
+ parts.append("PRIMARY KEY")
+
+ if field_info.auto_increment and field_info.primary_key:
+ if db_type.upper() in ("INTEGER", "INT", "BIGINT", "SMALLINT"):
+ parts.append(self.dialect.auto_increment_keyword())
+ elif db_type.upper() in ("SERIAL", "BIGSERIAL"):
+ # serial types are already auto-incremented in postgres
+ pass
+
+ nullable = getattr(field_info, "nullable", Undefined)
+ default = getattr(field_info, "default", Undefined)
+ default_factory = getattr(field_info, "default_factory", None)
+ auto_inc = is_true(getattr(field_info, "auto_increment", False))
+
+ if is_primary_key:
+ parts.append("NOT NULL")
+ elif nullable is True:
+ pass
+ elif nullable is False:
+ parts.append("NOT NULL")
+ elif nullable is Undefined:
+ has_default = default is not Undefined or default_factory is not None
+ if not has_default and not auto_inc:
+ parts.append("NOT NULL")
+
+ unique = getattr(field_info, "unique", Undefined)
+ if is_true(unique) and not is_primary_key:
+ parts.append("UNIQUE")
+
+ default_value = self._get_default_value(model_class, field_name)
+ has_default = default is not Undefined or default_factory is not None
+ if (
+ default_value is not None
+ and not is_primary_key
+ and not auto_inc
+ and has_default
+ ):
+ parts.append(f"DEFAULT {default_value}")
+
+ return " ".join(parts)
+
+ def _get_field_type(self, model_class: Type[NexiosModel], field_name: str) -> type:
+ """Get the python type for a field"""
+ annotation = model_class.__annotations__.get(field_name, str)
+
+ while True:
+ origin = get_origin(annotation)
+
+ # Annotated[T, ...]
+ if origin is Annotated:
+ annotation = get_args(annotation)[0]
+ continue
+
+ # Optional / Union[T, None]
+ if origin is Union:
+ args = [arg for arg in get_args(annotation) if arg is not type(None)]
+ annotation = args[0] if args else str
+ continue
+
+ break
+
+ return annotation
+
+ def _map_python_type(
+ self,
+ python_type: type,
+ max_digits: Optional[int] = None,
+ precision: Optional[int] = None,
+ scale: Optional[int] = None,
+ ) -> str:
+ """Map python type to database type"""
+ type_mapping = self.dialect.get_type_mapping(max_digits, precision, scale)
+
+ if python_type in type_mapping:
+ return type_mapping[python_type]
+
+ # Enum subclasses
+ if inspect.isclass(python_type) and issubclass(python_type, Enum):
+ return type_mapping[Enum]
+
+ return type_mapping.get(object, "TEXT")
+
+ def _get_default_value(
+ self, model_class: Type[NexiosModel], field_name: str
+ ) -> Optional[str]:
+ """Get SQL default value for a field"""
+ field = model_class.get_fields().get(field_name)
+
+ if not field:
+ return None
+
+ default = getattr(field, "default", Undefined)
+ default_factory = getattr(field, "default_factory", None)
+
+ if default is not Undefined:
+ return self._format_default_value(default)
+ elif default_factory is not None:
+ return self._format_default_factory(default_factory)
+ return None
+
+ def _format_default_value(self, value: Any):
+ if value is None:
+ return "NULL"
+
+ if isinstance(value, str):
+ escaped = value.replace("'", "''")
+ return f"'{escaped}'"
+
+ if isinstance(value, Enum):
+ return f"'{value.value}'"
+
+ if isinstance(value, bool):
+ return "TRUE" if value else "FALSE"
+
+ if isinstance(value, (dict, list)):
+ import json
+
+ return f"'{json.dumps(value)}'"
+
+ if isinstance(value, (int, float, Decimal)):
+ return str(value)
+
+ if isinstance(value, UUID):
+ return f"'{value}'"
+
+ if isinstance(value, (datetime, date, time)):
+ if isinstance(value, datetime):
+ return self.dialect.format_datetime_default(value)
+ elif isinstance(value, date):
+ return self.dialect.current_date()
+ elif isinstance(value, time):
+ return self.dialect.current_timestamp()
+ escaped = str(value).replace("'", "''")
+ return f"'{escaped}'"
+
+ def _format_default_factory(self, factory: Callable):
+ def utcnow():
+ return datetime.now(timezone.utc)
+
+ if factory is datetime.now or factory is utcnow:
+ return self.dialect.current_timestamp()
+ if factory is date.today:
+ return self.dialect.current_date()
+ if factory is UUID:
+ return self.dialect.generate_uuid()
+ try:
+ result = factory()
+ return self._format_default_value(result)
+ except TypeError:
+ return "NULL"
+
+ def _get_table_constraints(self, model_class: Type[NexiosModel]) -> List[str]:
+ """Generate FOREIGN KEY constraints from FIELD definitions only"""
+ constraints: List[str] = []
+
+ # Get all fields of this model
+ fields = model_class.get_fields()
+
+ for field_name, field_info in fields.items():
+ # Check if this field has a foreign key attribute
+ foreign_key = getattr(field_info, "foreign_key", Undefined)
+ if foreign_key is Undefined or not foreign_key:
+ continue # This field is not a foreign key
+
+ print(
+ f"DEBUG: Generating FK constraint for {model_class.__name__}.{field_name} -> {foreign_key}"
+ )
+
+ # Parse foreign key reference (e.g., "User.id" or "users.id")
+ if "." not in foreign_key: # type: ignore
+ print(f"ERROR: Invalid foreign key format: {foreign_key}")
+ continue
+
+ ref_model_name, ref_column = foreign_key.split(".") # type: ignore
+ ref_model_name = ref_model_name.lower() # "User" -> "user"
+ ref_column = ref_column.lower() # "id"
+
+ # Get the referenced table name
+ # We need to map model name to table name
+ ref_table_name = self._model_name_to_table_name(ref_model_name, model_class)
+
+ if not ref_table_name:
+ # Fallback: simple pluralization
+ ref_table_name = f"{ref_model_name}s"
+ print(f"WARNING: Using fallback table name: {ref_table_name}")
+
+ # Generate constraint
+ constraint_name = f"fk_{model_class.__tablename__}_{field_name}"
+
+ sql_parts = [
+ f"CONSTRAINT {self.dialect.quote_identifier(constraint_name)}",
+ f"FOREIGN KEY ({self.dialect.quote_identifier(field_name)})",
+ f"REFERENCES {self.dialect.quote_identifier(ref_table_name)}({self.dialect.quote_identifier(ref_column)})",
+ ]
+
+ # Add ON DELETE/UPDATE if specified
+ ondelete = getattr(field_info, "ondelete", Undefined)
+ if ondelete is not Undefined and ondelete:
+ sql_parts.append(f"ON DELETE {ondelete}")
+
+ onupdate = getattr(field_info, "onupdate", Undefined)
+ if onupdate is not Undefined and onupdate:
+ sql_parts.append(f"ON UPDATE {onupdate}")
+
+ constraints.append(" ".join(sql_parts))
+
+ return constraints
+
+ def _model_name_to_table_name(
+ self, model_name_lower: str, source_model: Type[NexiosModel]
+ ) -> Optional[str]:
+ """Convert a model name to its table name"""
+ # Look in the model registry
+ if hasattr(source_model, "__registry__"):
+ registry = source_model.__registry__
+ for model_cls in registry.values():
+ if model_cls.__name__.lower() == model_name_lower:
+ return model_cls.__tablename__
+
+ # Also check if we have the model directly
+ if hasattr(source_model, "__relationships__"):
+ for rel_info in source_model.__relationships__.values():
+ if (
+ rel_info.related_model_name
+ and rel_info.related_model_name.lower() == model_name_lower
+ ):
+ # Try to get the actual model
+ try:
+ model_cls = source_model.__registry__.get(
+ rel_info.related_model_name
+ )
+ # model_cls = registry.get(rel_info.related_model_name)
+ if model_cls:
+ return model_cls.__tablename__
+ except Exception:
+ pass
+
+ return None
+
+
+DRIVER_REGISTRY = {
+ "postgresql": {
+ True: [
+ PostgreSQLDriver.PSYCOPG3,
+ PostgreSQLDriver.ASYNCPG,
+ PostgreSQLDriver.AIOPG,
+ ],
+ False: [PostgreSQLDriver.PSYCOPG3, PostgreSQLDriver.PG8000],
+ },
+ "mysql": {
+ True: [MySQLDriver.AIOMYSQL, MySQLDriver.ASYNCMY],
+ False: [
+ MySQLDriver.MYSQL_CONNECTOR,
+ MySQLDriver.PYMySQL,
+ MySQLDriver.MYSQL_CLIENT,
+ MySQLDriver.MARIADB,
+ ],
+ },
+ "sqlite": {
+ True: [SQLiteDriver.AIOSQLITE],
+ False: [SQLiteDriver.SQLITE3, SQLiteDriver.APSW],
+ },
+}
+
+DIALECT_MAP = {
+ "postgresql": PostgreSQLDialect,
+ "postgres": PostgreSQLDialect,
+ "mysql": MySQLDialect,
+ "mariadb": MySQLDialect,
+ "sqlite": SQLiteDialect,
+}
+
+
+class DatabaseDetector:
+ """Automatically detects database type and driver from connection parameters."""
+
+ @staticmethod
+ def __is_installed(module_name: str) -> bool:
+ return importlib.util.find_spec(module_name) is not None
+
+ @staticmethod
+ def _find_installed(candidates: List[Tuple[Any, List[str]]], error_message: str):
+ for driver_enum, packages in candidates:
+ for package in packages:
+ if DatabaseDetector.__is_installed(package):
+ return driver_enum
+ raise ImportError(error_message)
+
+ @staticmethod
+ def detect_from_url(
+ url: str, is_async: bool = False
+ ) -> Tuple[Dialect, str, Dict[str, Any]]:
+ """Detect database type from connection URL."""
+ parsed = urlparse(url)
+ scheme = parsed.scheme.lower()
+
+ def parse_query(q: str) -> Dict[str, Any]:
+ from urllib.parse import parse_qs
+
+ values = {}
+ for k, v in parse_qs(q).items():
+ item = v[0] if len(v) == 1 else v
+ if isinstance(item, str):
+ if item.isdigit():
+ item = int(item)
+ elif item.replace(".", "").isdigit() and item.count(".") == 1:
+ item = float(item)
+ elif item.lower() in ("true", "false"):
+ item = item.lower() == "true"
+ elif item.lower() in ("none", "null"):
+ item = None
+ values[k] = item
+ return values
+
+ scheme = scheme.split("+")[-1]
+
+ kwargs = {
+ "host": parsed.hostname or "localhost",
+ "port": parsed.port,
+ "database": parsed.path.lstrip("/"),
+ "user": parsed.username,
+ "password": parsed.password,
+ }
+
+ if scheme == "sqlite":
+ db_type = SQLiteDialect()
+ driver = DatabaseDetector._detect_sqlite_driver(is_async)
+ database_path = parsed.path.lstrip("/") or ":memory:"
+ kwargs = {"database": database_path}
+
+ elif scheme in ["postgres", "postgresql"]:
+ db_type = PostgreSQLDialect()
+ driver = DatabaseDetector._detect_postgres_driver(is_async)
+ kwargs["port"] = kwargs["port"] or 5432
+ if driver != PostgreSQLDriver.ASYNCPG:
+ kwargs["dbname"] = kwargs.pop("database")
+
+ elif scheme in ["mysql", "mariadb"]:
+ db_type = MySQLDialect()
+ driver = DatabaseDetector._detect_mysql_driver(is_async)
+ kwargs["port"] = kwargs["port"] or 3306
+
+ else:
+ raise ValueError(f"Unsupported database URL scheme: {scheme}")
+
+ if parsed.query:
+ kwargs.update(parse_query(parsed.query))
+
+ clean_kwargs = {k: v for k, v in kwargs.items() if v is not None}
+
+ return db_type, driver, clean_kwargs
+
+ @staticmethod
+ def detect_from_kwargs(
+ kwargs: Dict[str, Any], is_async: bool = False
+ ) -> Tuple[Dialect, Any]:
+ """Detect database type from connection kwargs."""
+ if "driver" in kwargs:
+ driver = kwargs["driver"]
+ if driver in [d.value for d in PostgreSQLDriver]:
+ return PostgreSQLDialect(), driver
+ elif driver in [d.value for d in MySQLDriver]:
+ return MySQLDialect(), driver
+ elif driver in [d.value for d in SQLiteDriver]:
+ return SQLiteDialect(), driver
+ else:
+ raise ValueError(f"Unsupported driver: {driver}")
+
+ if "dialect" in kwargs:
+ dialect = kwargs["dialect"]
+ if isinstance(dialect, Dialect):
+ if isinstance(dialect, SQLiteDialect):
+ return dialect, DatabaseDetector._detect_sqlite_driver(is_async)
+ elif isinstance(dialect, PostgreSQLDialect):
+ return dialect, DatabaseDetector._detect_postgres_driver(is_async)
+ elif isinstance(dialect, MySQLDialect):
+ return dialect, DatabaseDetector._detect_mysql_driver(is_async)
+ if isinstance(dialect, str):
+ d = dialect.lower()
+ if d in ("sqlite", "sqlite3"):
+ return SQLiteDialect(), DatabaseDetector._detect_sqlite_driver(
+ is_async
+ )
+ if d in ("postgres", "postgresql"):
+ return (
+ PostgreSQLDialect(),
+ DatabaseDetector._detect_postgres_driver(is_async),
+ )
+ if d in ("mysql", "mariadb"):
+ return MySQLDialect(), DatabaseDetector._detect_mysql_driver(
+ is_async
+ )
+ raise ValueError(f"Unsupported dialect: {dialect}")
+
+ database_value = kwargs.get("database", "")
+ if isinstance(database_value, str):
+ if (
+ database_value == ":memory:"
+ or database_value.startswith("file:")
+ or database_value.endswith(".db")
+ or database_value.endswith(".sqlite")
+ or database_value.endswith("sqlite3")
+ ):
+ driver = DatabaseDetector._detect_sqlite_driver(is_async)
+ return SQLiteDialect(), driver
+ pg_indicators = [
+ kwargs.get("port") == 5432,
+ "postgres" in str(kwargs.get("dsn", "")),
+ "postgres" in str(kwargs.get("host", "")),
+ "postgres" in str(kwargs.get("dbname", "")),
+ any(key in kwargs for key in ["sslmode", "nexios"]),
+ ]
+
+ if any(pg_indicators):
+ driver = DatabaseDetector._detect_postgres_driver(is_async)
+ return PostgreSQLDialect(), driver
+
+ mysql_indicators = [
+ kwargs.get("port") == 3306,
+ any(
+ key in kwargs
+ for key in ["unix_socket", "auth_plugin", "charset", "cursorclass"]
+ ),
+ "mysql" in str(kwargs.get("host", "")),
+ "mysql" in str(kwargs.get("database", "")),
+ ]
+
+ if any(mysql_indicators):
+ driver = DatabaseDetector._detect_mysql_driver(is_async)
+ return MySQLDialect(), driver
+
+ if "database" in kwargs and isinstance(kwargs["database"], str):
+ if not any(key in kwargs for key in ["host", "port", "user", "password"]):
+ driver = DatabaseDetector._detect_sqlite_driver(is_async)
+ return SQLiteDialect(), driver
+
+ try:
+ DatabaseDetector._detect_postgres_driver(is_async)
+ return PostgreSQLDialect(), DatabaseDetector._detect_postgres_driver(
+ is_async
+ )
+ except ImportError:
+ pass
+
+ try:
+ DatabaseDetector._detect_mysql_driver(is_async)
+ return MySQLDialect(), DatabaseDetector._detect_mysql_driver(is_async)
+ except ImportError:
+ pass
+
+ # Ultimate fallback to SQLite
+ driver = DatabaseDetector._detect_sqlite_driver(is_async)
+ return SQLiteDialect(), driver
+
+ @staticmethod
+ def _detect_sqlite_driver(is_async: bool = False):
+ """Detect which SQLite driver is available."""
+ if is_async:
+ return DatabaseDetector._find_installed(
+ [(SQLiteDriver.AIOSQLITE, ["aiosqlite"])],
+ "aiosqlite is required for async SQLite operations",
+ )
+ return DatabaseDetector._find_installed(
+ [(SQLiteDriver.SQLITE3, ["sqlite3"]), (SQLiteDriver.APSW, ["apsw"])],
+ "sqlite3 or apsw is required for sync SQLite operations",
+ )
+
+ @staticmethod
+ def _detect_postgres_driver(is_async: bool = False):
+ """Detect which PostgreSQL driver is available."""
+ if is_async:
+ return DatabaseDetector._find_installed(
+ [
+ (PostgreSQLDriver.PSYCOPG3_ASYNC, ["psycopg"]),
+ (PostgreSQLDriver.ASYNCPG, ["asyncpg"]),
+ (PostgreSQLDriver.AIOPG, ["aiopg"]),
+ ],
+ "No async PostgreSQL driver found. Please install one of: "
+ "psycopg3, asyncpg, or aiopg",
+ )
+ return DatabaseDetector._find_installed(
+ [
+ (PostgreSQLDriver.PSYCOPG3, ["psycopg"]),
+ (PostgreSQLDriver.PG8000, ["pg8000"]),
+ ],
+ "No PostgreSQL driver found. Please install one of: "
+ "psycopg3, or pg8000",
+ )
+
+ @staticmethod
+ def _detect_mysql_driver(is_async: bool = False):
+ """Detect which MySQL driver is available."""
+ if is_async:
+ return DatabaseDetector._find_installed(
+ [
+ (MySQLDriver.AIOMYSQL, ["aiomysql"]),
+ (MySQLDriver.ASYNCMY, ["asyncmy"]),
+ ],
+ "No async MySQL driver found. Please install one of: "
+ "aiomysql or asyncmy",
+ )
+ return DatabaseDetector._find_installed(
+ [
+ (MySQLDriver.MYSQL_CONNECTOR, ["mysql.connector"]),
+ (MySQLDriver.PYMySQL, ["pymysql"]),
+ (MySQLDriver.MYSQL_CLIENT, ["MySQLdb"]),
+ (MySQLDriver.MARIADB, ["mariadb"]),
+ ],
+ "No MySQL driver found. Please install one of: "
+ "mysql-connector-python, pymysql, MySQLclient, or mariadb",
+ )
+
+
+class FTSConfig:
+ def __init__(
+ self,
+ language: str = "english",
+ weights: Optional[list] = None,
+ dictionary: Optional[str] = None,
+ ):
+ self.language = language
+ self.weights = weights or []
+ self.dictionary = dictionary or {}
+
+
+class TSVector:
+ def __init__(
+ self,
+ source_fields: Optional[list] = None,
+ config: Optional[FTSConfig] = None,
+ **kwargs,
+ ):
+ self.source_fields = source_fields or []
+ self.config = config or FTSConfig()
+
+ def get_column_type(self, dialect: Dialect) -> str:
+ if isinstance(dialect, PostgreSQLDialect):
+ return "TSVECTOR"
+ elif isinstance(dialect, MySQLDialect):
+ return "TEXT"
+ elif isinstance(dialect, SQLiteDialect):
+ return "TEXT"
+ else:
+ raise NotImplementedError()
diff --git a/nexios/orm/connection.py b/nexios/orm/connection.py
new file mode 100644
index 00000000..eb17716d
--- /dev/null
+++ b/nexios/orm/connection.py
@@ -0,0 +1,123 @@
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from typing import Any, Optional, Tuple, List
+
+
+class SyncQueryResult:
+ def __init__(self, cursor: SyncCursor) -> None:
+ self.cursor = cursor
+
+ def fetchone(self) -> Optional[Tuple[Any, ...]]:
+ return self.cursor.fetchone()
+
+ def fetchall(self) -> List[Tuple[Any, ...]]:
+ return self.cursor.fetchall()
+
+ def fetchmany(self, size: int = 1) -> List[Tuple[Any, ...]]:
+ return self.cursor.fetchmany(size=size)
+
+
+class AsyncQueryResult:
+ def __init__(self, cursor: AsyncCursor) -> None:
+ self.cursor = cursor
+
+ def __await__(self):
+ async def _await():
+ return self
+ return _await().__await__()
+
+ async def fetchone(self) -> Optional[Tuple[Any, ...]]:
+ return await self.cursor.fetchone()
+
+ async def fetchall(self) -> List[Tuple[Any, ...]]:
+ return await self.cursor.fetchall()
+
+ async def fetchmany(self, size: int = 1) -> List[Tuple[Any, ...]]:
+ return await self.cursor.fetchmany(size=size)
+
+
+class BaseCursor(ABC):
+ @property
+ @abstractmethod
+ def description(self) -> Any: ...
+
+ @property
+ @abstractmethod
+ def rowcount(self) -> int: ...
+
+
+class SyncCursor(BaseCursor):
+ @abstractmethod
+ def execute(self, sql: str, parameters: Tuple[Any, ...] = ...) -> SyncQueryResult: ...
+
+ @abstractmethod
+ def executemany(self, sql: str, seq_of_parameters: List[Tuple[Any, ...]]) -> SyncQueryResult: ...
+
+ @abstractmethod
+ def fetchone(self) -> Optional[Tuple[Any, ...]]: ...
+
+ @abstractmethod
+ def fetchall(self) -> List[Tuple[Any, ...]]: ...
+
+ @abstractmethod
+ def fetchmany(self, size: int = ...) -> List[Tuple[Any, ...]]: ...
+
+class AsyncCursor(BaseCursor):
+ @abstractmethod
+ async def execute(self, sql: str, parameters: Tuple[Any, ...] = ...) -> AsyncQueryResult: ...
+
+ @abstractmethod
+ async def executemany(self, sql: str, seq_of_parameters: List[Tuple[Any, ...]]) -> AsyncQueryResult: ...
+
+ @abstractmethod
+ async def fetchone(self) -> Optional[Tuple[Any, ...]]: ...
+
+ @abstractmethod
+ async def fetchall(self) -> List[Tuple[Any, ...]]: ...
+
+ @abstractmethod
+ async def fetchmany(self, size: int = ...) -> List[Tuple[Any, ...]]: ...
+
+class SyncDatabaseConnection(ABC):
+ @abstractmethod
+ def cursor(self) -> SyncCursor: ...
+
+ @abstractmethod
+ def commit(self) -> None: ...
+
+ @abstractmethod
+ def rollback(self) -> None: ...
+
+ @abstractmethod
+ def close(self) -> None: ...
+
+ @property
+ @abstractmethod
+ def raw_connection(self) -> Any: ...
+
+ @property
+ @abstractmethod
+ def is_connection_open(self) -> bool: ...
+
+class AsyncDatabaseConnection(ABC):
+ @abstractmethod
+ async def cursor(self) -> AsyncCursor: ...
+
+ @abstractmethod
+ async def commit(self) -> None: ...
+
+ @abstractmethod
+ async def rollback(self) -> None: ...
+
+ @abstractmethod
+ async def close(self) -> None: ...
+
+ @property
+ @abstractmethod
+ def raw_connection(self) -> Any: ...
+
+ @property
+ @abstractmethod
+ def is_connection_open(self) -> bool: ...
+
diff --git a/nexios/orm/dbapi/__init__.py b/nexios/orm/dbapi/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/nexios/orm/dbapi/mysql/__init__.py b/nexios/orm/dbapi/mysql/__init__.py
new file mode 100644
index 00000000..550ae87c
--- /dev/null
+++ b/nexios/orm/dbapi/mysql/__init__.py
@@ -0,0 +1,57 @@
+from typing import Any, Optional, Tuple, List
+
+import mysql
+import mysql.connector
+import mysql.connector.cursor
+from nexios.orm.connection import SyncCursor, SyncDatabaseConnection
+
+
+class MySQLConnectorCursor(SyncCursor):
+ def __init__(self, cursor: mysql.connector.cursor.MySQLCursor) -> None:
+ self._cursor = cursor
+
+ @property
+ def description(self) -> Any:
+ return self._cursor.description
+
+ @property
+ def rowcount(self) -> int:
+ return self._cursor.rowcount
+
+ def execute(self, sql: str, parameters: Tuple[Any, ...] = ()) -> Any:
+ return self._cursor.execute(sql, parameters)
+
+ def executemany(self, sql: str, seq_of_parameters: List[Tuple[Any, ...]]) -> Any:
+ return self._cursor.executemany(sql, seq_of_parameters)
+
+ def fetchone(self) -> Optional[Tuple[Any, ...]]:
+ row = self._cursor.fetchone()
+ if row is None:
+ return None
+ return tuple(row)
+
+ def fetchall(self) -> List[Tuple[Any, ...]]:
+ rows = self._cursor.fetchall()
+ return [tuple(row) for row in rows]
+
+ def fetchmany(self, size: int = 1) -> List[Tuple[Any, ...]]:
+ rows = self._cursor.fetchmany(size)
+ return [tuple(row) for row in rows]
+
+
+class MySQLConnectorConnection(SyncDatabaseConnection):
+ def __init__(self, connection: mysql.connector.connection.MySQLConnection) -> None:
+ self._connection = connection
+
+ def cursor(self) -> SyncCursor:
+ cursor = self._connection.cursor()
+ return MySQLConnectorCursor(cursor)
+
+ def commit(self) -> None:
+ self._connection.commit()
+
+ def rollback(self) -> None:
+ self._connection.rollback()
+
+ def close(self) -> None:
+ self._connection.close()
diff --git a/nexios/orm/dbapi/mysql/aiomysql_.py b/nexios/orm/dbapi/mysql/aiomysql_.py
new file mode 100644
index 00000000..1e0c098c
--- /dev/null
+++ b/nexios/orm/dbapi/mysql/aiomysql_.py
@@ -0,0 +1,69 @@
+from typing import Any, List, Optional, Tuple
+
+import aiomysql
+from nexios.orm.connection import AsyncCursor, AsyncDatabaseConnection, AsyncQueryResult
+from nexios.orm.misc.row_to_tuple import convert_row, convert_rows
+
+
+class MySQLAioMySQLCursor(AsyncCursor):
+ def __init__(self, cursor: aiomysql.Cursor) -> None:
+ self._cursor = cursor
+
+ @property
+ def description(self) -> Any:
+ return self._cursor.description
+
+ @property
+ def rowcount(self) -> int:
+ return self._cursor.rowcount
+
+ async def execute(self, sql: str, parameters: Tuple[Any, ...] = ()) -> AsyncQueryResult:
+ await self._cursor.execute(sql, parameters)
+ last_id = self._cursor.lastrowid
+ setattr(AsyncQueryResult, 'last_id', last_id)
+ return AsyncQueryResult(self)
+
+ async def executemany(
+ self, sql: str, seq_of_parameters: List[Tuple[Any, ...]]
+ ) -> AsyncQueryResult:
+ await self._cursor.executemany(sql, seq_of_parameters)
+ return AsyncQueryResult(self)
+
+ async def fetchone(self) -> Optional[Tuple[Any, ...]]:
+ row = await self._cursor.fetchone() # type: ignore
+ return convert_row(row)
+
+ async def fetchmany(self, size: int = 1) -> List[Tuple[Any, ...]]:
+ rows = await self._cursor.fetchmany(size) # type: ignore
+ return convert_rows(rows)
+
+ async def fetchall(self) -> List[Tuple[Any, ...]]:
+ rows = await self._cursor.fetchall() # type: ignore
+ return convert_rows(rows)
+
+class MySQLAioMySQLConnection(AsyncDatabaseConnection):
+ def __init__(self, connection: aiomysql.Connection) -> None:
+ self._connection = connection
+
+ async def cursor(self) -> MySQLAioMySQLCursor:
+ raw_cursor = self._connection.cursor()
+ cur = await raw_cursor.__aenter__()
+ return MySQLAioMySQLCursor(cur)
+
+ async def commit(self) -> None:
+ await self._connection.commit()
+
+ async def rollback(self) -> None:
+ await self._connection.rollback()
+
+ async def close(self) -> None:
+ self._connection.close()
+
+ @property
+ def raw_connection(self) -> aiomysql.Connection:
+ return self._connection
+
+ @property
+ def is_connection_open(self) -> bool:
+ return not self._connection.closed
+
diff --git a/nexios/orm/dbapi/mysql/asyncmy_.py b/nexios/orm/dbapi/mysql/asyncmy_.py
new file mode 100644
index 00000000..bafe4128
--- /dev/null
+++ b/nexios/orm/dbapi/mysql/asyncmy_.py
@@ -0,0 +1,65 @@
+from __future__ import annotations
+
+import asyncmy
+from typing import List, Tuple, Any
+
+import asyncmy.cursors
+from nexios.orm.connection import AsyncDatabaseConnection, AsyncCursor, AsyncQueryResult
+from nexios.orm.misc.row_to_tuple import convert_row, convert_rows
+
+
+class AsyncMyConnection(AsyncDatabaseConnection):
+ def __init__(self, connection: asyncmy.Connection):
+ self.connection = connection
+
+ async def cursor(self) -> AsyncCursor:
+ return AsyncMyCursor(self.connection.cursor())
+
+ async def commit(self) -> None:
+ await self.connection.commit()
+
+ async def rollback(self) -> None:
+ await self.connection.rollback()
+
+ async def close(self) -> None:
+ await self.connection.close()
+
+ @property
+ def raw_connection(self) -> Any:
+ return self.connection
+
+ @property
+ def is_connection_open(self) -> bool:
+ return self.connection
+
+class AsyncMyCursor(AsyncCursor):
+ def __init__(self, cursor: asyncmy.cursors.Cursor) -> None:
+ self._cursor = cursor
+
+ async def execute(self, sql: str, parameters: Tuple[Any, ...] = ()) -> AsyncQueryResult:
+ await self._cursor.execute(sql, parameters)
+ return AsyncQueryResult(self)
+
+ async def executemany(self, sql: str, seq_of_parameters: List[Tuple[Any, ...]]) -> AsyncQueryResult:
+ await self._cursor.executemany(sql, seq_of_parameters)
+ return AsyncQueryResult(self)
+
+ async def fetchone(self) -> Tuple[Any] | None:
+ row = await self._cursor.fetchone()
+ return convert_row(row)
+
+ async def fetchall(self) -> List[Tuple[Any]]:
+ rows = await self._cursor.fetchall()
+ return convert_rows(rows)
+
+ async def fetchmany(self, size: int = 1) -> List[Tuple[Any]]:
+ rows = await self._cursor.fetchmany(size)
+ return convert_rows(rows)
+
+ @property
+ def description(self):
+ return self._cursor.description
+
+ @property
+ def rowcount(self):
+ return self._cursor.rowcount
diff --git a/nexios/orm/dbapi/mysql/mariadb_.py b/nexios/orm/dbapi/mysql/mariadb_.py
new file mode 100644
index 00000000..4b46d941
--- /dev/null
+++ b/nexios/orm/dbapi/mysql/mariadb_.py
@@ -0,0 +1,66 @@
+from __future__ import annotations
+
+from typing import Any, List, Tuple, Optional
+import mariadb.connections
+from nexios.orm.connection import SyncDatabaseConnection, SyncCursor, SyncQueryResult
+from nexios.orm.misc.row_to_tuple import convert_row, convert_rows
+
+
+class MariaDBConnection(SyncDatabaseConnection):
+ def __init__(self, conn: mariadb.connections.Connection):
+ self.connection = conn
+
+ def cursor(self) -> SyncCursor:
+ return MariaDBCursor(self.connection.cursor())
+
+ def begin(self):
+ self.connection.begin()
+
+ def commit(self) -> None:
+ self.connection.commit()
+
+ def rollback(self) -> None:
+ self.connection.rollback()
+
+ def close(self) -> None:
+ self.connection.close()
+
+ @property
+ def raw_connection(self) -> Any:
+ return self.connection
+
+ @property
+ def is_connection_open(self) -> bool:
+ return self.connection.open
+
+class MariaDBCursor(SyncCursor):
+ def __init__(self, cur: mariadb.Cursor):
+ self.cur = cur
+
+ def execute(self, sql: str, parameters: Tuple[Any, ...] = ()) -> SyncQueryResult:
+ self.cur.execute(sql, parameters)
+ return SyncQueryResult(self)
+
+ def executemany(self, sql: str, seq_of_parameters: List[Tuple[Any, ...]]) -> SyncQueryResult:
+ self.cur.executemany(sql, seq_of_parameters)
+ return SyncQueryResult(self)
+
+ def fetchone(self) -> Optional[Tuple[Any, ...]]:
+ row = self.cur.fetchone()
+ return convert_row(row)
+
+ def fetchall(self) -> List[Tuple[Any, ...]]:
+ rows = self.cur.fetchall()
+ return convert_rows(rows)
+
+ def fetchmany(self, size: int = 1) -> List[Tuple[Any, ...]]:
+ rows = self.cur.fetchmany(size)
+ return convert_rows(rows)
+
+ @property
+ def description(self) -> Any:
+ return self.cur._description
+
+ @property
+ def rowcount(self) -> int:
+ return self.cur.rowcount
\ No newline at end of file
diff --git a/nexios/orm/dbapi/mysql/mysql_client.py b/nexios/orm/dbapi/mysql/mysql_client.py
new file mode 100644
index 00000000..b3fe3934
--- /dev/null
+++ b/nexios/orm/dbapi/mysql/mysql_client.py
@@ -0,0 +1,65 @@
+from __future__ import annotations
+
+from typing import Any, List, LiteralString, Tuple, Optional, cast
+from nexios.orm.connection import SyncDatabaseConnection, SyncCursor, SyncQueryResult
+from nexios.orm.misc.row_to_tuple import convert_rows, convert_row
+
+
+class MySQLClientConnection(SyncDatabaseConnection):
+
+ def __init__(self, conn: Any):
+ self.connection = conn
+
+ def cursor(self) -> SyncCursor:
+ return MySQLClientCursor(self.connection.cursor())
+
+ def commit(self) -> None:
+ self.connection.commit()
+
+ def rollback(self) -> None:
+ self.connection.rollback()
+
+ def close(self) -> None:
+ self.connection.close()
+
+ @property
+ def raw_connection(self) -> Any:
+ return self.connection
+
+ @property
+ def is_connection_open(self) -> bool:
+ return self.connection.open
+
+
+class MySQLClientCursor(SyncCursor):
+
+ def __init__(self, cur: Any):
+ self.cursor = cur
+
+ @property
+ def rowcount(self) -> int:
+ return self.cursor.rowcount
+
+ def execute(self, sql: str, parameters: Tuple[Any, ...] = ()) -> SyncQueryResult:
+ self.cursor.execute(sql, parameters)
+ return SyncQueryResult(self)
+
+ def executemany(self, sql: str, seq_of_parameters: List[Tuple[Any, ...]]) -> SyncQueryResult:
+ self.cursor.executemany(cast(LiteralString, sql), seq_of_parameters)
+ return SyncQueryResult(self)
+
+ def fetchone(self) -> Optional[Tuple[Any, ...]]:
+ row = self.cursor.fetchone()
+ return convert_row(row)
+
+ def fetchall(self) -> List[Tuple[Any, ...]]:
+ rows = self.cursor.fetchall()
+ return convert_rows(rows)
+
+ def fetchmany(self, size: int = 1) -> List[Tuple[Any, ...]]:
+ rows = self.cursor.fetchmany(size)
+ return convert_rows(rows)
+
+ @property
+ def description(self) -> Any:
+ return self.cursor.description
diff --git a/nexios/orm/dbapi/mysql/mysql_connector_.py b/nexios/orm/dbapi/mysql/mysql_connector_.py
new file mode 100644
index 00000000..822cefd8
--- /dev/null
+++ b/nexios/orm/dbapi/mysql/mysql_connector_.py
@@ -0,0 +1,67 @@
+from typing import Any, List, Optional, Tuple
+
+import mysql.connector
+import mysql.connector.cursor
+
+from nexios.orm.connection import SyncCursor, SyncDatabaseConnection, SyncQueryResult
+from nexios.orm.misc.row_to_tuple import convert_row, convert_rows
+
+
+class MySQLConnectorCursor(SyncCursor):
+ def __init__(self, cursor: mysql.connector.cursor.MySQLCursor) -> None:
+ self._cursor = cursor
+
+ @property
+ def description(self) -> Any:
+ return self._cursor.description
+
+ @property
+ def rowcount(self) -> int:
+ return self._cursor.rowcount
+
+ def execute(self, sql: str, parameters: Tuple[Any, ...] = ()) -> SyncQueryResult:
+ self._cursor.execute(sql, parameters)
+ return SyncQueryResult(self)
+
+ def executemany(self, sql: str, seq_of_parameters: List[Tuple[Any, ...]]) -> SyncQueryResult:
+ self._cursor.executemany(sql, seq_of_parameters)
+ return SyncQueryResult(self)
+
+ def fetchone(self) -> Optional[Tuple[Any, ...]]:
+ row = self._cursor.fetchone()
+ return convert_row(row)
+ def fetchall(self) -> List[Tuple[Any, ...]]:
+ rows = self._cursor.fetchall()
+ return convert_rows(rows)
+
+ def fetchmany(self, size: int = 1) -> List[Tuple[Any, ...]]:
+ rows = self._cursor.fetchmany(size)
+ return convert_rows(rows)
+
+class MySQLConnectorConnection(SyncDatabaseConnection):
+ def __init__(self, connection: mysql.connector.connection.MySQLConnection) -> None:
+ self._connection = connection
+
+ def cursor(self) -> SyncCursor:
+ cursor = self._connection.cursor()
+ return MySQLConnectorCursor(cursor) # type: ignore
+
+ def begin(self):
+ self._connection
+
+ def commit(self) -> None:
+ self._connection.commit()
+
+ def rollback(self) -> None:
+ self._connection.rollback()
+
+ def close(self) -> None:
+ self._connection.close()
+
+ @property
+ def raw_connection(self) -> mysql.connector.connection.MySQLConnection:
+ return self._connection
+
+ @property
+ def is_connection_open(self) -> bool:
+ return self._connection.is_connected()
\ No newline at end of file
diff --git a/nexios/orm/dbapi/mysql/pymysql_.py b/nexios/orm/dbapi/mysql/pymysql_.py
new file mode 100644
index 00000000..978c954b
--- /dev/null
+++ b/nexios/orm/dbapi/mysql/pymysql_.py
@@ -0,0 +1,63 @@
+from typing import Any, Optional, Tuple, List, Self
+
+import pymysql
+from nexios.orm.connection import SyncCursor, SyncDatabaseConnection, SyncQueryResult
+from nexios.orm.misc.row_to_tuple import convert_row, convert_rows
+
+
+class PyMySQLCursor(SyncCursor):
+ def __init__(self, cursor: pymysql.cursors.Cursor) -> None:
+ self._cursor = cursor
+
+ @property
+ def description(self) -> Any:
+ return self._cursor.description
+
+ @property
+ def rowcount(self) -> int:
+ return self._cursor.rowcount
+
+ def execute(self, sql: str, parameters: Tuple[Any, ...] = ()) -> SyncQueryResult:
+ self._cursor.execute(sql, parameters)
+ return SyncQueryResult(self)
+
+ def executemany(self, sql: str, seq_of_parameters: List[Tuple[Any, ...]]) -> SyncQueryResult:
+ self._cursor.executemany(sql, seq_of_parameters)
+ return SyncQueryResult(self)
+
+ def fetchone(self) -> Optional[Tuple[Any, ...]]:
+ row = self._cursor.fetchone()
+ return convert_row(row)
+
+ def fetchall(self) -> List[Tuple[Any, ...]]:
+ rows = self._cursor.fetchall()
+ return convert_rows(rows)
+
+ def fetchmany(self, size: int = 1) -> List[Tuple[Any, ...]]:
+ rows = self._cursor.fetchmany(size)
+ return convert_rows(rows)
+
+class PyMySQLConnection(SyncDatabaseConnection):
+ def __init__(self, connection: pymysql.Connection) -> None:
+ self._connection = connection
+
+ def cursor(self) -> SyncCursor:
+ cursor = self._connection.cursor()
+ return PyMySQLCursor(cursor)
+
+ def commit(self) -> None:
+ self._connection.commit()
+
+ def rollback(self) -> None:
+ self._connection.rollback()
+
+ def close(self) -> None:
+ self._connection.close()
+
+ @property
+ def raw_connection(self) -> pymysql.Connection:
+ return self._connection
+
+ @property
+ def is_connection_open(self) -> bool:
+ return self._connection.open
\ No newline at end of file
diff --git a/nexios/orm/dbapi/postgres/__init__.py b/nexios/orm/dbapi/postgres/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/nexios/orm/dbapi/postgres/aiopg_.py b/nexios/orm/dbapi/postgres/aiopg_.py
new file mode 100644
index 00000000..6f8c6bcc
--- /dev/null
+++ b/nexios/orm/dbapi/postgres/aiopg_.py
@@ -0,0 +1,65 @@
+from __future__ import annotations
+
+from typing import Any, List, Tuple, Optional
+
+import aiopg
+
+from nexios.orm.connection import AsyncQueryResult, AsyncCursor, AsyncDatabaseConnection
+from nexios.orm.misc.row_to_tuple import convert_row, convert_rows
+
+
+class AioPgCursor(AsyncCursor):
+ def __init__(self, cursor: aiopg.Cursor):
+ self.cursor = cursor
+
+ async def execute(self, sql: str, parameters: Tuple[Any, ...] = ()) -> AsyncQueryResult:
+ await self.cursor.execute(sql, parameters)
+ return AsyncQueryResult(self)
+
+ async def executemany(self, sql: str, seq_of_parameters: List[Tuple[Any, ...]]) -> AsyncQueryResult:
+ await self.cursor.executemany()
+ return AsyncQueryResult(self)
+
+ async def fetchone(self) -> Optional[Tuple[Any, ...]]:
+ row = await self.cursor.fetchone()
+ return convert_row(row)
+
+ async def fetchall(self) -> List[Tuple[Any, ...]]:
+ rows = await self.cursor.fetchall()
+ return convert_rows(rows)
+
+ async def fetchmany(self, size: int = 1) -> List[Tuple[Any, ...]]:
+ rows = await self.cursor.fetchmany(size)
+ return convert_rows(rows)
+
+ @property
+ def description(self) -> Any:
+ return self.cursor.description
+
+ @property
+ def rowcount(self) -> int:
+ return self.cursor.rowcount
+
+class AioPgConnection(AsyncDatabaseConnection):
+ def __init__(self, connection: aiopg.Connection):
+ self.connection = connection
+
+ async def cursor(self) -> AsyncCursor:
+ return AioPgCursor(self.connection.cursor_factory)
+
+ async def commit(self) -> None:
+ await self.connection.commit()
+
+ async def rollback(self) -> None:
+ await self.connection.rollback()
+
+ async def close(self) -> None:
+ await self.connection.close()
+
+ @property
+ def raw_connection(self) -> Any:
+ return self.connection
+
+ @property
+ def is_connection_open(self) -> bool:
+ return not self.connection.closed
\ No newline at end of file
diff --git a/nexios/orm/dbapi/postgres/async_psycopg_.py b/nexios/orm/dbapi/postgres/async_psycopg_.py
new file mode 100644
index 00000000..189775da
--- /dev/null
+++ b/nexios/orm/dbapi/postgres/async_psycopg_.py
@@ -0,0 +1,66 @@
+from typing import Any, List, Optional, Tuple, cast, LiteralString
+
+import psycopg
+
+from nexios.orm.connection import AsyncCursor, AsyncDatabaseConnection, AsyncQueryResult
+from nexios.orm.misc.row_to_tuple import convert_row, convert_rows
+
+
+class AsyncPsycopgCursor(AsyncCursor):
+ def __init__(self, cursor: psycopg.AsyncCursor) -> None:
+ self._cursor = cursor
+
+ @property
+ def description(self) -> Any:
+ return self._cursor.description
+
+ @property
+ def rowcount(self) -> int:
+ return self._cursor.rowcount
+
+ async def execute(self, sql: str, parameters: Tuple[Any, ...] = ()) -> AsyncQueryResult:
+ sql_to_execute = cast(LiteralString, sql)
+ await self._cursor.execute(query=sql_to_execute, params=parameters)
+ return AsyncQueryResult(self)
+
+ async def executemany(self, sql: str, seq_of_parameters: List[Tuple[Any, ...]]) -> AsyncQueryResult:
+ sql_to_execute = cast(LiteralString, sql)
+ await self._cursor.executemany(sql_to_execute, seq_of_parameters)
+ return AsyncQueryResult(self)
+
+ async def fetchone(self) -> Optional[Tuple[Any, ...]]:
+ row = await self._cursor.fetchone()
+ return convert_row(row)
+
+ async def fetchall(self) -> List[Tuple[Any, ...]]:
+ rows = await self._cursor.fetchall()
+ return convert_rows(rows)
+
+ async def fetchmany(self, size: int = -1) -> List[Tuple[Any, ...]]:
+ rows = await self._cursor.fetchmany(size)
+ return convert_rows(rows)
+
+class AsyncPsycopgConnection(AsyncDatabaseConnection):
+ def __init__(self, connection: psycopg.AsyncConnection) -> None:
+ self._connection = connection
+
+ async def cursor(self) -> AsyncPsycopgCursor:
+ raw_cursor = self._connection.cursor()
+ return AsyncPsycopgCursor(raw_cursor)
+
+ async def commit(self) -> None:
+ await self._connection.commit()
+
+ async def rollback(self) -> None:
+ await self._connection.rollback()
+
+ async def close(self) -> None:
+ await self._connection.close()
+
+ @property
+ def raw_connection(self) -> psycopg.AsyncConnection:
+ return self._connection
+
+ @property
+ def is_connection_open(self) -> bool:
+ return not self._connection.closed
\ No newline at end of file
diff --git a/nexios/orm/dbapi/postgres/asyncpg_.py b/nexios/orm/dbapi/postgres/asyncpg_.py
new file mode 100644
index 00000000..7f547b95
--- /dev/null
+++ b/nexios/orm/dbapi/postgres/asyncpg_.py
@@ -0,0 +1,141 @@
+from __future__ import annotations
+
+from typing import Any, List, Optional, Tuple
+
+import asyncpg
+import asyncpg.cursor
+
+from nexios.orm.connection import AsyncCursor, AsyncDatabaseConnection, AsyncQueryResult
+from nexios.orm.misc.row_to_tuple import convert_row, convert_rows
+
+
+class AsyncPgCursor(AsyncCursor):
+ def __init__(self, connection: AsyncPgConnection):
+ self._conn = connection
+ self._result: Optional[List[asyncpg.Record]] = None
+ self._position: int = 0
+ self._description: Optional[List[Tuple]] = None
+ self._in_transaction: bool = False
+
+ @property
+ def description(self) -> Any:
+ # return None
+ if self._description is None:
+ return None
+ return self._description
+
+ @property
+ def rowcount(self) -> int:
+ # return -1
+ if self._result is None:
+ return -1
+ return len(self._result)
+
+ async def execute(self, sql: str, parameters: Tuple[Any, ...] = ()) -> AsyncQueryResult:
+ self._result = None
+ self._position = 0
+
+ if hasattr(self._conn, '_ensure_transaction'):
+ await self._conn._ensure_transaction()
+
+ try:
+ self._result = await self._conn.raw_connection.fetch(sql, *parameters)
+ if self._result:
+ first_record = self._result[0]
+ self._description = [
+ (key, type(value).__module__, None, None, None, None)
+ for key, value in first_record.items()
+ ]
+ else:
+ self._description = []
+ return AsyncQueryResult(self)
+ except asyncpg.exceptions.PostgresError as e:
+ self._result = None
+ self._description = None
+ raise e
+ except Exception as e:
+ raise e
+
+ async def executemany(self, sql: str, seq_of_parameters: List[Tuple[Any, ...]]) -> AsyncQueryResult:
+ await self._conn.raw_connection.executemany(sql, seq_of_parameters)
+ self._result = None
+ self._description = None
+ self._position = 0
+ return AsyncQueryResult(self)
+
+ async def fetchone(self) -> Optional[Tuple[Any, ...]]:
+ if self._result is None:
+ raise RuntimeError("No query has been executed yet. Call execute() first.")
+
+ if self._position >= len(self._result):
+ return None
+
+ row = self._result[self._position]
+ self._position += 1
+ return convert_row(row)
+
+ async def fetchall(self) -> List[Tuple[Any, ...]]:
+ if self._result is None:
+ raise RuntimeError("No query has been executed yet. Call execute() first.")
+
+ rows = self._result[self._position:]
+ self._position = len(self._result)
+ return convert_rows(rows)
+
+ async def fetchmany(self, size: int = 1) -> List[Tuple[Any, ...]]:
+ if self._result is None:
+ raise RuntimeError("No query has been executed yet. Call execute() first.")
+
+ end_pos = min(self._position + size, len(self._result))
+ rows = self._result[self._position:end_pos]
+ self._position = end_pos
+ return convert_rows(rows)
+
+class AsyncPgConnection(AsyncDatabaseConnection):
+ def __init__(self, connection: asyncpg.Connection):
+ from asyncpg.transaction import Transaction
+
+ self._connection = connection
+ self._transaction:Optional[Transaction] = None
+ self._transaction_depth: int = 0
+ self._auto_start_transaction = True
+
+ async def _ensure_transaction(self):
+ if self._auto_start_transaction and self._transaction_depth == 0:
+ self._transaction = self._connection.transaction()
+ await self._transaction.start()
+ self._transaction_depth += 1
+
+ async def cursor(self) -> AsyncPgCursor:
+ return AsyncPgCursor(self)
+
+ async def commit(self) -> None:
+ if self._transaction_depth <= 0:
+ return
+
+ self._transaction_depth -= 1
+ if self._transaction_depth == 0 and self._transaction:
+ await self._transaction.commit()
+ self._transaction = None
+
+ async def rollback(self) -> None:
+ if self._transaction_depth <= 0:
+ return
+
+ self._transaction_depth -= 1
+ if self._transaction_depth == 0 and self._transaction:
+ await self._transaction.rollback()
+ self._transaction = None
+
+ async def close(self) -> None:
+ if self._transaction_depth > 0 and self._transaction:
+ await self._transaction.rollback()
+ await self._connection.close()
+
+ @property
+ def raw_connection(self) -> asyncpg.Connection:
+ return self._connection
+
+ @property
+ def is_connection_open(self) -> bool:
+ return not self._connection.is_closed()
\ No newline at end of file
diff --git a/nexios/orm/dbapi/postgres/pg8000_.py b/nexios/orm/dbapi/postgres/pg8000_.py
new file mode 100644
index 00000000..ae6f37cd
--- /dev/null
+++ b/nexios/orm/dbapi/postgres/pg8000_.py
@@ -0,0 +1,71 @@
+from typing import Any, Optional, Tuple, List
+
+import pg8000
+import pg8000.dbapi
+
+from nexios.orm.connection import SyncCursor, SyncDatabaseConnection, SyncQueryResult
+from nexios.orm.misc.row_to_tuple import convert_row, convert_rows
+
+
+class Pg8000Cursor(SyncCursor):
+ def __init__(self, cursor: pg8000.dbapi.Cursor):
+ self._cursor = cursor
+
+ @property
+ def description(self) -> Any:
+ return self._cursor.description
+
+ @property
+ def rowcount(self) -> int:
+ return self._cursor.rowcount
+
+ def execute(self, sql: str, parameters: Tuple[Any, ...] = ()) -> SyncQueryResult:
+ self._cursor.execute(sql, parameters)
+ return SyncQueryResult(self)
+
+ def executemany(self, sql: str, seq_of_parameters: List[Tuple[Any, ...]]) -> SyncQueryResult:
+ self._cursor.executemany(sql, seq_of_parameters)
+ return SyncQueryResult(self)
+
+ def fetchone(self) -> Optional[Tuple[Any, ...]]:
+ row = self._cursor.fetchone()
+ return convert_row(row)
+
+ def fetchall(self) -> List[Tuple[Any, ...]]:
+ rows = self._cursor.fetchall()
+ return convert_rows(rows)
+
+ def fetchmany(self, size: int = -1) -> List[Tuple[Any, ...]]:
+ rows = self._cursor.fetchmany(size)
+ return convert_rows(rows)
+
+class Pg8000Connection(SyncDatabaseConnection):
+ def __init__(self, connection: pg8000.dbapi.Connection):
+ self._connection = connection
+
+ def cursor(self) -> Pg8000Cursor:
+ cursor = self._connection.cursor()
+ return Pg8000Cursor(cursor)
+
+ def commit(self) -> None:
+ self._connection.commit()
+
+ def rollback(self) -> None:
+ self._connection.rollback()
+
+ def close(self) -> None:
+ self._connection.close()
+
+ @property
+ def raw_connection(self) -> pg8000.dbapi.Connection:
+ return self._connection
+
+ @property
+ def is_connection_open(self) -> bool:
+ try:
+ stmt = "SELECT 1"
+ cur = self._connection.cursor()
+ cur.execute(stmt)
+ return True
+ except pg8000.dbapi.InterfaceError:
+ return False
\ No newline at end of file
diff --git a/nexios/orm/dbapi/postgres/psycopg_.py b/nexios/orm/dbapi/postgres/psycopg_.py
new file mode 100644
index 00000000..5ced2880
--- /dev/null
+++ b/nexios/orm/dbapi/postgres/psycopg_.py
@@ -0,0 +1,67 @@
+from typing import Any, Tuple, List, Optional, LiteralString, cast
+
+import psycopg
+
+from nexios.orm.connection import SyncCursor, SyncDatabaseConnection, SyncQueryResult
+from nexios.orm.misc.row_to_tuple import convert_row, convert_rows
+
+
+class PsycopgCursor(SyncCursor):
+ def __init__(self, cursor: psycopg.cursor.Cursor) -> None:
+ self._cursor = cursor
+
+ @property
+ def description(self) -> Any:
+ return self._cursor.description
+
+ @property
+ def rowcount(self) -> int:
+ return self._cursor.rowcount
+
+ def execute(self, sql: str, parameters: Tuple[Any, ...] = ()) -> SyncQueryResult:
+ sql_to_execute = cast(LiteralString, sql)
+ self._cursor.execute(sql_to_execute, parameters)
+ return SyncQueryResult(self)
+
+ def executemany(self, sql: str, seq_of_parameters: List[Tuple[Any, ...]]) -> SyncQueryResult:
+ sql_to_execute = cast(LiteralString, sql)
+ self._cursor.executemany(sql_to_execute, seq_of_parameters)
+ return SyncQueryResult(self)
+
+ def fetchone(self) -> Optional[Tuple[Any, ...]]:
+ row = self._cursor.fetchone()
+ return convert_row(row)
+
+ def fetchall(self) -> List[Tuple[Any, ...]]:
+ rows = self._cursor.fetchall()
+ return convert_rows(rows)
+
+ def fetchmany(self, size: int = 1) -> List[Tuple[Any, ...]]:
+ rows = self._cursor.fetchmany(size)
+ return convert_rows(rows)
+
+class PsycopgConnection(SyncDatabaseConnection):
+ def __init__(self, connection: psycopg.Connection) -> None:
+ self._connection = connection
+
+ def cursor(self) -> SyncCursor:
+ raw_cursor = self._connection.cursor()
+ return PsycopgCursor(raw_cursor)
+
+ def commit(self) -> None:
+ self._connection.commit()
+
+ def rollback(self) -> None:
+ self._connection.rollback()
+
+ def close(self) -> None:
+ self._connection.close()
+
+ @property
+ def raw_connection(self) -> psycopg.Connection:
+ return self._connection
+
+ @property
+ def is_connection_open(self) -> bool:
+ return not self._connection.closed
+
diff --git a/nexios/orm/dbapi/sqlite/__init__.py b/nexios/orm/dbapi/sqlite/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/nexios/orm/dbapi/sqlite/aiosqlite_.py b/nexios/orm/dbapi/sqlite/aiosqlite_.py
new file mode 100644
index 00000000..986fdd92
--- /dev/null
+++ b/nexios/orm/dbapi/sqlite/aiosqlite_.py
@@ -0,0 +1,70 @@
+from typing import Any, List, Optional, Tuple
+
+import aiosqlite
+
+from nexios.orm.connection import AsyncCursor, AsyncDatabaseConnection, AsyncQueryResult
+from nexios.orm.misc.row_to_tuple import convert_row, convert_rows
+
+
+class AioSQLiteCursor(AsyncCursor):
+ def __init__(self, cursor: aiosqlite.Cursor) -> None:
+ self._cursor = cursor
+
+ @property
+ def description(self) -> Any:
+ return self._cursor.description
+
+ @property
+ def rowcount(self) -> int:
+ return self._cursor.rowcount
+
+ async def execute(self, sql: str, parameters: Tuple[Any, ...] = ()) -> AsyncQueryResult:
+ await self._cursor.execute(sql, parameters)
+ return AsyncQueryResult(self)
+
+ async def executemany(self, sql: str, seq_of_parameters: List[Tuple[Any, ...]]) -> AsyncQueryResult:
+ await self._cursor.executemany(sql, seq_of_parameters)
+ return AsyncQueryResult(self)
+
+ async def fetchone(self) -> Optional[Tuple[Any, ...]]:
+ row = await self._cursor.fetchone()
+ return convert_row(row)
+
+ async def fetchall(self) -> List[Tuple[Any, ...]]:
+ rows = await self._cursor.fetchall()
+ return convert_rows(rows)
+
+ async def fetchmany(self, size: int = 1) -> List[Tuple[Any, ...]]:
+ rows = await self._cursor.fetchmany(size)
+ return convert_rows(rows)
+
+
+class AioSQLiteConnection(AsyncDatabaseConnection):
+ def __init__(self, connection: aiosqlite.Connection) -> None:
+ self._connection = connection
+
+ async def cursor(self) -> AsyncCursor:
+ cursor = await self._connection.cursor()
+ return AioSQLiteCursor(cursor)
+
+ async def commit(self) -> None:
+ await self._connection.commit()
+
+ async def rollback(self) -> None:
+ await self._connection.rollback()
+
+ async def close(self) -> None:
+ await self._connection.close()
+
+ @property
+ def raw_connection(self) -> aiosqlite.Connection:
+ return self._connection
+
+ @property
+ async def is_connection_open(self) -> bool:
+ try:
+ stmt = "SELECT 1"
+ await self._connection.execute(stmt)
+ return True
+ except aiosqlite.ProgrammingError:
+ return False
\ No newline at end of file
diff --git a/nexios/orm/dbapi/sqlite/apsw_.py b/nexios/orm/dbapi/sqlite/apsw_.py
new file mode 100644
index 00000000..20fc43b1
--- /dev/null
+++ b/nexios/orm/dbapi/sqlite/apsw_.py
@@ -0,0 +1,78 @@
+from __future__ import annotations
+
+import apsw
+from typing import Any, List, Tuple, Optional
+
+from nexios.orm.connection import SyncDatabaseConnection, SyncCursor, SyncQueryResult
+from nexios.orm.misc.row_to_tuple import convert_row, convert_rows
+
+
+class ApswConnection(SyncDatabaseConnection):
+ def __init__(self, conn: apsw.Connection) -> None:
+ self.connection = conn
+ self.__cursor = self.connection.cursor()
+
+ def cursor(self) -> SyncCursor:
+ return ApswCursor(self.connection.cursor())
+
+ def commit(self) -> None:
+ self.__cursor.execute("COMMIT")
+
+ def rollback(self) -> None:
+ self.__cursor.execute("ROLLBACK")
+
+ def close(self) -> None:
+ self.connection.close()
+
+ @property
+ def raw_connection(self) -> Any:
+ return self.connection
+
+ @property
+ def is_connection_open(self) -> bool:
+ return self.connection is not None
+
+
+class ApswCursor(SyncCursor):
+ def __init__(self, cur: apsw.Cursor):
+ self.cursor = cur
+
+ def execute(self, sql: str, parameters: Tuple[Any, ...] = ()) -> SyncQueryResult:
+ self.cursor.execute("BEGIN").execute(sql, parameters)
+ return SyncQueryResult(self)
+
+ def executemany(
+ self, sql: str, seq_of_parameters: List[Tuple[Any, ...]]
+ ) -> SyncQueryResult:
+ self.cursor.execute("BEGIN").executemany(
+ statements=sql, sequenceofbindings=seq_of_parameters
+ )
+ return SyncQueryResult(self)
+
+ def fetchone(self) -> Optional[Tuple[Any, ...]]:
+ row = self.cursor.fetchone()
+ return convert_row(row)
+
+ def fetchall(self) -> List[Tuple[Any, ...]]:
+ rows = self.cursor.fetchall()
+ return convert_rows(rows)
+
+ def fetchmany(self, size: int = 1) -> List[Tuple[Any, ...]]:
+ rows = []
+ while True:
+ for _ in range(size):
+ row = self.cursor.fetchone()
+ if row is None:
+ break
+ rows.append(row)
+ if not rows:
+ break
+ return convert_rows(rows)
+
+ @property
+ def description(self) -> Any:
+ return self.cursor.description
+
+ @property
+ def rowcount(self) -> int:
+ return self.cursor.bindings_count
diff --git a/nexios/orm/dbapi/sqlite/sqlite_.py b/nexios/orm/dbapi/sqlite/sqlite_.py
new file mode 100644
index 00000000..8122fb8e
--- /dev/null
+++ b/nexios/orm/dbapi/sqlite/sqlite_.py
@@ -0,0 +1,69 @@
+import sqlite3
+from typing import Any, List, Optional, Tuple
+
+from nexios.orm.connection import SyncCursor, SyncDatabaseConnection, SyncQueryResult
+from nexios.orm.misc.row_to_tuple import convert_row, convert_rows
+
+
+class SQLiteCursor(SyncCursor):
+ def __init__(self, cursor: sqlite3.Cursor) -> None:
+ self._cursor = cursor
+
+ @property
+ def description(self) -> Any:
+ return self._cursor.description
+
+ @property
+ def rowcount(self) -> int:
+ return self._cursor.rowcount
+
+ def execute(self, sql: str, parameters: Tuple[Any, ...] = ()) -> SyncQueryResult:
+ self._cursor.execute(sql, parameters)
+ return SyncQueryResult(self)
+
+ def executemany(self, sql: str, seq_of_parameters: List[Tuple[Any, ...]]) -> SyncQueryResult:
+ self._cursor.executemany(sql, seq_of_parameters)
+ return SyncQueryResult(self)
+
+ def fetchone(self) -> Optional[Tuple[Any, ...]]:
+ row = self._cursor.fetchone()
+ return convert_row(row)
+
+ def fetchall(self) -> List[Tuple[Any, ...]]:
+ rows = self._cursor.fetchall()
+ return convert_rows(rows)
+
+ def fetchmany(self, size: int = 1) -> List[Tuple[Any, ...]]:
+ rows = self._cursor.fetchmany(size)
+ return convert_rows(rows)
+
+
+class SQLiteConnection(SyncDatabaseConnection):
+ def __init__(self, connection: sqlite3.Connection) -> None:
+ self._connection = connection
+
+ def cursor(self) -> SyncCursor:
+ cursor = self._connection.cursor()
+ return SQLiteCursor(cursor)
+
+ def commit(self) -> None:
+ self._connection.commit()
+
+ def rollback(self) -> None:
+ self._connection.rollback()
+
+ def close(self) -> None:
+ self._connection.close()
+
+ @property
+ def raw_connection(self) -> sqlite3.Connection:
+ return self._connection
+
+ @property
+ def is_connection_open(self) -> bool:
+ try:
+ stmt = "SELECT 1"
+ self._connection.execute(stmt)
+ return True
+ except sqlite3.ProgrammingError:
+ return False
\ No newline at end of file
diff --git a/nexios/orm/descriptors.py b/nexios/orm/descriptors.py
new file mode 100644
index 00000000..a7275621
--- /dev/null
+++ b/nexios/orm/descriptors.py
@@ -0,0 +1,667 @@
+from __future__ import annotations
+
+import types
+
+from typing import Type, Optional, Any, List, overload, cast, TYPE_CHECKING
+from pydantic_core import PydanticUndefined as Undefined
+
+from nexios.orm.relationships import RelationshipInfo, RelationshipType
+from nexios.orm.misc.context import get_context_data
+
+if TYPE_CHECKING:
+ from nexios.orm.model import NexiosModel
+ from nexios.orm import Select
+ from nexios.orm.utils import (
+ to_snake_case,
+ get_tablename_for_class,
+ InstanceOrType,
+ )
+
+
+_NOT_LOADED = object()
+
+
+class ColumnDescriptor:
+ """Descriptor that returns ColumnExpression when accessed from class"""
+
+ def __init__(self, field_name: str, model_class: Type[NexiosModel]) -> None:
+ self.field_name = field_name
+ self.model_class = model_class
+
+ def __get__(self, instance, owner):
+ from nexios.orm.query.expressions import ColumnExpression
+
+ if instance is None:
+ return ColumnExpression(self.model_class, self.field_name)
+ return instance.__dict__.get(self.field_name, None)
+
+ def __set__(self, instance, value):
+ instance.__dict__[self.field_name] = value
+
+ def __delete__(self, instance):
+ if self.field_name in instance.__dict__:
+ del instance.__dict__[self.field_name]
+
+
+class RelationshipDescriptor:
+ def __init__(
+ self,
+ model_class: Type[NexiosModel],
+ field_name: str,
+ relationship_info: Optional[RelationshipInfo] = None,
+ ) -> types.NoneType:
+ self.model_class = model_class
+ self.field_name = field_name
+
+ if relationship_info is not None:
+ self._relationship_info = relationship_info
+ else:
+ self._relationship_info = model_class.__relationships__[field_name]
+
+ def __get__(self, obj: Optional[NexiosModel], objType: Type[NexiosModel]):
+ if obj is None:
+ return self
+
+ cached = self._get_cache(obj)
+ if cached is not _NOT_LOADED:
+ return cached
+
+ session = get_context_data("session")
+ if not session:
+ raise RuntimeError(
+ f"No session available for loading relationship '{self.field_name}'. "
+ f"Make sure you're inside a session context manager."
+ )
+
+ if self._relationship_info.lazy == "select":
+ return self._load_select_lazy(obj, session)
+ elif self._relationship_info.lazy == "joined":
+ return self._load_select_lazy(obj, session)
+ elif self._relationship_info.lazy == "dynamic":
+ return self._load_dynamic(obj, session)
+ elif self._relationship_info.lazy == "subquery":
+ return self._load_subquery(obj, session)
+ else:
+ return self._load_select_lazy(obj, session)
+
+ def __set__(self, obj: NexiosModel, value: Any):
+ """Set relationship value and update foreign key if applicable."""
+ # clear cache
+ self._clear_cache(obj)
+ self._set_cache(obj, value)
+
+ # If setting to None, clear foreign key and return
+ if value is None:
+ if self._relationship_info.foreign_key:
+ fk_field = (
+ self._relationship_info.foreign_key
+ or self._find_local_foreign_key()
+ )
+ if fk_field and hasattr(obj, fk_field):
+ setattr(obj, fk_field, None)
+ return
+
+ local_fk = self._relationship_info.foreign_key or self._find_local_foreign_key()
+ if local_fk:
+ pk_val = getattr(value, self._get_pk_field(value), None)
+ setattr(obj, local_fk, pk_val)
+
+ if self._relationship_info.back_populates:
+ back_field = self._relationship_info.back_populates
+
+ if hasattr(value.__class__, back_field):
+ back_desc = getattr(value.__class__, back_field, None)
+
+ if isinstance(back_desc, RelationshipDescriptor):
+ back_desc._set_cache(value, obj)
+ remote_fk = (
+ back_desc._relationship_info.foreign_key
+ or back_desc._find_local_foreign_key()
+ )
+ if remote_fk:
+ obj_pk_name = self._get_pk_field(obj)
+ obj_pk_value = getattr(obj, obj_pk_name, None)
+ setattr(value, remote_fk, obj_pk_value)
+ else:
+ setattr(value, back_field, obj)
+
+ def _find_local_foreign_key(self) -> Optional[str]:
+ related_model = self._relationship_info.related_model
+ if not related_model:
+ return None
+
+ return self._find_foreign_key_on_related(related_model, self.model_class)
+
+ def _set_cache(self, obj: NexiosModel, value: Any) -> None:
+ if "__relationship_cache__" not in obj.__dict__:
+ obj.__dict__["__relationship_cache__"] = {} #type: ignore
+ obj.__dict__["__relationship_cache__"][self.field_name] = value
+
+ def _get_cache(self, obj: NexiosModel) -> Any:
+ """Get cached relationship value if exists."""
+ relationship_cache = obj.__dict__.get("__relationship_cache__", {})
+ return relationship_cache.get(self.field_name, _NOT_LOADED)
+
+ def _clear_cache(self, obj: NexiosModel) -> None:
+ """Clear existing cache"""
+ if "__relationship_cache__" in obj.__dict__:
+ if self.field_name in obj.__dict__["__relationship_cache__"]:
+ del obj.__dict__["__relationship_cache__"][self.field_name]
+
+ def _get_pk_field(self, model: InstanceOrType[NexiosModel]) -> Any:
+ """Helper to safely get primary key."""
+ pk = model.get_primary_key()
+ return pk[0] if isinstance(pk, (list, tuple)) else pk
+
+ def _find_foreign_key_on_related(
+ self, related_model: Type[NexiosModel], current_model_class: Type[NexiosModel]
+ ) -> str:
+ """Find foreign key on related model that points to current model"""
+ from nexios.orm.utils import to_snake_case, get_model_fields, get_tablename_for_class
+
+
+ # For one-to-one, the foreign key could be on either side
+ # Check if current model has a field that points to related model first
+ current_fields = get_model_fields(current_model_class)
+ expected_name = f"{to_snake_case(related_model.__name__)}_id"
+
+ # Check if foreign key exists on current model pointing to related
+ for field_name, field_info in current_fields.items():
+ fk = getattr(field_info, "foreign_key", Undefined)
+ if fk is not Undefined and fk:
+ if isinstance(fk, str):
+ fk_s = fk.strip()
+ if "." in fk_s:
+ left, _ = fk_s.rsplit(".", 1)
+ left_l = left.lower()
+ tablename = (
+ get_tablename_for_class(related_model) or ""
+ ).lower()
+ candidates = {
+ related_model.__name__.lower(),
+ to_snake_case(related_model.__name__),
+ tablename,
+ }
+ if left_l in candidates:
+ return field_name
+ else:
+ if fk_s == expected_name or fk_s == field_name:
+ return field_name
+
+ # If not found, check the related model (original logic)
+ expected_name_on_related = f"{to_snake_case(current_model_class.__name__)}_id"
+ pk_name = self._get_pk_field(current_model_class)
+ related_fields = get_model_fields(related_model)
+
+ # Check explicit foreign key metadata first
+ for field_name, field_info in related_fields.items():
+ fk = getattr(field_info, "foreign_key", Undefined)
+ if fk is not Undefined and fk:
+ if isinstance(fk, str):
+ fk_s = fk.strip()
+ if "." in fk_s:
+ left, _ = fk_s.rsplit(".", 1)
+ left_l = left.lower()
+ tablename = (
+ get_tablename_for_class(current_model_class) or ""
+ ).lower()
+ candidates = {
+ current_model_class.__name__.lower(),
+ to_snake_case(current_model_class.__name__),
+ tablename,
+ }
+ if left_l in candidates:
+ return field_name
+ else:
+ if (
+ fk_s == expected_name_on_related
+ or fk_s == str(pk_name)
+ or fk_s == field_name
+ ):
+ return field_name
+
+ # fallback: find by naming convention on the related model's field names
+ for field_name in related_fields.keys():
+ if field_name == expected_name_on_related:
+ return field_name
+ if pk_name and field_name.endswith(f"_{pk_name}"):
+ return field_name
+
+ # last resort: if attribute exists on the related model, return it
+ if hasattr(related_model, expected_name_on_related):
+ return expected_name_on_related
+
+ return expected_name_on_related
+
+ def _load_select_lazy(self, obj: NexiosModel, session: Any) -> Any:
+ rel_type = self._relationship_info.relationship_type
+
+ if rel_type == RelationshipType.MANY_TO_ONE:
+ return self._load_many_to_one(obj, session)
+ elif rel_type == RelationshipType.ONE_TO_MANY:
+ return self._load_one_to_many(obj, session)
+ elif rel_type == RelationshipType.MANY_TO_MANY:
+ return self._load_many_to_many(obj, session)
+ else: # ONE_TO_ONE
+ return self._load_one_to_one(obj, session)
+
+ def _load_subquery(self, obj: NexiosModel, session: Any) -> Any:
+ """Load using subquery loading strategy."""
+ return self._load_select_lazy(obj, session)
+
+ def _load_many_to_one(self, obj: NexiosModel, session: Any) -> Any:
+ from nexios.orm.misc.event_loop import NexiosEventLoop
+ from nexios.orm.query.builder import select
+ from nexios.orm.sessions import AsyncSession
+
+ cached = self._get_cache(obj)
+ if cached is not _NOT_LOADED:
+ return cached
+
+ loop = NexiosEventLoop()
+ result: Optional[NexiosModel] = None
+
+ related_model = self._relationship_info.related_model
+
+ fk_field_name = self._relationship_info.foreign_key
+ if not fk_field_name:
+ raise ValueError(
+ f"No foreign key defined for relationship {self._relationship_info.field_name}"
+ )
+
+ fk_value = getattr(obj, fk_field_name, None)
+ if fk_value is None:
+ result = None
+
+ assert related_model is not None
+
+ related_pk_field = self._get_pk_field(related_model)
+
+ query = select(related_model).where(
+ getattr(related_model, related_pk_field) == fk_value
+ )
+
+ query._bind(session)
+
+ if isinstance(session, AsyncSession):
+
+ async def async_fetch():
+ return await query._first_async()
+
+ result = loop.run(async_fetch())
+ else:
+ result = query._first()
+
+ # Cache the result
+ self._set_cache(obj, result)
+
+ return result
+
+ def _load_one_to_many(self, obj: NexiosModel, session: Any) -> Any:
+ from nexios.orm.misc.event_loop import NexiosEventLoop
+ from nexios.orm.query.builder import select
+ from nexios.orm.sessions import AsyncSession
+
+ cached = self._get_cache(obj)
+ if cached is not _NOT_LOADED:
+ return cached
+
+ loop = NexiosEventLoop()
+ results: List[NexiosModel] = []
+ related_model = self._relationship_info.related_model
+
+ pk_name = self._get_pk_field(obj)
+ pk_value = getattr(obj, pk_name, None)
+
+ if related_model is None:
+ raise ValueError("Related model is None")
+
+ fk_field_name = self._find_foreign_key_on_related(related_model, obj.__class__)
+
+ query = select(related_model).where(
+ getattr(related_model, fk_field_name) == pk_value
+ )
+
+ # Bind and execute
+ query._bind(session)
+
+ if isinstance(session, AsyncSession):
+
+ async def async_fetch():
+ return await query._all_async() # type: ignore
+
+ results = loop.run(async_fetch())
+ else:
+ results = query._all()
+
+ # Cache the results
+ self._set_cache(obj, results)
+
+ return results
+
+ def _load_one_to_one(self, obj: NexiosModel, session: Any) -> Any:
+ # return self._load_many_to_one(obj, session)
+ from nexios.orm.misc.event_loop import NexiosEventLoop
+ from nexios.orm.query.builder import select
+ from nexios.orm.sessions import AsyncSession
+
+ cached = self._get_cache(obj)
+ print(f"Cached data: {cached}")
+ if cached is not _NOT_LOADED:
+ return cached
+
+ loop = NexiosEventLoop()
+ result: Optional[NexiosModel] = None
+ related_model = self._relationship_info.related_model
+
+ assert related_model is not None
+
+ # Check if this is the side with the foreign key
+ if self._relationship_info.foreign_key:
+ # This side has the foreign key (like many-to-one)
+ fk_field_name = self._relationship_info.foreign_key
+ fk_value = getattr(obj, fk_field_name, None)
+
+ if fk_value is None:
+ result = None
+
+ related_pk = self._get_pk_field(related_model)
+
+ query = select(related_model).where(
+ getattr(related_model, related_pk) == fk_value
+ )
+ else:
+ # This side doesn't have foreign key, related side does
+ pk_field_name = self._get_pk_field(obj)
+ pk_value = getattr(obj, pk_field_name, None)
+
+ if pk_value is None:
+ result = None
+
+ # Find the foreign key field on related model
+ fk_field_name = self._find_foreign_key_on_related(
+ related_model, obj.__class__
+ )
+
+ query = select(related_model).where(
+ getattr(related_model, fk_field_name) == pk_value
+ )
+
+ query._bind(session)
+
+ if isinstance(session, AsyncSession):
+
+ async def async_fetch():
+ return await query._first_async()
+
+ result = loop.run(async_fetch())
+ else:
+ result = query._first()
+
+ # Cache the result
+ self._set_cache(obj, result)
+
+ return result
+
+ def _load_many_to_many(self, obj: NexiosModel, session: Any) -> List[Any]:
+ from nexios.orm.misc.event_loop import NexiosEventLoop
+ from nexios.orm.query.builder import select
+ from nexios.orm.sessions import AsyncSession
+
+ cached = self._get_cache(obj)
+ if cached is not _NOT_LOADED:
+ return cached
+
+ loop = NexiosEventLoop()
+ results: List[NexiosModel] = []
+ related_model = self._relationship_info.related_model
+ through_model = self._relationship_info.through_model
+
+ association_table = self._relationship_info.association_table
+
+ if not association_table:
+ raise ValueError(
+ f"No association table defined for many-to-many relationship {self._relationship_info.field_name}"
+ )
+
+ if not through_model:
+ raise ValueError(
+ f"No through model specified for many-to-many relationship '{self.field_name}'"
+ )
+
+ pk_name = self._get_pk_field(obj)
+ pk_value = getattr(obj, pk_name, None)
+
+ assert related_model is not None
+
+ related_pk = self._get_pk_field(related_model)
+
+ assert related_model is not None
+
+ local_col = (
+ self._relationship_info.local_column
+ or f"{to_snake_case(obj.__class__.__name__)}_id"
+ )
+ foreign_col = (
+ self._relationship_info.foreign_column
+ or f"{to_snake_case(related_model.__name__)}_id"
+ )
+
+ query_ids = select(getattr(through_model, foreign_col)).where(
+ getattr(through_model, local_col) == pk_value
+ )
+
+ query_ids._bind(session)
+
+ def _fetch_m2m():
+ from nexios.orm.sessions import AsyncSession
+
+ if isinstance(session, AsyncSession):
+ # This needs to be run inside the loop
+ async def _async_logic():
+ rows = await query_ids._all_async()
+ ids = [
+ row[0] if isinstance(row, (list, tuple)) else row
+ for row in rows
+ ]
+ if not ids:
+ return []
+ q = select(related_model).where(
+ getattr(related_model, related_pk).in_(ids)
+ )
+ q._bind(session)
+ return await q._all_async()
+
+ return loop.run(_async_logic())
+ else:
+ rows = query_ids._all()
+ ids = [
+ row[0] if isinstance(row, (list, tuple)) else row for row in rows
+ ]
+ if not ids:
+ return []
+ q = select(related_model).where(
+ getattr(related_model, related_pk).in_(ids)
+ )
+ q._bind(session)
+ return q._all()
+
+ results = _fetch_m2m()
+ self._set_cache(obj, results)
+ return results
+
+ def _load_dynamic(self, obj: NexiosModel, session: Any) -> Select:
+ from nexios.orm.query.builder import select, Select
+ from nexios.orm.sessions import AsyncSession
+
+ from nexios.orm.misc.event_loop import NexiosEventLoop
+
+ _loop = NexiosEventLoop()
+
+ pk_name = self._get_pk_field(obj)
+ pk_value = getattr(obj, pk_name, None)
+
+ related_model = self._relationship_info.related_model
+
+ if self._relationship_info.relationship_type == RelationshipType.ONE_TO_MANY:
+ assert related_model is not None
+ fk_field_name = self._find_foreign_key_on_related(
+ related_model, obj.__class__
+ )
+
+ query = select(related_model).where(
+ getattr(related_model, fk_field_name) == pk_value
+ )
+
+ query._bind(session)
+ return query
+ elif self._relationship_info.relationship_type == RelationshipType.MANY_TO_MANY:
+ through_model = self._relationship_info.through_model
+ if not through_model:
+ raise ValueError("No through model for many-to-many dynamic loading")
+
+ class DynamicManyToManyQuery:
+ def __init__(
+ self, obj, rel_info, session, pk_name, loop
+ ) -> types.NoneType:
+ self.obj = obj
+ self.rel_info = rel_info
+ self.session = session
+ self.pk_name = pk_name
+ self._query = None
+ self.loop = loop
+
+ def where(self, *conditions):
+ if not self._query:
+ self._build_query()
+ self._query = self._query.where(*conditions) # type: ignore
+ return self
+
+ def _build_query(self):
+ related_model = self.rel_info.related_model
+ through_model = self.rel_info.through_model
+
+ local_col = (
+ self.rel_info.local_column
+ or f"{self.obj.__class__.__name__.lower()}_id"
+ )
+ foreign_col = (
+ self.rel_info.foreign_column
+ or f"{related_model.__name__.lower()}_id"
+ )
+
+ from nexios.orm.query.builder import select
+
+ self._query = (
+ select(related_model)
+ .join(
+ through_model,
+ getattr(through_model, foreign_col)
+ == getattr(related_model, self.pk_name),
+ )
+ .where(
+ getattr(through_model, local_col)
+ == getattr(self.obj, self.pk_name)
+ )
+ )
+ self._query._bind(self.session)
+ return self
+
+ @overload
+ def all(self): ...
+
+ @overload
+ async def all(self): ...
+
+ def all(self):
+ if not self._query:
+ self._build_query()
+ if isinstance(self.session, AsyncSession):
+
+ async def all_async():
+ return await self._query._all_async() # type: ignore
+
+ return self.loop.run(all_async())
+ else:
+ return self._query._all() # type: ignore
+
+ return cast(
+ Select,
+ DynamicManyToManyQuery(
+ obj, self._relationship_info, session, pk_name, _loop
+ ),
+ )
+ elif self._relationship_info.relationship_type == RelationshipType.MANY_TO_ONE:
+ fk_field = self._relationship_info.foreign_key
+ if not fk_field:
+ raise ValueError(
+ f"No foreign key specified for many to one relationship {fk_field}"
+ )
+ fk_value = getattr(obj, fk_field, None)
+
+ class DynamicManyToOneQuery:
+ def __init__(
+ self, obj, rel_info, session, fk_field, fk_value
+ ) -> types.NoneType:
+ self.obj = obj
+ self.rel_info = rel_info
+ self.session = session
+ self.fk_field = fk_field
+ self.fk_value = fk_value
+ self._query = None
+
+ def where(self, *conditions):
+ if not self._query:
+ self._build_query()
+ self._query = self._query.where(*conditions) # type: ignore
+ return self
+
+ def _build_query(self):
+ related_model = self.rel_info.related_model
+ from nexios.orm.query.builder import select
+
+ if self.fk_value is None:
+ # Return a query that will always yield empty results
+ self._query = select(related_model).where(False)
+ else:
+ pk_field = related_model.get_primary_key()
+ if isinstance(pk_field, (list, tuple)):
+ pk_field = pk_field[0]
+ self._query = select(related_model).where(
+ getattr(related_model, pk_field) == self.fk_value
+ )
+ return self
+
+ def first(self):
+ if not self._query:
+ self._build_query()
+ return self._query._first() # type: ignore
+
+ async def first_async(self):
+ if not self._query:
+ self._build_query()
+ return await self._query._first_async() # type: ignore
+
+ def all(self):
+ if not self._query:
+ self._build_query()
+ result = self._query._first() # type: ignore
+ return [result] if result is not None else []
+
+ async def all_async(self):
+ if not self._query:
+ self._build_query()
+ # For MANY_TO_ONE, _all_async() should return a list with 0 or 1 item
+ result = await self._query._first_async() # type: ignore
+ return [result] if result is not None else []
+
+ return cast(
+ Select,
+ DynamicManyToOneQuery(
+ obj, self._relationship_info, session, fk_field, fk_value
+ ),
+ )
+ else:
+ raise ValueError(
+ f"Unsupported relationship type: {self._relationship_info.relationship_type}"
+ )
diff --git a/nexios/orm/engine.py b/nexios/orm/engine.py
new file mode 100644
index 00000000..547e3b83
--- /dev/null
+++ b/nexios/orm/engine.py
@@ -0,0 +1,113 @@
+from logging import getLogger
+from typing import Optional, overload
+from typing_extensions import Tuple, Any
+from nexios.orm.manager import AsyncDatabaseManager, DatabaseManager
+from nexios.orm.connection import AsyncDatabaseConnection, SyncDatabaseConnection
+
+
+class Engine:
+ """Database engine"""
+
+ def __init__(
+ self,
+ url: Optional[str] = None,
+ echo: bool = False,
+ pool_size: int = 10,
+ min_pool_size: int = 1,
+ use_pool: bool = True,
+ **kwargs,
+ ):
+ self.url = url
+ self.echo = echo
+ self.pool_size = pool_size
+ self.min_pool_size = min_pool_size
+ self.use_pool = use_pool
+ self.kwargs = kwargs
+
+ manager_kwargs = kwargs.copy()
+ manager_kwargs.update({
+ 'use_pool': use_pool,
+ 'pool_min_size': min_pool_size,
+ 'pool_max_size': pool_size
+ })
+
+ # Initialize managers - they handle detection internally
+ self.db_manager = DatabaseManager(url=url, **manager_kwargs)
+ self.async_db_manager = AsyncDatabaseManager(url=url, **manager_kwargs)
+
+ # Get detected dialect and driver from managers
+ self.dialect = self.db_manager.db_type
+ self.driver = self.db_manager.driver
+
+ self.logger = getLogger(__name__)
+
+ def connect(self) -> SyncDatabaseConnection:
+ """Get a sync connection from pool or create direct connection"""
+ return self.db_manager.connect()
+
+ def return_connection(self, conn: SyncDatabaseConnection) -> None:
+ """Return a sync connection to pool or close it"""
+ self.db_manager.return_connection(conn)
+
+ def sync_cursor(self):
+ return self.db_manager.cursor()
+
+ async def async_connect(self) -> AsyncDatabaseConnection:
+ """Get an async connection from pool or create direct connection"""
+ return await self.async_db_manager.connect()
+
+ async def return_async_connection(self, conn: AsyncDatabaseConnection) -> None:
+ """Return an async connection to pool or close it"""
+ await self.async_db_manager.return_connection(conn)
+
+ def _log_sql(self, sql: str, parameters: Tuple[Any, ...] = ()) -> None:
+ """Log SQL statements if echo is enabled"""
+ if self.echo:
+ self.logger.info("SQL: %s, Parameters: %s", sql, parameters)
+
+ def close(self) -> None:
+ """Close all connections and pools"""
+ self.db_manager.close()
+
+ async def aclose(self) -> None:
+ """Async close all connections and pools"""
+ await self.async_db_manager.close()
+
+# with pool
+@overload
+def create_engine(
+ url: Optional[str] = None,
+ *,
+ echo: bool = False,
+ pool_size: int = 10,
+ min_pool_size: int = 1,
+ use_pool: bool = True,
+ **kwargs
+) -> Engine: ...
+
+@overload
+def create_engine(
+ url: Optional[str] = None,
+ *,
+ echo: bool = False,
+ **kwargs
+) -> Engine: ...
+
+def create_engine(
+ url: Optional[str] = None,
+ *,
+ echo: bool = False,
+ pool_size: int = 10,
+ min_pool_size: int = 1,
+ use_pool: bool = True,
+ **kwargs
+) -> Engine:
+ """Create a database engine from URL or parameters"""
+ return Engine(
+ url=url,
+ echo=echo,
+ pool_size=pool_size,
+ min_pool_size=min_pool_size,
+ use_pool=use_pool,
+ **kwargs
+ )
diff --git a/nexios/orm/fields.py b/nexios/orm/fields.py
new file mode 100644
index 00000000..9a6611d8
--- /dev/null
+++ b/nexios/orm/fields.py
@@ -0,0 +1,185 @@
+from __future__ import annotations
+
+from typing import (
+ Union,
+ Optional,
+ Dict,
+ Callable,
+ Any,
+ overload,
+)
+
+from pydantic_core import (
+ PydanticUndefined as Undefined,
+ PydanticUndefinedType as UndefinedType,
+)
+from pydantic.fields import FieldInfo as PydanticFieldInfo
+
+from nexios.orm.utils import OnDeleteOrUpdate
+
+
+class FieldInfo(PydanticFieldInfo):
+ def __init__(self, default: Any = Undefined, **kwargs: Any) -> None:
+ primary_key = kwargs.pop("primary_key", Undefined)
+ nullable = kwargs.pop("nullable", Undefined)
+ foreign_key = kwargs.pop("foreign_key", Undefined)
+ ondelete = kwargs.pop("ondelete", Undefined)
+ unique = kwargs.pop("unique", Undefined)
+ index = kwargs.pop("index", Undefined)
+ auto_increment = kwargs.pop("auto_increment", False)
+
+ if auto_increment and default is Undefined:
+ default = None
+
+ super().__init__(default=default, **kwargs)
+ self.primary_key = primary_key
+ self.nullable = nullable
+ self.foreign_key = foreign_key
+ self.ondelete = ondelete
+ self.unique = unique
+ self.index = index
+ self.auto_increment = auto_increment
+
+
+@overload
+def Field(
+ default: Any = Undefined,
+ *,
+ default_factory: Optional[Callable[[], Any]] = None,
+ title: Optional[str] = None,
+ description: Optional[str] = None,
+ alias: Optional[str] = None,
+ const: Optional[bool] = None,
+ gt: Optional[float] = None,
+ ge: Optional[float] = None,
+ lt: Optional[float] = None,
+ le: Optional[float] = None,
+ multiple_of: Optional[float] = None,
+ max_digits: Optional[int] = None,
+ decimal_places: Optional[int] = None,
+ min_items: Optional[int] = None,
+ max_items: Optional[int] = None,
+ unique_items: Optional[bool] = None,
+ min_length: Optional[int] = None,
+ max_length: Optional[int] = None,
+ allow_mutation: bool = True,
+ regex: Optional[str] = None,
+ discriminator: Optional[str] = None,
+ repr: bool = True,
+ primary_key: Union[bool, UndefinedType] = Undefined,
+ auto_increment: bool = False,
+ foreign_key: Any = Undefined,
+ unique: Union[bool, UndefinedType] = Undefined,
+ nullable: Union[bool, UndefinedType] = Undefined,
+ index: Union[bool, UndefinedType] = Undefined,
+ schema_extra: Optional[Dict[str, Any]] = None,
+) -> Any: ...
+
+
+@overload
+def Field(
+ default: Any = Undefined,
+ *,
+ default_factory: Optional[Callable[[], Any]] = None,
+ title: Optional[str] = None,
+ description: Optional[str] = None,
+ alias: Optional[str] = None,
+ const: Optional[bool] = None,
+ gt: Optional[float] = None,
+ ge: Optional[float] = None,
+ lt: Optional[float] = None,
+ le: Optional[float] = None,
+ multiple_of: Optional[float] = None,
+ max_digits: Optional[int] = None,
+ decimal_places: Optional[int] = None,
+ min_items: Optional[int] = None,
+ max_items: Optional[int] = None,
+ unique_items: Optional[bool] = None,
+ min_length: Optional[int] = None,
+ max_length: Optional[int] = None,
+ allow_mutation: bool = True,
+ regex: Optional[str] = None,
+ discriminator: Optional[str] = None,
+ repr: bool = True,
+ primary_key: Union[bool, UndefinedType] = Undefined,
+ auto_increment: bool = False,
+ foreign_key: str,
+ ondelete: Union[OnDeleteOrUpdate, UndefinedType] = Undefined,
+ onupdate: Union[OnDeleteOrUpdate, UndefinedType] = Undefined,
+ unique: Union[bool, UndefinedType] = Undefined,
+ nullable: Union[bool, UndefinedType] = Undefined,
+ index: Union[bool, UndefinedType] = Undefined,
+ schema_extra: Optional[Dict[str, Any]] = None,
+) -> Any: ...
+
+
+def Field(
+ default: Any = Undefined,
+ *,
+ default_factory: Optional[Callable[[], Any]] = None,
+ title: Optional[str] = None,
+ description: Optional[str] = None,
+ alias: Optional[str] = None,
+ const: Optional[bool] = None,
+ gt: Optional[float] = None,
+ ge: Optional[float] = None,
+ lt: Optional[float] = None,
+ le: Optional[float] = None,
+ multiple_of: Optional[float] = None,
+ max_digits: Optional[int] = None,
+ decimal_places: Optional[int] = None,
+ min_items: Optional[int] = None,
+ max_items: Optional[int] = None,
+ unique_items: Optional[bool] = None,
+ min_length: Optional[int] = None,
+ max_length: Optional[int] = None,
+ allow_mutation: bool = True,
+ regex: Optional[str] = None,
+ discriminator: Optional[str] = None,
+ repr: bool = True,
+ primary_key: Union[bool, UndefinedType] = Undefined,
+ auto_increment: bool = False,
+ foreign_key: Any = Undefined,
+ ondelete: Union[OnDeleteOrUpdate, UndefinedType] = Undefined,
+ onupdate: Union[OnDeleteOrUpdate, UndefinedType] = Undefined,
+ unique: Union[bool, UndefinedType] = Undefined,
+ nullable: Union[bool, UndefinedType] = Undefined,
+ index: Union[bool, UndefinedType] = Undefined,
+ schema_extra: Optional[Dict[str, Any]] = None,
+) -> Any:
+ current_schema_extra = schema_extra or {}
+
+ field_info = FieldInfo(
+ default,
+ default_factory=default_factory,
+ alias=alias,
+ title=title,
+ description=description,
+ const=const,
+ gt=gt,
+ ge=ge,
+ lt=lt,
+ le=le,
+ multiple_of=multiple_of,
+ max_digits=max_digits,
+ decimal_places=decimal_places,
+ min_items=min_items,
+ max_items=max_items,
+ unique_items=unique_items,
+ min_length=min_length,
+ max_length=max_length,
+ allow_mutation=allow_mutation,
+ regex=regex,
+ discriminator=discriminator,
+ repr=repr,
+ primary_key=primary_key,
+ auto_increment=auto_increment,
+ foreign_key=foreign_key,
+ ondelete=ondelete,
+ onupdate=onupdate,
+ unique=unique,
+ nullable=nullable,
+ index=index,
+ **current_schema_extra,
+ )
+ return field_info
diff --git a/nexios/orm/manager.py b/nexios/orm/manager.py
new file mode 100644
index 00000000..e1c0bc9e
--- /dev/null
+++ b/nexios/orm/manager.py
@@ -0,0 +1,272 @@
+from __future__ import annotations
+
+import logging
+from typing import Dict
+
+import aiomysql
+import pg8000.dbapi
+from typing_extensions import Any, Optional
+
+from nexios.orm.config import DatabaseDetector, MySQLDriver, PostgreSQLDriver, SQLiteDialect, PostgreSQLDialect, \
+ MySQLDialect, SQLiteDriver
+from nexios.orm.connection import (
+ AsyncCursor,
+ AsyncDatabaseConnection,
+ SyncCursor,
+ SyncDatabaseConnection,
+)
+from nexios.orm.dbapi.mysql.aiomysql_ import MySQLAioMySQLConnection
+from nexios.orm.dbapi.mysql.asyncmy_ import AsyncMyConnection
+from nexios.orm.dbapi.mysql.mariadb_ import MariaDBConnection
+from nexios.orm.dbapi.mysql.mysql_client import MySQLClientConnection
+from nexios.orm.dbapi.mysql.mysql_connector_ import MySQLConnectorConnection
+from nexios.orm.dbapi.mysql.pymysql_ import PyMySQLConnection
+from nexios.orm.dbapi.postgres.aiopg_ import AioPgConnection
+from nexios.orm.dbapi.postgres.async_psycopg_ import AsyncPsycopgConnection
+from nexios.orm.dbapi.postgres.asyncpg_ import AsyncPgConnection
+from nexios.orm.dbapi.postgres.pg8000_ import Pg8000Connection
+from nexios.orm.dbapi.postgres.psycopg_ import PsycopgConnection
+from nexios.orm.dbapi.sqlite.aiosqlite_ import AioSQLiteConnection
+from nexios.orm.dbapi.sqlite.apsw_ import ApswConnection
+from nexios.orm.dbapi.sqlite.sqlite_ import SQLiteConnection
+from nexios.orm.pool.base import BaseAsyncConnectionPool, BaseConnectionPool
+from nexios.orm.pool.factory import ConnectionPoolFactory
+
+
+def _excluded_kwargs(**kwargs) -> Dict[str, Any]:
+ kwargs_copy = kwargs.copy()
+
+ kwargs_copy.pop('use_pool')
+ kwargs_copy.pop('pool_min_size')
+ kwargs_copy.pop('pool_max_size')
+ return kwargs_copy
+
+
+class DatabaseManager:
+ def __init__(self, url: Optional[str] = None, logger: Optional[logging.Logger] = None, **kwargs: Any):
+ self.logger = logger or logging.getLogger(__name__)
+
+ if url:
+ self.db_type, self.driver, connection_params = DatabaseDetector.detect_from_url(url, False)
+ connection_params.update(kwargs)
+ self.connection_params = _excluded_kwargs(**connection_params)
+ else:
+ self.db_type, self.driver = DatabaseDetector.detect_from_kwargs(kwargs, False)
+ self.connection_params = _excluded_kwargs(**kwargs)
+
+ self._connection: Optional[SyncDatabaseConnection] = None
+ self._connection_pool: Optional[BaseConnectionPool] = None
+ self._use_pool = kwargs.get('use_pool', False)
+ self._pool_min_size = kwargs.get('pool_min_size', 1)
+ self._pool_max_size = kwargs.get('pool_max_size', 10)
+
+ print(f"Database driver detected: {self.db_type}, {self.driver}, {self.connection_params}")
+
+ def connect(self) -> SyncDatabaseConnection:
+ if self._use_pool:
+ if self._connection_pool is None:
+ self._connection_pool = ConnectionPoolFactory.create_sync_pool(
+ connection=self._create_direct_connection,
+ min_size=self._pool_min_size,
+ max_size=self._pool_max_size,
+ **self.connection_params
+ )
+ conn = self._connection_pool.get_connection()
+ if conn is None:
+ raise RuntimeError("Connection pool returned no connection.")
+ return conn
+ else:
+ conn = self._create_direct_connection()
+ if conn is None:
+ raise RuntimeError("Failed to establish a DB connection.")
+ return conn
+
+ def _create_direct_connection(self) -> SyncDatabaseConnection:
+
+ if isinstance(self.db_type, SQLiteDialect):
+ if self.driver == SQLiteDriver.SQLITE3:
+ import sqlite3
+ raw_conn = sqlite3.connect(**self.connection_params)
+ raw_conn.execute("PRAGMA foreign_keys=ON")
+ self._connection = SQLiteConnection(raw_conn)
+ elif self.driver == SQLiteDriver.APSW:
+ import apsw
+ raw_conn = apsw.Connection(**self.connection_params)
+ raw_conn.execute("PRAGMA foreign_keys=ON")
+ self._connection = ApswConnection(raw_conn)
+ else:
+ raise ValueError(f"Unsupported SQL driver: {self.driver}")
+ elif isinstance(self.db_type, PostgreSQLDialect):
+ if self.driver == PostgreSQLDriver.PSYCOPG3:
+ import psycopg
+ raw_conn = psycopg.connect(**self.connection_params)
+ self._connection = PsycopgConnection(raw_conn)
+ elif self.driver == PostgreSQLDriver.PG8000:
+ import pg8000
+ import pg8000.dbapi
+ raw_conn = pg8000.dbapi.connect(**self.connection_params)
+ self._connection = Pg8000Connection(raw_conn)
+ else:
+ raise ValueError(f"Unsupported Postgres driver: {self.driver}")
+
+ elif isinstance(self.db_type, MySQLDialect):
+ if self.driver == MySQLDriver.MYSQL_CONNECTOR:
+ import mysql.connector
+ mysql_conn = mysql.connector.connect(**self.connection_params)
+ self._connection = MySQLConnectorConnection(mysql_conn) # type: ignore
+ elif self.driver == MySQLDriver.PYMySQL:
+ import pymysql
+ raw_conn = pymysql.connect(**self.connection_params)
+ self._connection = PyMySQLConnection(raw_conn)
+ elif self.driver == MySQLDriver.MARIADB:
+ import mariadb
+
+ raw_conn = mariadb.connect(**self.connection_params)
+ self._connection = MariaDBConnection(raw_conn)
+ elif self.driver == MySQLDriver.MYSQL_CLIENT:
+ import MySQLdb
+
+ raw_conn = MySQLdb.connect(**self.connection_params)
+ self._connection = MySQLClientConnection(raw_conn)
+ else:
+ raise ValueError(f"Unsupported MySQL driver: {self.driver}")
+
+ return self._connection # type: ignore
+
+ def return_connection(self, conn: SyncDatabaseConnection) -> None:
+ if self._use_pool and self._connection_pool:
+ self._connection_pool.return_connection(conn)
+ else:
+ conn.close()
+
+
+ def cursor(self) -> SyncCursor:
+ if self._connection is None:
+ print("WARNING: Connection is None, trying to establish connection...")
+ self._connection = self.connect()
+
+ if self._connection is None:
+ raise RuntimeError(
+ "Failed to establish a DB connection. "
+ f"db_type={self.db_type!r}, driver={self.driver!r}, params={self.connection_params}"
+ )
+
+ return self._connection.cursor()
+
+ def close(self) -> None:
+ if self._connection:
+ if not self._use_pool:
+ self._connection.close()
+ self._connection = None
+
+ if self._connection_pool:
+ self._connection_pool.close()
+ self._connection_pool = None
+
+
+class AsyncDatabaseManager:
+ def __init__(self, url: Optional[str] = None, logger: Optional[logging.Logger] = None, **kwargs: Any):
+ self.logger = logger or logging.getLogger(__name__)
+
+ if url:
+ self.db_type, self.driver, connection_params = DatabaseDetector.detect_from_url(url, True)
+ connection_params.update(kwargs)
+ self.connection_params = _excluded_kwargs(**connection_params)
+ else:
+ self.db_type, self.driver = DatabaseDetector.detect_from_kwargs(kwargs, True)
+ self.connection_params = _excluded_kwargs(**kwargs)
+
+ self._connection: Optional[AsyncDatabaseConnection] = None
+ self._connection_pool: Optional['BaseAsyncConnectionPool'] = None
+ self._use_pool = kwargs.get('use_pool', False)
+ self._pool_min_size = kwargs.get('pool_min_size', 1)
+ self._pool_max_size = kwargs.get('pool_max_size', 10)
+
+ async def connect(self) -> AsyncDatabaseConnection:
+ if self._use_pool:
+ if self._connection_pool is None:
+ self._connection_pool = ConnectionPoolFactory.create_async_pool(
+ connection=self._create_async_direct_connection,
+ min_size=self._pool_min_size,
+ max_size=self._pool_max_size,
+ **self.connection_params
+ )
+ return await self._connection_pool.get_connection()
+ else:
+ return await self._create_async_direct_connection()
+
+ async def _create_async_direct_connection(self) -> AsyncDatabaseConnection:
+ if isinstance(self.db_type, SQLiteDialect):
+ import aiosqlite
+ raw_conn = await aiosqlite.connect(**self.connection_params)
+ await raw_conn.execute("PRAGMA foreign_keys=ON")
+ self._connection = AioSQLiteConnection(raw_conn)
+ elif isinstance(self.db_type, PostgreSQLDialect):
+ if self.driver == PostgreSQLDriver.PSYCOPG3_ASYNC:
+ import psycopg
+ raw_conn = await psycopg.AsyncConnection.connect(**self.connection_params)
+ self._connection = AsyncPsycopgConnection(raw_conn)
+ elif self.driver == PostgreSQLDriver.ASYNCPG:
+ from asyncpg import connect
+
+ if 'dbname' in self.connection_params:
+ self.connection_params['database'] = self.connection_params.pop('dbname')
+ self.connection_params.pop('sslmode', None)
+
+ raw_conn = await connect(**self.connection_params)
+ self._connection = AsyncPgConnection(raw_conn)
+ elif self.driver == PostgreSQLDriver.AIOPG:
+ import aiopg
+ raw_conn = await aiopg.connect(**self.connection_params)
+ self._connection = AioPgConnection(raw_conn)
+ else:
+ raise ValueError(f"Unsupported Postgres driver: {self.driver}")
+
+ elif isinstance(self.db_type, MySQLDialect):
+ if self.driver == MySQLDriver.AIOMYSQL:
+
+ if 'database' in self.connection_params:
+ self.connection_params['db'] = self.connection_params.pop('database')
+
+ raw_conn = await aiomysql.connect(**self.connection_params)
+ self._connection = MySQLAioMySQLConnection(raw_conn)
+
+ elif self.driver == MySQLDriver.ASYNCMY:
+ import asyncmy
+
+ raw_conn = await asyncmy.connect(**self.connection_params)
+ self._connection = AsyncMyConnection(raw_conn)
+ else:
+ raise ValueError(f"Unsupported MySQL driver: {self.driver}")
+ else:
+ raise ValueError(f"Unsupported database type: {self.db_type}")
+ return self._connection
+
+ async def return_connection(self, conn: AsyncDatabaseConnection):
+ if self._use_pool and self._connection_pool:
+ await self._connection_pool.return_connection(conn)
+ else:
+ await conn.close()
+
+ async def cursor(self) -> AsyncCursor:
+ if self._connection is None:
+ print("WARNING: Connection is None, trying to establish connection...")
+ self._connection = await self.connect()
+
+ if self._connection is None:
+ raise RuntimeError(
+ "Failed to establish a DB connection. "
+ f"db_type={self.db_type!r}, driver={self.driver!r}, params={self.connection_params}"
+ )
+ return await self._connection.cursor()
+
+
+ async def close(self) -> None:
+ if self._connection:
+ if not self._use_pool:
+ await self._connection.close()
+ self._connection = None
+
+ if self._connection_pool:
+ await self._connection_pool.close()
+ self._connection_pool = None
diff --git a/nexios/orm/migration.py b/nexios/orm/migration.py
new file mode 100644
index 00000000..ae3a13b0
--- /dev/null
+++ b/nexios/orm/migration.py
@@ -0,0 +1,704 @@
+from __future__ import annotations
+
+import hashlib
+import importlib.util
+import inspect
+import logging
+import sys
+from dataclasses import dataclass, field
+from datetime import datetime, timezone
+from enum import Enum
+from pathlib import Path
+from time import perf_counter
+from typing import Any, Dict, List, Optional, Tuple, Type, Union
+
+from nexios.orm.config import generate_placeholders, get_param_placeholder
+from nexios.orm.misc.event_loop import NexiosEventLoop
+from nexios.orm.model import NexiosModel
+from nexios.orm.sessions import AsyncSession, Session
+
+
+def utcnow():
+ return datetime.now(timezone.utc)
+
+class MigrationStatus(str, Enum):
+ PENDING = "pending"
+ RUNNING = "running"
+ COMPLETED = "completed"
+ FAILED = "failed"
+ ROLLED_BACK = "rolled_back"
+
+
+@dataclass(slots=True)
+class Migration:
+ name: str
+ version: str
+ up_sql: str
+ down_sql: str = ""
+ created_at: datetime = field(default_factory=utcnow)
+
+ def checksum(self) -> str:
+ payload = f"{self.name}|{self.version}|{self.up_sql}|{self.down_sql}"
+ return hashlib.sha256(payload.encode("utf-8")).hexdigest()
+
+
+class MigrationError(Exception):
+ pass
+
+
+class MigrationValidationError(MigrationError):
+ pass
+
+
+class MigrationExecutionError(MigrationError):
+ pass
+
+
+class MigrationLoader:
+ def __init__(self, migrations_dir: Union[str, Path], logger: logging.Logger) -> None:
+ self.migrations_dir = Path(migrations_dir)
+ self.logger = logger
+
+ def load(self) -> Dict[str, Migration]:
+ migrations: Dict[str, Migration] = {}
+
+ if not self.migrations_dir.exists():
+ return migrations
+
+ files = sorted(
+ f for f in self.migrations_dir.iterdir()
+ if f.is_file() and f.suffix == ".py" and not f.name.startswith("_")
+ )
+
+ for file_path in files:
+ migration = self._load_single(file_path)
+
+ if migration.version in migrations:
+ raise MigrationValidationError(
+ f"Duplicate migration version detected: {migration.version}"
+ )
+
+ migrations[migration.version] = migration
+
+ return migrations
+
+ def _load_single(self, file_path: Path) -> Migration:
+ module_name = f"migrations.{file_path.stem}"
+
+ try:
+ spec = importlib.util.spec_from_file_location(module_name, str(file_path))
+ if spec is None or spec.loader is None:
+ raise MigrationValidationError(f"Could not load spec for {file_path.name}")
+
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+
+ if not hasattr(module, "get_migration"):
+ raise MigrationValidationError(
+ f"{file_path.name} does not define get_migration()"
+ )
+
+ migration = module.get_migration()
+
+ if not isinstance(migration, Migration):
+ raise MigrationValidationError(
+ f"{file_path.name} get_migration() must return Migration"
+ )
+
+ return migration
+
+ except Exception as exc:
+ raise MigrationValidationError(
+ f"Failed to load migration '{file_path.name}': {exc}"
+ ) from exc
+
+
+class SessionAdapter:
+ """
+ Thin wrapper over sync/async session behavior.
+
+ Assumptions:
+ - execute(sql, params) works for both Session and AsyncSession
+ - sync session may expose begin/commit/rollback
+ - async session may expose begin/commit/rollback as coroutines
+ """
+
+ def __init__(self, session: Union[Session, AsyncSession]) -> None:
+ self.session = session
+ self._loop = NexiosEventLoop()
+ self.is_async = isinstance(session, AsyncSession)
+
+ @property
+ def driver(self) -> str:
+ return getattr(getattr(self.session, "engine", None), "driver", "sqlite3")
+
+ @property
+ def ddl(self) -> Any:
+ return getattr(self.session, "_ddl", None)
+
+ def execute(self, sql: str, params: Tuple[Any, ...] = ()) -> Any:
+ if self.is_async:
+ async def _run() -> Any:
+ return await self.session.execute(sql, params) # type: ignore[arg-type]
+ return self._loop.run(_run())
+
+ return self.session.execute(sql, params) # type: ignore[arg-type]
+
+ def fetchall(self, sql: str, params: Tuple[Any, ...] = ()) -> List[Any]:
+ if self.is_async:
+ async def _run() -> List[Any]:
+ result = await self.session.execute(sql, params) # type: ignore[arg-type]
+ return await result.fetchall()
+ return self._loop.run(_run())
+
+ result = self.session.execute(sql, params) # type: ignore[arg-type]
+ return result.fetchall() #type: ignore[arg-type]
+
+ def begin(self) -> None:
+ begin_fn = getattr(self.session, "begin", None)
+ if begin_fn is None:
+ return
+
+ if self.is_async:
+ async def _run() -> None:
+ maybe = begin_fn()
+ if inspect.isawaitable(maybe):
+ await maybe
+ self._loop.run(_run())
+ return
+
+ maybe = begin_fn()
+ if inspect.isawaitable(maybe):
+ raise RuntimeError("Sync path received awaitable begin()")
+
+ def commit(self) -> None:
+ commit_fn = getattr(self.session, "commit", None)
+ if commit_fn is None:
+ return
+
+ if self.is_async:
+ async def _run() -> None:
+ maybe = commit_fn()
+ if inspect.isawaitable(maybe):
+ await maybe
+ self._loop.run(_run())
+ return
+
+ maybe = commit_fn()
+ if inspect.isawaitable(maybe):
+ raise RuntimeError("Sync path received awaitable commit()")
+
+ def rollback(self) -> None:
+ rollback_fn = getattr(self.session, "rollback", None)
+ if rollback_fn is None:
+ return
+
+ if self.is_async:
+ async def _run() -> None:
+ maybe = rollback_fn()
+ if inspect.isawaitable(maybe):
+ await maybe
+ self._loop.run(_run())
+ return
+
+ maybe = rollback_fn()
+ if inspect.isawaitable(maybe):
+ raise RuntimeError("Sync path received awaitable rollback()")
+
+
+class MigrationRepository:
+ TABLE_NAME = "_migrations"
+
+ def __init__(self, db: SessionAdapter, logger: logging.Logger) -> None:
+ self.db = db
+ self.logger = logger
+ self._ensured = False
+
+ def ensure_table(self) -> None:
+ if self._ensured:
+ return
+
+ sql = f"""
+ CREATE TABLE IF NOT EXISTS {self.TABLE_NAME} (
+ version TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ status TEXT NOT NULL,
+ checksum TEXT NOT NULL,
+ applied_at TIMESTAMP NULL,
+ execution_time INTEGER NULL
+ )
+ """.strip()
+
+ self.db.execute(sql)
+ self._ensured = True
+
+ def all(self) -> List[Dict[str, Any]]:
+ self.ensure_table()
+
+ rows = self.db.fetchall(
+ f"""
+ SELECT version, name, status, checksum, applied_at, execution_time
+ FROM {self.TABLE_NAME}
+ ORDER BY version
+ """.strip()
+ )
+
+ return [
+ {
+ "version": row[0],
+ "name": row[1],
+ "status": row[2],
+ "checksum": row[3],
+ "applied_at": row[4],
+ "execution_time": row[5],
+ }
+ for row in rows
+ ]
+
+ def by_version(self, version: str) -> Optional[Dict[str, Any]]:
+ self.ensure_table()
+
+ rows = self.db.fetchall(
+ f"""
+ SELECT version, name, status, checksum, applied_at, execution_time
+ FROM {self.TABLE_NAME}
+ WHERE version = {get_param_placeholder(self.db.driver)}
+ """.strip(),
+ (version,),
+ )
+
+ if not rows:
+ return None
+
+ row = rows[0]
+ return {
+ "version": row[0],
+ "name": row[1],
+ "status": row[2],
+ "checksum": row[3],
+ "applied_at": row[4],
+ "execution_time": row[5],
+ }
+
+ def record_running(self, migration: Migration) -> None:
+ existing = self.by_version(migration.version)
+
+ if existing is None:
+ placeholders = generate_placeholders(
+ driver=self.db.driver,
+ count=4,
+ start_index=1,
+ )
+ self.db.execute(
+ f"""
+ INSERT INTO {self.TABLE_NAME} (version, name, status, checksum)
+ VALUES ({placeholders})
+ """.strip(),
+ (
+ migration.version,
+ migration.name,
+ MigrationStatus.RUNNING.value,
+ migration.checksum(),
+ ),
+ )
+ return
+
+ p = get_param_placeholder(self.db.driver)
+ self.db.execute(
+ f"""
+ UPDATE {self.TABLE_NAME}
+ SET name = {p},
+ status = {p},
+ checksum = {p},
+ applied_at = NULL,
+ execution_time = NULL
+ WHERE version = {p}
+ """.strip(),
+ (
+ migration.name,
+ MigrationStatus.RUNNING.value,
+ migration.checksum(),
+ migration.version,
+ ),
+ )
+
+ def record_completed(self, migration: Migration, execution_time_ms: int) -> None:
+ p = get_param_placeholder(self.db.driver)
+ self.db.execute(
+ f"""
+ UPDATE {self.TABLE_NAME}
+ SET status = {p},
+ applied_at = CURRENT_TIMESTAMP,
+ execution_time = {p},
+ checksum = {p}
+ WHERE version = {p}
+ """.strip(),
+ (
+ MigrationStatus.COMPLETED.value,
+ execution_time_ms,
+ migration.checksum(),
+ migration.version,
+ ),
+ )
+
+ def record_failed(self, migration: Migration) -> None:
+ p = get_param_placeholder(self.db.driver)
+ self.db.execute(
+ f"""
+ UPDATE {self.TABLE_NAME}
+ SET status = {p}
+ WHERE version = {p}
+ """.strip(),
+ (
+ MigrationStatus.FAILED.value,
+ migration.version,
+ ),
+ )
+
+ def record_rolled_back(self, migration: Migration) -> None:
+ p = get_param_placeholder(self.db.driver)
+ self.db.execute(
+ f"""
+ UPDATE {self.TABLE_NAME}
+ SET status = {p}
+ WHERE version = {p}
+ """.strip(),
+ (
+ MigrationStatus.ROLLED_BACK.value,
+ migration.version,
+ ),
+ )
+
+
+class MigrationManager:
+ def __init__(
+ self,
+ session: Union[Session, AsyncSession],
+ migrations_dir: Union[str, Path] = "migrations",
+ ) -> None:
+ self.logger = logging.getLogger(self.__class__.__name__)
+ self.db = SessionAdapter(session)
+ self.repo = MigrationRepository(self.db, self.logger)
+ self.loader = MigrationLoader(migrations_dir, self.logger)
+ self.migrations_dir = Path(migrations_dir)
+ self.migrations: Dict[str, Migration] = self.loader.load()
+
+ def reload(self) -> None:
+ self.migrations = self.loader.load()
+
+ def add_migration(self, migration: Migration) -> None:
+ if migration.version in self.migrations:
+ raise MigrationValidationError(
+ f"Migration version already registered: {migration.version}"
+ )
+ self.migrations[migration.version] = migration
+
+ def _scan_models(self) -> List[Type[NexiosModel]]:
+ models: List[Type[NexiosModel]] = []
+
+ for module_name, module in list(sys.modules.items()):
+ if not module or module_name.startswith(("_", "builtins", "sys")):
+ continue
+
+ try:
+ for _, obj in inspect.getmembers(module, inspect.isclass):
+ if not issubclass(obj, NexiosModel) or obj is NexiosModel:
+ continue
+
+ if obj.__module__ != module_name:
+ continue
+
+ is_table = bool(
+ getattr(obj, "model_config", {}).get("table")
+ or getattr(obj, "__tablename__", None)
+ )
+ if is_table:
+ models.append(obj)
+
+ except (ImportError, TypeError, AttributeError):
+ continue
+
+ # De-duplicate while preserving deterministic order
+ unique: Dict[str, Type[NexiosModel]] = {}
+ for model in models:
+ unique[f"{model.__module__}.{model.__name__}"] = model
+
+ return [unique[key] for key in sorted(unique.keys())]
+
+ def _build_snapshot_up_sql(self) -> str:
+ ddl = self.db.ddl
+ if ddl is None:
+ raise MigrationError("Session does not expose DDL generator via session._ddl")
+
+ statements: List[str] = []
+ for model in self._scan_models():
+ statements.append(ddl.create_table(model))
+
+ return "\n\n".join(statements)
+
+ def _build_snapshot_down_sql(self) -> str:
+ ddl = self.db.ddl
+ if ddl is None:
+ raise MigrationError("Session does not expose DDL generator via session._ddl")
+
+ statements: List[str] = []
+ for model in reversed(self._scan_models()):
+ statements.append(ddl.drop_table(model))
+
+ return "\n\n".join(statements)
+
+ def create_migration(self, name: str) -> str:
+ version = f"{utcnow().strftime('%Y%m%d%H%M%S')}_{name}"
+ self.migrations_dir.mkdir(parents=True, exist_ok=True)
+
+ migration_file = self.migrations_dir / f"{version}.py"
+
+ template = f'''"""
+Migration: {name}
+Version: {version}
+Created: {utcnow().isoformat()}Z
+"""
+
+from nexios.orm.migration import Migration
+
+
+def get_migration() -> Migration:
+ return Migration(
+ name="{name}",
+ version="{version}",
+ up_sql=\"\"\"{self._build_snapshot_up_sql()}\"\"\",
+ down_sql=\"\"\"{self._build_snapshot_down_sql()}\"\"\",
+ )
+'''
+
+ migration_file.write_text(template, encoding="utf-8")
+ self.logger.info("Created migration file: %s", migration_file)
+
+ self.reload()
+ return str(migration_file)
+
+ def applied_migrations(self) -> List[Dict[str, Any]]:
+ return self.repo.all()
+
+ def pending_migrations(self, target_version: Optional[str] = None) -> List[Migration]:
+ self.repo.ensure_table()
+ applied = {
+ row["version"]
+ for row in self.repo.all()
+ if row["status"] == MigrationStatus.COMPLETED.value
+ }
+
+ pending = [
+ self.migrations[version]
+ for version in sorted(self.migrations.keys())
+ if version not in applied
+ ]
+
+ if target_version is not None:
+ pending = [m for m in pending if m.version <= target_version]
+
+ return pending
+
+ def _split_sql_statements(self, sql: str) -> List[str]:
+ """
+ Conservative splitter.
+
+ Best practice is still one statement per execute block or storing statements
+ as a list, but this is already safer than a raw split(';') because it ignores
+ empty chunks and preserves content more cleanly.
+
+ If your migrations may contain procedures/triggers/functions with internal
+ semicolons, replace this with a dialect-aware parser.
+ """
+ parts = [part.strip() for part in sql.split(";")]
+ return [part for part in parts if part]
+
+ def _execute_migration_sql(self, sql: str) -> None:
+ for statement in self._split_sql_statements(sql):
+ self.db.execute(statement)
+
+ def migrate(self, target_version: Optional[str] = None) -> None:
+ self.repo.ensure_table()
+ pending = self.pending_migrations(target_version=target_version)
+
+ if not pending:
+ self.logger.info("No pending migrations to apply.")
+ return
+
+ self.logger.info("Found %d pending migrations.", len(pending))
+
+ for migration in pending:
+ self.logger.info(
+ "Applying migration %s (%s)",
+ migration.name,
+ migration.version,
+ )
+ start = perf_counter()
+
+ try:
+ self.db.begin()
+ self.repo.record_running(migration)
+
+ if migration.up_sql.strip():
+ self._execute_migration_sql(migration.up_sql)
+
+ execution_time_ms = int((perf_counter() - start) * 1000)
+ self.repo.record_completed(migration, execution_time_ms)
+ self.db.commit()
+
+ self.logger.info(
+ "Applied migration %s in %d ms",
+ migration.version,
+ execution_time_ms,
+ )
+
+ except Exception as exc:
+ try:
+ self.db.rollback()
+ except Exception:
+ self.logger.exception(
+ "Rollback failed while handling migration failure: %s",
+ migration.version,
+ )
+
+ try:
+ self.repo.record_failed(migration)
+ except Exception:
+ self.logger.exception(
+ "Failed to record migration failure state: %s",
+ migration.version,
+ )
+
+ raise MigrationExecutionError(
+ f"Failed to apply migration {migration.version}: {exc}"
+ ) from exc
+
+ def rollback(
+ self,
+ steps: int = 1,
+ target_version: Optional[str] = None,
+ ) -> None:
+ """
+ Rollback semantics:
+ - If target_version is provided:
+ rollback completed migrations newer than target_version.
+ target_version itself remains applied.
+ - Else:
+ rollback the latest `steps` completed migrations.
+ """
+ self.repo.ensure_table()
+
+ completed = [
+ row for row in self.repo.all()
+ if row["status"] == MigrationStatus.COMPLETED.value
+ ]
+ completed_sorted = sorted(completed, key=lambda x: x["version"], reverse=True)
+
+ if target_version is not None:
+ candidates = [
+ self.migrations[row["version"]]
+ for row in completed_sorted
+ if row["version"] > target_version and row["version"] in self.migrations
+ ]
+ else:
+ candidates = []
+ for row in completed_sorted:
+ migration = self.migrations.get(row["version"])
+ if migration is not None:
+ candidates.append(migration)
+ if len(candidates) >= steps:
+ break
+
+ rollbackable = [m for m in candidates if m.down_sql.strip()]
+
+ if not rollbackable:
+ self.logger.info("No migrations to rollback.")
+ return
+
+ self.logger.info("Rolling back %d migrations.", len(rollbackable))
+
+ for migration in rollbackable:
+ self.logger.info(
+ "Rolling back migration %s (%s)",
+ migration.name,
+ migration.version,
+ )
+ try:
+ self.db.begin()
+
+ if migration.down_sql.strip():
+ self._execute_migration_sql(migration.down_sql)
+
+ self.repo.record_rolled_back(migration)
+ self.db.commit()
+
+ self.logger.info("Rolled back migration %s", migration.version)
+
+ except Exception as exc:
+ try:
+ self.db.rollback()
+ except Exception:
+ self.logger.exception(
+ "Rollback transaction failed while reverting migration %s",
+ migration.version,
+ )
+
+ raise MigrationExecutionError(
+ f"Failed to rollback migration {migration.version}: {exc}"
+ ) from exc
+
+ def status(self) -> Dict[str, Any]:
+ rows = self.repo.all()
+ applied = [r for r in rows if r["status"] == MigrationStatus.COMPLETED.value]
+ rolled_back = [r for r in rows if r["status"] == MigrationStatus.ROLLED_BACK.value]
+ failed = [r for r in rows if r["status"] == MigrationStatus.FAILED.value]
+
+ applied_versions = {r["version"] for r in applied}
+ pending_count = len(set(self.migrations.keys()) - applied_versions)
+
+ return {
+ "applied": len(applied),
+ "pending": pending_count,
+ "rolled_back": len(rolled_back),
+ "failed": len(failed),
+ "total": len(self.migrations),
+ "migrations": rows,
+ }
+
+ def validate(self) -> List[str]:
+ self.repo.ensure_table()
+ issues: List[str] = []
+
+ applied = self.repo.all()
+ disk_versions = set(self.migrations.keys())
+
+ for row in applied:
+ version = row["version"]
+ migration = self.migrations.get(version)
+
+ if migration is None:
+ issues.append(
+ f"Migration {version} is recorded in DB but missing from files."
+ )
+ continue
+
+ actual_checksum = migration.checksum()
+ if row["checksum"] != actual_checksum:
+ issues.append(
+ f"Checksum mismatch for {version}: "
+ f"db={row['checksum']} file={actual_checksum}"
+ )
+
+ for version in sorted(disk_versions):
+ if not self._is_valid_version(version):
+ issues.append(f"Invalid migration version format: {version}")
+
+ return issues
+
+ @staticmethod
+ def _is_valid_version(version: str) -> bool:
+ if "_" not in version:
+ return False
+
+ prefix, _ = version.split("_", 1)
+ return len(prefix) == 14 and prefix.isdigit()
diff --git a/nexios/orm/misc/context.py b/nexios/orm/misc/context.py
new file mode 100644
index 00000000..6fdf67f3
--- /dev/null
+++ b/nexios/orm/misc/context.py
@@ -0,0 +1,77 @@
+from __future__ import annotations
+
+import contextvars
+import inspect
+from typing import Any, Dict, Optional
+
+class ContextData:
+ def __init__(self, name: str, default: Any = None):
+ self._var = contextvars.ContextVar(name, default=default)
+ self._name = name
+
+ def get(self):
+ return self._var.get()
+
+ def set(self, value):
+ return self._var.set(value)
+
+ def reset(self, token: contextvars.Token):
+ self._var.reset(token)
+
+ @property
+ def name(self):
+ return self._name
+
+_context_registry: Dict[str, ContextData] = {}
+
+class _ContextCache:
+ def __init__(self):
+ self._cache: Dict[Any, ContextData] = {}
+
+ def get_or_create(self, key: Any, name: Optional[str] = None, default: Optional[Any] = None) -> ContextData:
+ if key not in self._cache:
+ if name is None:
+ name = f"context_data_{id(key)}"
+ self._cache[key] = ContextData(name, default)
+ _context_registry[name] = self._cache[key]
+ return self._cache[key]
+
+_cache = _ContextCache()
+
+def context_data(key: Any, default: Optional[Any] = None, name: Optional[str] = None) -> ContextData:
+ return _cache.get_or_create(key, default, name)
+
+def get_context_data(key: Any):
+ data = context_data(key)
+ return data.get()
+
+def set_context_data(key: Any, value: Any):
+ data = context_data(key)
+ return data.set(value)
+
+def reset_context_data(key: Any, token: contextvars.Token):
+ data = context_data(key)
+ data.reset(token)
+
+def with_context(key: Any, value: Any):
+ def decorator(func):
+ if inspect.iscoroutinefunction(func):
+ async def async_wrapper(*args, **kwargs):
+ data = context_data(key)
+ token = data.set(value)
+ try:
+ return await func(*args, **kwargs)
+ finally:
+ data.reset(token)
+ return async_wrapper
+ else:
+ def sync_wrapper(*args, **kwargs):
+ data = context_data(key)
+ token = data.set(value)
+
+ try:
+ return func(*args, **kwargs)
+ finally:
+ data.reset(token)
+ return sync_wrapper
+ return decorator
diff --git a/nexios/orm/misc/event_loop.py b/nexios/orm/misc/event_loop.py
new file mode 100644
index 00000000..1e44e092
--- /dev/null
+++ b/nexios/orm/misc/event_loop.py
@@ -0,0 +1,248 @@
+from __future__ import annotations
+
+import atexit
+import concurrent.futures
+from dataclasses import dataclass
+from enum import Enum
+import os
+import platform
+import asyncio
+import sys
+import threading
+from typing import Any, Coroutine, List, Optional, TypeVar, Union
+
+import concurrent
+import warnings
+
+import nest_asyncio
+
+T = TypeVar("T")
+
+PYTHON_VERSION = sys.version_info
+PYTHON_311_PLUS = PYTHON_VERSION.major > 3 or (
+ PYTHON_VERSION.major == 3 and PYTHON_VERSION.minor >= 11
+)
+IS_WINDOWS = platform.system() == "Windows"
+
+USING_UVLOOP = False
+if not IS_WINDOWS:
+ try:
+ import uvloop
+
+ if not IS_WINDOWS:
+ loop = uvloop.new_event_loop()
+ asyncio.set_event_loop(loop)
+ USING_UVLOOP = True
+ except ImportError:
+ pass
+
+
+class LoopBackend(Enum):
+ ASYNCIO = "asyncio"
+ UVLOOP = "uvloop"
+
+ @classmethod
+ def get_available(cls) -> LoopBackend:
+ if USING_UVLOOP and not IS_WINDOWS:
+ return cls.UVLOOP
+ return cls.ASYNCIO
+
+
+@dataclass
+class ExecutionResult:
+ success: bool
+ result: Any = None
+ exception: Optional[Exception] = None
+
+ @property
+ def value(self):
+ if not self.success and self.exception:
+ raise self.exception
+ return self.result
+
+
+class NexiosEventLoop:
+ _instance: Optional[NexiosEventLoop] = None
+ _lock = threading.Lock()
+
+ def __new__(cls):
+ with cls._lock:
+ if cls._instance is None:
+ cls._instance = super().__new__(cls)
+ cls._instance._initialized = False
+ return cls._instance
+
+ def __init__(
+ self,
+ backend: Optional[LoopBackend] = None,
+ max_workers: Optional[int] = None,
+ enable_thread_pool: bool = False,
+ ) -> None:
+ if getattr(self, "_initialized", False):
+ return
+
+ self.backend = backend or LoopBackend.get_available()
+ self._loop: Optional[asyncio.AbstractEventLoop] = None
+ self._thread: Optional[threading.Thread] = None
+ self._ready_event = threading.Event()
+ self._shutdown_event = threading.Event()
+ self._initialized = True
+ self._thread_pool: Optional[concurrent.futures.ThreadPoolExecutor] = None
+
+ if enable_thread_pool:
+ self._thread_pool = concurrent.futures.ThreadPoolExecutor(
+ max_workers=max_workers or (os.cpu_count() or 4) * 5,
+ thread_name_prefix="NexiosEventLoop_Pool",
+ )
+ self._start_background_loop()
+ atexit.register(self.stop)
+
+ def _start_background_loop(self):
+ def run_loop():
+ try:
+ if self.backend == LoopBackend.UVLOOP and USING_UVLOOP:
+ import uvloop
+
+ self._loop = uvloop.new_event_loop()
+ else:
+ self._loop = asyncio.new_event_loop()
+
+ asyncio.set_event_loop(self._loop)
+
+ if self._loop and self._thread_pool:
+ self._loop.set_default_executor(self._thread_pool)
+
+ self._ready_event.set()
+ self._loop.run_forever()
+ except Exception as e:
+ warnings.warn(f"Event loop crushed: {e}")
+ finally:
+ if self._loop and not self._loop.is_closed():
+ pending = asyncio.all_tasks(self._loop)
+ for task in pending:
+ task.cancel()
+
+ if pending:
+ self._loop.run_until_complete(
+ asyncio.gather(*pending, return_exceptions=True)
+ )
+
+ self._loop.close()
+
+ self._thread = threading.Thread(
+ target=run_loop, daemon=True, name=f"NexiosEventLoop-{self.backend.value}"
+ )
+ self._thread.start()
+
+ if not self._ready_event.wait(timeout=10.0):
+ raise RuntimeError("Failed to start event loop with timeout")
+
+ def run(self, coro: Coroutine[Any, Any, T], timeout: Optional[float] = None) -> T:
+ try:
+ running_loop = asyncio.get_running_loop()
+ except RuntimeError:
+ running_loop = None
+
+ if running_loop:
+ nest_asyncio.apply(running_loop)
+ return running_loop.run_until_complete(coro)
+
+ if PYTHON_311_PLUS and self._loop is None:
+ with asyncio.Runner() as runner:
+ return runner.run(coro)
+
+ if self._loop is None or self._loop.is_closed():
+ # raise RuntimeError("Event loop is not running")
+ return asyncio.run(coro)
+
+ future = asyncio.run_coroutine_threadsafe(coro, self._loop)
+
+ try:
+ return future.result(timeout=timeout)
+ except concurrent.futures.TimeoutError:
+ future.cancel()
+ raise TimeoutError(f"Operation timeouted out after {timeout} seconds")
+ except KeyboardInterrupt:
+ future.cancel()
+ raise
+ except Exception as e:
+ raise e
+
+ def run_multiple(
+ self,
+ coros: List[Coroutine[Any, Any, T]],
+ timeout: Optional[float] = None,
+ return_exceptions: bool = False,
+ ) -> List[Union[T, BaseException]]:
+
+ if not coros:
+ return []
+
+ if PYTHON_311_PLUS and self._loop is None:
+ with asyncio.Runner() as runner:
+ async def gather_wrapper():
+ tasks = [asyncio.create_task(coro) for coro in coros]
+ return await asyncio.gather(*tasks, return_exceptions=return_exceptions)
+
+ return runner.run(gather_wrapper())
+
+ async def gather_coros():
+ tasks = [self._create_task(coro) for coro in coros]
+ return await asyncio.gather(*tasks, return_exceptions=return_exceptions)
+ assert self._loop is not None
+ future = asyncio.run_coroutine_threadsafe(gather_coros(), self._loop)
+
+ try:
+ return future.result(timeout=timeout)
+ except concurrent.futures.TimeoutError:
+ future.cancel()
+ raise TimeoutError(f"Operation timeouted out after {timeout} seconds")
+
+ def run_batch(
+ self,
+ coros: List[Coroutine[Any, Any, T]],
+ batch_size: int = 10,
+ timeout_per_batch: Optional[float] = None,
+ ) -> List[ExecutionResult]:
+ results: List[ExecutionResult] = []
+ for i in range(0, len(coros), batch_size):
+ batch = coros[i : i + batch_size]
+ batch_results = self.run_multiple(
+ batch, timeout=timeout_per_batch, return_exceptions=True
+ )
+ for j, result in enumerate(batch_results):
+ if isinstance(result, Exception):
+ results.append(ExecutionResult(success=False, exception=result))
+ else:
+ results.append(ExecutionResult(success=True, result=result))
+ return results
+
+ def stop(self, timeout: float = 5.0):
+ if not self._initialized:
+ return
+
+ if self._loop and self._loop.is_running():
+ self._loop.call_soon_threadsafe(self._loop.stop)
+
+ self._shutdown_event.set()
+
+ if self._thread_pool:
+ self._thread_pool.shutdown(wait=False, cancel_futures=True)
+
+ if self._thread and self._thread.is_alive():
+ self._thread.join(timeout=timeout)
+
+ if self._thread.is_alive():
+ warnings.warn("NexiosEventLoop runner thread did not terminate gracefully")
+
+ self._initialized = False
+ NexiosEventLoop._instance = None
+
+ def _create_task(self, coro: Coroutine[Any, Any, T]) -> asyncio.Task[T]:
+ if self._loop and not self._loop.is_closed():
+ return self._loop.create_task(coro)
+ else:
+ return asyncio.create_task(coro)
+
+ def __del__(self):
+ self.stop()
diff --git a/nexios/orm/misc/exceptions.py b/nexios/orm/misc/exceptions.py
new file mode 100644
index 00000000..e69de29b
diff --git a/nexios/orm/misc/exp_backoff.py b/nexios/orm/misc/exp_backoff.py
new file mode 100644
index 00000000..6fb94e12
--- /dev/null
+++ b/nexios/orm/misc/exp_backoff.py
@@ -0,0 +1,4 @@
+import time
+
+def exponential_backoff(retry_delay: float, attempt: int) -> float:
+ return time.sleep(retry_delay * (2 ** (attempt - 1))) # type: ignore
\ No newline at end of file
diff --git a/nexios/orm/misc/refs.py b/nexios/orm/misc/refs.py
new file mode 100644
index 00000000..f5c4c473
--- /dev/null
+++ b/nexios/orm/misc/refs.py
@@ -0,0 +1,187 @@
+from __future__ import annotations
+
+import sys
+import builtins
+import re
+from typing import (
+ Any,
+ Dict,
+ List,
+ ForwardRef,
+ get_type_hints,
+)
+
+class ResolveForwardRefs:
+
+ @classmethod
+ def resolve_forward_references(cls):
+ localns = cls._get_local_namespace()
+ globalns = cls._get_global_namespace()
+
+ try:
+ resolved_hints = get_type_hints(cls, globalns=globalns, localns=localns)
+ cls.__annotations__.update(resolved_hints)
+ except Exception:
+ cls._custom_resolve_forward_references(globalns, localns)
+
+
+ @classmethod
+ def _get_global_namespace(cls) -> Dict[str, Any]:
+ globalns = {**builtins.__dict__}
+ import typing
+
+ globalns.update(typing.__dict__)
+ try:
+ import typing_extensions
+
+ globalns.update(typing_extensions.__dict__)
+ except ImportError:
+ pass
+ globalns.update(cls.__registry__) # type: ignore
+ module = sys.modules.get(cls.__module__)
+ if module:
+ globalns.update(getattr(module, "__dict__", {}))
+ return globalns
+
+ @classmethod
+ def _get_local_namespace(cls) -> Dict[str, Any]:
+ localns = {}
+
+ localns[cls.__name__] = cls
+
+ module = sys.modules.get(cls.__module__)
+ if module:
+ for name, obj in module.__dict__.items():
+ if (
+ isinstance(obj, type)
+ and hasattr(obj, "__module__")
+ and obj.__module__ == cls.__module__
+ ):
+ localns[name] = obj
+
+ return localns
+
+ @classmethod
+ def _custom_resolve_forward_references(
+ cls, globalns: Dict[str, Any], localns: Dict[str, Any]
+ ):
+ for field_name, annotation in list(cls.__annotations__.items()):
+ try:
+ if isinstance(annotation, str):
+ resolved = cls._resolve_single_forward_ref(
+ annotation, globalns, localns
+ )
+ if resolved is not None:
+ cls.__annotations__[field_name] = resolved
+ elif isinstance(annotation, ForwardRef):
+ resolved = cls._resolve_forward_ref_instance(
+ annotation, globalns, localns
+ )
+ if resolved is not None:
+ cls.__annotations__[field_name] = resolved
+ except Exception as e:
+ print(
+ f"Warning: Could not resolve forward reference for {cls.__name__}.{field_name}: {e}"
+ )
+
+ @classmethod
+ def _resolve_single_forward_ref(
+ cls, annotation: str, globalns: Dict[str, Any], localns: Dict[str, Any]
+ ) -> Any:
+ if re.match(r"^[A-Z][a-zA-Z_]*\[", annotation):
+ return cls._resolve_generic_forward_ref(annotation, globalns, localns)
+
+ if annotation in localns:
+ return localns[annotation]
+
+ if annotation in globalns:
+ return globalns[annotation]
+
+ for key, model_class in cls.__registry__.items(): # type: ignore[no-def]
+ if key.endswith(f".{annotation}") or model_class.__name__ == annotation:
+ return model_class
+
+ try:
+ return get_type_hints(annotation, globalns, localns)
+ except Exception:
+ pass
+ return None
+
+ @classmethod
+ def _resolve_generic_forward_ref(
+ cls, annotation: str, globalns: Dict[str, Any], localns: Dict[str, Any]
+ ) -> Any:
+ try:
+ match = re.match(r"^([A-Z][a-zA-Z_]*)\[(.*)]$", annotation)
+ if not match:
+ return None
+
+ outer_type_name, inner_type_str = match.groups()
+
+ outer_type = None
+ if outer_type_name in localns:
+ outer_type = localns[outer_type_name]
+ elif outer_type_name in globalns:
+ outer_type = globalns[outer_type_name]
+
+ if outer_type is None:
+ return None
+
+ inner_types = cls._parse_inner_types(inner_type_str)
+ resolved_inner_types = []
+
+ for inner_type in inner_types:
+ if isinstance(inner_type, str) and inner_type.strip():
+ resolved_inner = cls._resolve_single_forward_ref(
+ inner_type.strip(), globalns, localns
+ )
+ resolved_inner_types.append(resolved_inner or inner_type)
+ else:
+ resolved_inner_types.append(inner_type)
+
+ if hasattr(outer_type, "__getitem__"):
+ if len(resolved_inner_types) == 1:
+ return outer_type[resolved_inner_types[0]]
+ else:
+ return outer_type[tuple(resolved_inner_types)]
+ else:
+ return outer_type
+ except Exception:
+ return None
+
+ @classmethod
+ def _parse_inner_types(cls, inner_type_str: str) -> List[str]:
+ inner_types = []
+ depth = 0
+ current = []
+
+ for char in inner_type_str:
+ if char == "[":
+ depth += 1
+ elif char == "]":
+ depth -= 1
+ elif char == "," and depth == 0:
+ inner_types.append("".join(current).strip())
+ current = []
+ continue
+
+ current.append(char)
+
+ if current:
+ inner_types.append("".join(current).strip())
+
+ return inner_types
+
+ @classmethod
+ def _resolve_forward_ref_instance(
+ cls, forward_ref: ForwardRef, globalns: Dict[str, Any], localns: Dict[str, Any]
+ ) -> Any:
+ try:
+ if hasattr(forward_ref, "_evaluate"):
+ return get_type_hints(cls, globalns=globalns, localns=localns)
+ else:
+ return get_type_hints(
+ forward_ref.__forward_arg__, globalns=globalns, localns=localns
+ )
+ except Exception:
+ return None
\ No newline at end of file
diff --git a/nexios/orm/misc/row_to_tuple.py b/nexios/orm/misc/row_to_tuple.py
new file mode 100644
index 00000000..7bdb9708
--- /dev/null
+++ b/nexios/orm/misc/row_to_tuple.py
@@ -0,0 +1,64 @@
+from typing import Any, Optional, Tuple, Sequence, Mapping, Iterable, List
+
+
+def convert_row(row: Any) -> Optional[Tuple[Any, ...]]:
+ """
+ Convert any database row to tuple format
+
+ Args:
+ row: Raw row from any database driver
+ Returns:
+ Tuple representation or None if input is None
+ """
+ if row is None:
+ return None
+
+ if isinstance(row, tuple):
+ return row
+
+ if isinstance(row, Sequence) and not isinstance(row, (str, bytes)):
+ return tuple(row)
+
+ if hasattr(row, '_fields'):
+ try:
+ return tuple(row)
+ except (TypeError, AttributeError):
+ pass
+
+ if isinstance(row, Mapping):
+ return tuple(row.values())
+
+ if hasattr(row, 'values') and callable(row.values): # type: ignore
+ try:
+ return tuple(row.values()) # type: ignore
+ except (TypeError, AttributeError):
+ pass
+
+ if hasattr(row, '__iter__') and not isinstance(row, (str, bytes)):
+ try:
+ return tuple(iter(row))
+ except (TypeError, StopIteration):
+ pass
+
+ return (row,)
+
+
+def convert_rows(rows: Iterable[Any]) -> List[Tuple[Any, ...]]:
+ """
+ Convert multiple rows to list of tuples.
+
+ Args:
+ rows: Iterable of rows from database
+
+ Returns:
+ List of tuple representations
+ """
+ if rows is None:
+ return []
+
+ result = []
+ for row in rows:
+ converted = convert_row(row)
+ if converted is not None:
+ result.append(converted)
+ return result
diff --git a/nexios/orm/model.py b/nexios/orm/model.py
new file mode 100644
index 00000000..e651fd89
--- /dev/null
+++ b/nexios/orm/model.py
@@ -0,0 +1,504 @@
+from __future__ import annotations
+
+import types
+from typing import (
+ Union,
+ Optional,
+ List,
+ Type,
+ Tuple,
+ Dict,
+ Any,
+ ClassVar,
+ ForwardRef,
+ get_origin,
+ get_args,
+ dataclass_transform,
+)
+from pydantic import BaseModel as PydanticBaseModel
+from pydantic_core import PydanticUndefined as Undefined
+from pydantic._internal._model_construction import ModelMetaclass as ModelMetaclass
+from nexios.orm.fields import Field, FieldInfo
+from nexios.orm.relationships import RelationshipInfo, RelationshipType
+from nexios.orm.descriptors import RelationshipDescriptor
+from nexios.orm.misc.refs import ResolveForwardRefs
+from nexios.orm.utils import (
+ NexiosModelConfig,
+ get_tablename_for_class,
+ get_model_fields,
+ set_config_value,
+ to_snake_case,
+ IS_PYDANTIC_V2,
+)
+
+
+@dataclass_transform(kw_only_default=True, field_specifiers=(Field, FieldInfo))
+class NexiosModelMetaclass(ModelMetaclass):
+ __relationships__: Dict[str, RelationshipInfo] = {}
+ model_config: NexiosModelConfig
+ model_fields: Dict[str, FieldInfo] = {}
+ __config__: Type[NexiosModelConfig]
+ __registry__: Dict[str, Type["NexiosModel"]] = {}
+
+ def __new__(
+ mcs,
+ name: str,
+ bases: Tuple[Type[Any], ...],
+ namespace: Dict[str, Any],
+ **kwargs: Any,
+ ):
+ namespace["__relationships__"] = {}
+
+ relationships: Dict[str, RelationshipInfo] = {}
+ relationship_items = {}
+ pydantic_dict = {}
+ original_annotations = namespace.get("__annotations__", {})
+ pydantic_annotations = {}
+ relationship_annotations = {}
+
+ for k, v in namespace.items():
+ if isinstance(v, RelationshipInfo):
+ relationship_items[k] = v
+ relationship_annotations[k] = original_annotations.get(k)
+ else:
+ pydantic_dict[k] = v
+ if k in original_annotations:
+ pydantic_annotations[k] = original_annotations.get(k)
+ dict_used = {
+ **pydantic_dict,
+ "__annotations__": pydantic_annotations,
+ }
+ allowed_config_keys = {"read_from_attributes", "from_attributes", "table"}
+
+ config_kwargs = {
+ key: kwargs[key] for key in kwargs.keys() & allowed_config_keys
+ }
+ for k in list(kwargs.keys()):
+ if k in allowed_config_keys:
+ config_kwargs[k] = kwargs.pop(k)
+
+ # Create the class
+ cls = super().__new__(mcs, name, bases, dict_used, **config_kwargs)
+
+ # Register the class
+ mcs.__registry__[cls.__name__] = cls
+
+ # Process relationships
+ for attr_name, rel_info in relationship_items.items():
+ mcs._process_relationship(
+ cls, attr_name, rel_info, original_annotations, relationships
+ )
+
+ cls.__relationships__ = relationships
+
+ # Add ColumnDescriptor
+ mcs._add_column_descriptors(cls)
+
+ cls.__annotations__ = {
+ **relationship_annotations,
+ **pydantic_annotations,
+ **cls.__annotations__,
+ }
+
+ cls.resolve_forward_references()
+ return cls
+
+ @classmethod
+ def _add_column_descriptors(mcs, cls: Type["NexiosModel"]):
+ """Replace model fields with ColumnDescriptors"""
+ from nexios.orm.descriptors import ColumnDescriptor
+
+ # Get all field names from Pydantic's model_fields
+ # These are the actual database columns (not relationships)
+ for field_name, field_info in cls.model_fields.items():
+ # Skip relationship fields (they're handled separately)
+ if field_name in cls.__relationships__:
+ continue
+
+ # Create and set the ColumnDescriptor
+ descriptor = ColumnDescriptor(field_name, cls)
+ setattr(cls, field_name, descriptor)
+
+ @classmethod
+ def _process_relationship(
+ mcs,
+ cls: Type[NexiosModel],
+ attr_name: str,
+ rel_info: RelationshipInfo,
+ original_annotations: Dict[str, Any],
+ relationships: Dict[str, RelationshipInfo],
+ ):
+ annotation = original_annotations.get(attr_name)
+ # Parse relationship
+ parsed_info = mcs._parse_relationship_annotation(annotation, rel_info)
+ # Determine related model
+ related_model_name = mcs._determine_related_model(parsed_info, rel_info)
+ if not related_model_name:
+ raise ValueError(
+ f"Could not determine related model for relationship '{attr_name}' "
+ f"in class '{cls.__name__}'"
+ )
+ # Determine relationship type
+ relationship_type = mcs._determine_relationship_type(
+ parsed_info, rel_info, attr_name
+ )
+
+ # Determine foreign key
+ foreign_key = mcs._determine_foreign_key(
+ rel_info, relationship_type, cls.__name__, related_model_name
+ )
+
+ print(
+ f"Foreign key for relationship '{attr_name}' in '{cls.__name__}': {foreign_key}"
+ )
+
+ # Resolve through model if provided
+ through = mcs._resolve_through_model(rel_info)
+
+ association_table = None
+ if rel_info.through:
+ if isinstance(rel_info.through, str):
+ association_table = rel_info.through
+ elif (
+ hasattr(rel_info.through, "__tablename__")
+ and rel_info.through.__tablename__
+ ):
+ association_table = rel_info.through.__tablename__
+ else:
+ association_table = getattr(rel_info.through, "__name__", None)
+
+ # reate final relationship info
+ final_info = RelationshipInfo(
+ field_name=attr_name,
+ related_model_name=related_model_name,
+ relationship_type=relationship_type,
+ foreign_key=foreign_key,
+ related_field_name=rel_info.related_field_name,
+ through=through,
+ ondelete=rel_info.ondelete,
+ onupdate=rel_info.onupdate,
+ nullable=rel_info.nullable,
+ unique=rel_info.unique,
+ back_populates=rel_info.back_populates,
+ lazy=rel_info.lazy,
+ deferrable=rel_info.deferrable,
+ initially_deferred=rel_info.initially_deferred,
+ association_table=association_table,
+ local_column=rel_info.local_column,
+ foreign_column=rel_info.foreign_column,
+ metadata=rel_info.metadata,
+ )
+ relationships[attr_name] = final_info
+ setattr(cls, attr_name, RelationshipDescriptor(cls, attr_name, final_info))
+
+ @classmethod
+ def _parse_relationship_annotation(
+ mcs, annotation: Any, rel_info: RelationshipInfo
+ ) -> Dict[str, Any]:
+ result = {
+ "related_model": None,
+ "relationship_type": None,
+ "is_optional": False,
+ "is_list": False,
+ }
+
+ if annotation is None:
+ return result
+
+ def normalize_annotation(ann: str) -> str:
+ return ann.replace("typing.", "").replace("typing_extensions.", "")
+
+ # String annotations
+ if isinstance(annotation, str):
+ annotation = annotation.replace(" ", "")
+
+ # Optional[T]
+ normalized_annotation = normalize_annotation(annotation)
+ if normalized_annotation.startswith(("Optional[", "Union[")):
+ result["is_optional"] = True
+ inner = normalized_annotation[normalized_annotation.find("[") + 1 : -1]
+ nested = mcs._parse_relationship_annotation(inner, rel_info)
+ result.update(nested)
+ result["is_optional"] = True
+ return result
+
+ # List[T]
+ if normalized_annotation.startswith(("List[", "list[")):
+ result["is_list"] = True
+ inner = normalized_annotation[
+ normalized_annotation.find("[") + 1 : -1
+ ].strip("\"'")
+ result["related_model"] = inner
+ result["relationship_type"] = RelationshipType.ONE_TO_MANY
+ return result
+
+ # Bare types
+ result["related_model"] = annotation.strip("\"'")
+ return result
+
+ # typing objects
+ origin = get_origin(annotation)
+ args = get_args(annotation)
+
+ # Optional[T]
+ if origin is Union:
+ result["is_optional"] = True
+ for arg in args:
+ if arg is type(None):
+ continue
+ nested = mcs._parse_relationship_annotation(arg, rel_info)
+ result.update(nested)
+ result["is_optional"] = True
+ return result
+
+ # List[T]
+ if origin in (list, List):
+ result["is_list"] = True
+ if args:
+ arg = args[0]
+ if isinstance(arg, str):
+ result["related_model"] = arg
+ elif isinstance(arg, ForwardRef):
+ result["related_model"] = arg.__forward_arg__
+ elif isinstance(arg, type):
+ result["related_model"] = arg.__name__
+ result["relationship_type"] = RelationshipType.ONE_TO_MANY
+ return result
+
+ # ForwardRef
+ if isinstance(annotation, ForwardRef):
+ result["related_model"] = annotation.__forward_arg__
+ return result
+
+ # type directly
+ if isinstance(annotation, type):
+ result["related_model"] = annotation.__name__
+ return result
+
+ return result
+
+ @classmethod
+ def _determine_related_model(
+ mcs, parsed_info: Dict[str, Any], rel_info: RelationshipInfo
+ ) -> Optional[str]:
+ if rel_info.related_model:
+ if isinstance(rel_info.related_model, str):
+ return rel_info.related_model
+ return rel_info.related_model.__name__
+
+ if parsed_info.get("related_model"):
+ return parsed_info["related_model"]
+
+ return rel_info.related_model_name
+
+ @classmethod
+ def _determine_relationship_type(
+ mcs, parsed_info: Dict[str, Any], rel_info: RelationshipInfo, attr_name: str
+ ) -> RelationshipType:
+ rel_type: RelationshipType = RelationshipType.MANY_TO_ONE
+
+ if rel_info.relationship_type:
+ return rel_info.relationship_type
+
+ if parsed_info.get("relationship_type"):
+ rel_type = parsed_info["relationship_type"]
+
+ if parsed_info.get("is_list"):
+ rel_type = RelationshipType.ONE_TO_MANY
+
+ if rel_info.unique:
+ rel_type = RelationshipType.ONE_TO_ONE
+
+ if not parsed_info.get("is_list") and rel_type == RelationshipType.MANY_TO_ONE:
+ rel_type = RelationshipType.ONE_TO_ONE
+
+ return rel_type
+
+ @classmethod
+ def _determine_foreign_key(
+ mcs,
+ rel_info: RelationshipInfo,
+ relationship_type: RelationshipType,
+ current_model_name: str,
+ related_model_name: str,
+ ) -> Optional[str]:
+
+ if rel_info.foreign_key:
+ return rel_info.foreign_key
+
+ current_cls = None
+ try:
+ current_cls = mcs.__registry__.get(current_model_name)
+ except AttributeError:
+ current_cls = None
+
+ related_cls = mcs.__registry__.get(related_model_name)
+
+ def fk_matches_target(fk_val: Any, target_name: str) -> bool:
+ if not fk_val:
+ return False
+ if isinstance(fk_val, str):
+ fk_s = fk_val.strip()
+ if "." in fk_s:
+ left, _ = fk_s.rsplit(".", 1)
+ left_l = left.lower()
+
+ model_cls_from_registry = mcs.__registry__.get(target_name)
+ tablename_from_registry = ""
+ if model_cls_from_registry:
+ tablename_from_registry = (
+ get_tablename_for_class(model_cls_from_registry) or ""
+ )
+
+ candidates = {
+ target_name.lower(),
+ to_snake_case(target_name),
+ tablename_from_registry.lower(),
+ }
+ return left_l in candidates
+ else:
+ if fk_s == f"{to_snake_case(target_name)}_id" or fk_s == "id":
+ return True
+ return False
+
+ # For one-to-one, we only return the foreign key name IF it's on the CURRENT model.
+ if relationship_type == RelationshipType.ONE_TO_ONE:
+ if current_cls is not None:
+ for field_name, field_info in get_model_fields(current_cls).items():
+ fk_val = getattr(field_info, "foreign_key", Undefined)
+ if fk_val is not Undefined and fk_val:
+ if fk_matches_target(fk_val, related_model_name):
+ return field_name
+
+ if related_cls is not None:
+ for field_name, field_info in get_model_fields(related_cls).items():
+ fk_val = getattr(field_info, "foreign_key", Undefined)
+ if fk_val is not Undefined and fk_val:
+ if fk_matches_target(fk_val, current_model_name):
+ return None # FK is on the other side
+
+ conv = f"{to_snake_case(related_model_name)}_id"
+ if current_cls is not None and conv in get_model_fields(current_cls):
+ return conv
+ return None
+
+ # Rest of your existing logic for other relationship types...
+ current = to_snake_case(current_model_name)
+ related = to_snake_case(related_model_name)
+
+ print(
+ f"Determining FK for relationship in '{current_model_name}' to '{related_model_name}' with relationship type '{relationship_type}'"
+ )
+
+ fk = f"{related}_id"
+
+ match (relationship_type):
+ case RelationshipType.MANY_TO_ONE:
+ return (
+ fk
+ if current_cls is not None and fk in get_model_fields(current_cls)
+ else None
+ )
+ case RelationshipType.ONE_TO_ONE:
+ return (
+ fk
+ if current_cls is not None and fk in get_model_fields(current_cls)
+ else None
+ )
+ case RelationshipType.ONE_TO_MANY:
+ return None
+ case RelationshipType.MANY_TO_MANY:
+ return None
+ case _:
+ return None
+
+ @classmethod
+ def _resolve_through_model(mcs, rel_info: RelationshipInfo) -> Optional[str]:
+ if rel_info.through is None:
+ return None
+
+ if isinstance(rel_info.through, str):
+ return rel_info.through
+
+ if hasattr(rel_info.through, "__name__"):
+ return rel_info.through.__name__
+
+ return str(rel_info.through)
+
+
+class NexiosModel(
+ PydanticBaseModel, ResolveForwardRefs, metaclass=NexiosModelMetaclass
+):
+ __tablename__: ClassVar[Optional[str]] = None
+ __relationships__: ClassVar[Dict[str, RelationshipInfo]] = {}
+ __primary_key__: ClassVar[Union[Tuple[str, ...], List[str], str, None]] = None
+
+ if IS_PYDANTIC_V2:
+ model_config = NexiosModelConfig(from_attributes=True)
+ else:
+
+ class Config:
+ orm_mode = True
+
+ def __init_subclass__(cls, table: Optional[bool] = None, **kwargs):
+ super().__init_subclass__(**kwargs)
+
+ cls.__registry__[cls.__name__] = cls
+
+ if table is not None:
+ set_config_value(model=cls, parameter="table", value=table)
+
+ tablename = get_tablename_for_class(cls)
+ cls.__tablename__ = tablename
+
+ def model_post_init(self, __context: Any) -> None:
+ """Pydantic v2 hook for post-initialization."""
+ self.__dict__["__relationship_cache__"] = {} # type: ignore
+
+ def __setattr__(self, name: str, value: Any) -> types.NoneType:
+ if name not in self.__relationships__:
+ super().__setattr__(name, value)
+
+ @classmethod
+ def _resolve_relationships(cls):
+ for rel_name, rel_info in cls.__relationships__.items():
+ if not rel_info.is_resolved:
+ if rel_info.back_populates:
+ related_model = rel_info.related_model
+ if related_model and hasattr(related_model, "__relationships__"):
+ related_rel = related_model.__relationships__.get(
+ rel_info.back_populates
+ )
+
+ if related_rel:
+ related_rel.back_populates = rel_name
+ related_rel.is_resolved = True
+ rel_info.is_resolved = True
+ rel_info.is_resolved = True
+
+ @classmethod
+ def get_fields(cls) -> Dict[str, FieldInfo]:
+ return cls.model_fields
+
+ @classmethod
+ def get_relationships(cls) -> Dict[str, RelationshipInfo]:
+ return cls.__relationships__
+
+ @classmethod
+ def get_primary_key(cls) -> Any:
+ from nexios.orm.query.expressions import ColumnExpression
+
+ pk = getattr(cls, "__primary_key__", None)
+ if pk:
+ if isinstance(pk, (tuple, list)):
+ return tuple(pk) if len(pk) > 1 else pk[0] # type: ignore
+ return pk
+
+ for field_name, field_info in get_model_fields(cls).items():
+ if getattr(field_info, "primary_key", Undefined):
+ field_ = getattr(cls, field_name)
+ if isinstance(field_, ColumnExpression):
+ return field_.field_name
+ else:
+ return field_
+ return None
diff --git a/nexios/orm/nexios.db b/nexios/orm/nexios.db
new file mode 100644
index 00000000..7159d2bf
Binary files /dev/null and b/nexios/orm/nexios.db differ
diff --git a/nexios/orm/pool/__init__.py b/nexios/orm/pool/__init__.py
new file mode 100644
index 00000000..32671edf
--- /dev/null
+++ b/nexios/orm/pool/__init__.py
@@ -0,0 +1,10 @@
+from .connection_pool import ConnectionPool
+from .async_connection_pool import AsyncConnectionPool
+from .base import BaseConnectionPool, BaseAsyncConnectionPool
+
+__all__ = [
+ "ConnectionPool",
+ "AsyncConnectionPool",
+ "BaseConnectionPool",
+ "BaseAsyncConnectionPool"
+]
\ No newline at end of file
diff --git a/nexios/orm/pool/async_connection_pool.py b/nexios/orm/pool/async_connection_pool.py
new file mode 100644
index 00000000..9179ad62
--- /dev/null
+++ b/nexios/orm/pool/async_connection_pool.py
@@ -0,0 +1,379 @@
+import asyncio
+from collections import deque
+from contextlib import asynccontextmanager
+import logging
+import time
+from typing import Awaitable, Callable, Deque, Dict, List, Optional, Tuple
+import weakref
+from nexios.orm.pool.base import BaseAsyncConnectionPool, PoolConfig, PoolEvent
+from nexios.orm.connection import AsyncDatabaseConnection
+
+
+class AsyncConnectionPool(BaseAsyncConnectionPool):
+ """High-performance asynchronous connection pool for database connections."""
+
+ def __init__(
+ self,
+ create_connection: Callable[[], Awaitable[AsyncDatabaseConnection]],
+ config: Optional[PoolConfig] = None,
+ logger: Optional[logging.Logger] = None,
+ ) -> None:
+ self._create_connection = create_connection
+ self.config = config or PoolConfig()
+ self.logger = logger or logging.getLogger(__name__)
+
+ if self.logger.level == logging.NOTSET:
+ self.logger.setLevel(logging.INFO)
+
+ self._available: Deque[Tuple[AsyncDatabaseConnection, float]] = deque()
+ self._in_use: Dict[AsyncDatabaseConnection, float] = {}
+ self._all_connections: weakref.WeakSet[AsyncDatabaseConnection] = weakref.WeakSet()
+
+ self._lock = asyncio.Lock()
+ self._condition = asyncio.Condition(self._lock)
+
+ self._connection_times: Dict[AsyncDatabaseConnection, float] = {}
+ self._connection_usage: Dict[AsyncDatabaseConnection, int] = {}
+ self._closed = False
+
+ self._event_handlers: Dict[PoolEvent, List[Callable[..., None]]] = {}
+
+ self._maintenance_task: Optional[asyncio.Task[None]] = None
+ self._shrink_task: Optional[asyncio.Task[None]] = None
+ self._stop_event = asyncio.Event()
+
+ self._stats = {
+ 'connections_created': 0,
+ 'connections_closed': 0,
+ 'acquire_requests': 0,
+ 'acquire_timeouts': 0,
+ }
+
+ self._initialized = False
+
+ async def initialize(self):
+ if self._initialized:
+ return
+
+ await self._initialize_pool()
+ self._start_background_tasks()
+ self._initialized = True
+
+ self.logger.info("AsyncConnectionPool initialized with config: %s", self.config)
+
+ async def _initialize_pool(self):
+ """Initialize the pool with minimum connections."""
+ for _ in range(self.config.min_size):
+ try:
+ conn = await self._create_connection()
+ self._available.append((conn, time.monotonic()))
+ self._all_connections.add(conn)
+ self._connection_times[conn] = time.monotonic()
+ self._connection_usage[conn] = 0
+ self._stats['connections_created'] += 1
+ await self._trigger_event(PoolEvent.CONNECTION_CREATED, conn)
+ except Exception as e:
+ self.logger.error("Failed to create initial connection: %s", e)
+
+
+ def _start_background_tasks(self):
+ """Start maintenance and shrinking background tasks."""
+ async def maintenance_worker():
+ while not self._stop_event.is_set():
+ try:
+ await asyncio.sleep(self.config.health_check_interval)
+
+ if not self._stop_event.is_set():
+ await self._background_health_check()
+ except Exception as e:
+ self.logger.error("Error in maintenance worker: %s", e)
+
+ async def shrink_worker():
+ while not self._stop_event.is_set():
+ try:
+ await asyncio.sleep(self.config.shrink_interval)
+
+ if not self._stop_event.is_set():
+ await self._background_shrink()
+ except Exception as e:
+ self.logger.error("Error in shrink worker: %s", e)
+
+ self._maintenance_task = asyncio.create_task(maintenance_worker())
+ self._shrink_task = asyncio.create_task(shrink_worker())
+
+ async def _background_health_check(self):
+ """Perform health checks on idle connections."""
+ async with self._lock:
+ if self._closed:
+ return
+
+ current_time = time.monotonic()
+ bad_connections = []
+
+ for conn, last_used in list(self._available):
+ if current_time - self._connection_times.get(conn, 0) > self.config.max_lifetime:
+ bad_connections.append(conn)
+ continue
+
+ if current_time - last_used > self.config.idle_timeout:
+ try:
+ if not conn.is_connection_open:
+ bad_connections.append(conn)
+ await self._trigger_event(PoolEvent.CONNECTION_INVALID, conn)
+ except Exception:
+ bad_connections.append(conn)
+ await self._trigger_event(PoolEvent.CONNECTION_INVALID, conn)
+
+ for conn in bad_connections:
+ self._available = deque([
+ (c, t) for c, t in self._available if c != conn
+ ])
+ await self._safe_close_connection(conn)
+ self._stats['connections_closed'] += 1
+
+ async def _background_shrink(self):
+ """Shrink pool by closing excess idle connections"""
+ async with self._lock:
+ if self._closed:
+ return
+
+ current_time = time.monotonic()
+ total_size = len(self._available) + len(self._in_use)
+
+ if total_size <= self.config.min_size:
+ return
+
+ max_idle_to_keep = max(
+ self.config.min_size - len(self._in_use),
+ self.config.max_idle
+ )
+
+ while len(self._available) > max_idle_to_keep:
+ conn, _ = self._available.popleft()
+ await self._safe_close_connection(conn)
+ self._stats['connections_closed'] += 1
+ await self._trigger_event(PoolEvent.POOL_SHRINK, conn)
+
+ current_available = list(self._available)
+ self._available.clear()
+
+ for conn, last_used in current_available:
+ if current_time - last_used > self.config.idle_timeout:
+ await self._safe_close_connection(conn)
+ self._stats['connections_closed'] += 1
+ await self._trigger_event(PoolEvent.CONNECTION_CLOSED, conn)
+ else:
+ self._available.append((conn, last_used))
+
+ async def get_connection(self) -> AsyncDatabaseConnection:
+ """Async connection acquisition"""
+ if not self._initialized:
+ await self.initialize()
+
+ if self._closed:
+ raise RuntimeError("Connection pool is closed.")
+
+ self._stats['acquire_requests'] += 1
+ start_time = time.monotonic()
+
+ async with self._condition:
+ while self._available:
+ conn, last_used = self._available.pop()
+ if await self._quick_validate(conn, last_used):
+ self._in_use[conn] = start_time
+ self._connection_usage[conn] = self._connection_usage.get(conn, 0) + 1
+ return conn
+ else:
+ await self._safe_close_connection(conn)
+ self._stats['connections_closed'] += 1
+
+ current_size = len(self._all_connections)
+ if current_size < self.config.max_size:
+ try:
+ conn = await self._create_connection()
+ self._all_connections.add(conn)
+ self._in_use[conn] = start_time
+ self._connection_times[conn] = start_time
+ self._connection_usage[conn] = 1
+ self._stats['connections_created'] += 1
+ await self._trigger_event(PoolEvent.CONNECTION_CREATED, conn)
+ await self._trigger_event(PoolEvent.POOL_GROW, conn)
+ return conn
+ except Exception as e:
+ self.logger.error("Failed to create new connection: %s", e)
+ raise
+
+ timeout = self.config.connection_timeout - (time.monotonic() - start_time)
+ if timeout <= 0:
+ self._stats['acquire_timeouts'] += 1
+ raise asyncio.TimeoutError("Timed out waiting for a connection from the pool.")
+
+ try:
+ await asyncio.wait_for(self._condition.wait(), timeout)
+ except asyncio.TimeoutError:
+ self._stats['acquire_timeouts'] += 1
+ raise asyncio.TimeoutError(f"Timed out waiting for a connection from the pool after: {timeout:.1f}s")
+
+ while self._available:
+ conn, last_used = self._available.pop()
+ if await self._quick_validate(conn, last_used):
+ self._in_use[conn] = time.monotonic()
+ self._connection_usage[conn] = self._connection_usage.get(conn, 0) + 1
+ return conn
+ else:
+ await self._safe_close_connection(conn)
+ self._stats['connections_closed'] += 1
+
+ self._stats['acquire_timeouts'] += 1
+ raise asyncio.TimeoutError("Timed out waiting for a connection from the pool.")
+
+ async def _quick_validate(self, conn: AsyncDatabaseConnection, last_used: Optional[float] = None) -> bool:
+ """Connetion validation"""
+ try:
+ if conn.is_connection_open:
+ if last_used is not None:
+ conn_time = self._connection_times.get(conn, 0)
+ current_time = time.monotonic()
+ return (current_time - conn_time) <= self.config.max_lifetime
+ return True
+ return False
+ except Exception:
+ return False
+
+ async def return_connection(self, conn: AsyncDatabaseConnection) -> None:
+ """Return connection to pool"""
+ if self._closed:
+ await self._safe_close_connection(conn)
+ return
+
+ return_time = time.monotonic()
+
+ async with self._condition:
+ if conn in self._in_use:
+ del self._in_use[conn]
+ else:
+ await self._safe_close_connection(conn)
+ return
+
+ if await self._quick_validate(conn):
+ await self._reset_connection(conn)
+ self._available.append((conn, return_time))
+ self._condition.notify()
+ else:
+ await self._safe_close_connection(conn)
+ self._stats['connections_closed'] += 1
+ await self._trigger_event(PoolEvent.CONNECTION_INVALID, conn)
+
+ async def _reset_connection(self, conn: AsyncDatabaseConnection) -> None:
+ """Reset connection state before returning to pool."""
+ try:
+ await conn.rollback()
+ except Exception as e:
+ pass
+
+ async def _safe_close_connection(self, conn: AsyncDatabaseConnection) -> None:
+ """Safely close a connection."""
+ try:
+ if conn in self._all_connections:
+ self._all_connections.discard(conn)
+ if conn in self._connection_times:
+ del self._connection_times[conn]
+ if conn in self._connection_usage:
+ del self._connection_usage[conn]
+ await conn.close()
+ await self._trigger_event(PoolEvent.CONNECTION_CLOSED, conn)
+ except Exception as e:
+ self.logger.debug("Error closing connection: %s", e)
+
+ async def _trigger_event(self, event: PoolEvent, conn: AsyncDatabaseConnection) -> None:
+ """Trigger event handlers for a specific pool event."""
+ if event in self._event_handlers:
+ for handler in self._event_handlers[event]:
+ try:
+ if asyncio.iscoroutinefunction(handler):
+ await handler(conn)
+ else:
+ handler(conn)
+ except Exception as e:
+ self.logger.error("Error in event handler for %s: %s", event, e)
+
+ async def add_event_handler(
+ self,
+ event: PoolEvent,
+ handler: Callable[..., None]
+ ) -> None:
+ """Add event handler for a specific pool event."""
+ if event not in self._event_handlers:
+ self._event_handlers[event] = []
+ self._event_handlers[event].append(handler)
+
+ async def close(self) -> None:
+ """Close the connection pool and all its connections."""
+ if self._closed:
+ return
+
+ self._closed = True
+ self._stop_event.set()
+
+ if self._maintenance_task:
+ self._maintenance_task.cancel()
+ if self._shrink_task:
+ self._shrink_task.cancel()
+
+ async with self._condition:
+ for conn, _ in self._available:
+ await self._safe_close_connection(conn)
+ self._available.clear()
+
+ for conn in list(self._in_use.keys()):
+ await self._safe_close_connection(conn)
+ self._in_use.clear()
+
+ self._condition.notify_all()
+
+ self.logger.info("Connection pool closed.")
+
+ async def health_check(self) -> None:
+ """Manual health check of all connections in the pool."""
+ await self._background_health_check()
+
+ @property
+ async def size(self) -> int:
+ async with self._lock:
+ return len(self._all_connections)
+
+ @property
+ async def available(self) -> int:
+ async with self._lock:
+ return len(self._available)
+
+ async def get_stats(self) -> Dict:
+ """Get pool statistics."""
+ async with self._lock:
+ usage_counts = list(self._connection_usage.values())
+ return {
+ **self._stats,
+ 'total_connections': len(self._all_connections),
+ 'idle_connections': len(self._available),
+ 'in_use_connections': len(self._in_use),
+ 'avg_usage_per_conn': sum(usage_counts) / len(usage_counts) if usage_counts else 0
+ }
+
+ @asynccontextmanager
+ async def connection(self):
+ """Context manager for connections"""
+ conn = None
+ try:
+ conn = await self.get_connection()
+ yield conn
+ except Exception:
+ if conn:
+ async with self._lock:
+ if conn in self._in_use:
+ del self._in_use[conn]
+ await self._safe_close_connection(conn)
+ raise
+ finally:
+ if conn:
+ await self.return_connection(conn)
+
diff --git a/nexios/orm/pool/base.py b/nexios/orm/pool/base.py
new file mode 100644
index 00000000..df79e9bf
--- /dev/null
+++ b/nexios/orm/pool/base.py
@@ -0,0 +1,117 @@
+from abc import ABC, abstractmethod
+from dataclasses import dataclass
+import statistics
+from enum import StrEnum
+from typing import List, Optional
+from nexios.orm.connection import AsyncDatabaseConnection, SyncDatabaseConnection
+
+
+@dataclass
+class PoolConfig:
+ min_size: int = 1
+ max_size: int = 50
+ connection_timeout: float = 5.0
+ max_lifetime: int = 7200 # 2 hour in seconds
+ idle_timeout: int = 300 # 5 minutes in seconds
+ health_check_interval: int = 60 # 1 minute
+
+ shrink_interval: int = 30 # Check every 30 seconds for shrinking
+ max_idle: int = 10 # Maximum idle connections to keep
+
+class PoolEvent(StrEnum):
+ CONNECTION_CREATED = "connection_created"
+ CONNECTION_CLOSED = "connection_closed"
+ CONNECTION_INVALID = "connection_invalid"
+ POOL_SHRINK = "pool_shrink"
+ POOL_GROW = "pool_grow"
+
+@dataclass
+class PoolMetrics:
+ total_connections: int = 0
+ connections_created: int = 0
+ connections_closed: int = 0
+ connection_errors: int = 0
+ total_operations: int = 0
+ slow_operations: int = 0
+ wait_times: Optional[List[float]] = None
+ average_wait_time: float = 0.0
+
+ def __post_init__(self):
+ if self.wait_times is None:
+ self.wait_times = []
+
+ def record_wait_time(self, wait_time: float):
+ if self.wait_times is not None:
+ self.wait_times.append(wait_time)
+ if len(self.wait_times) > 1000:
+ self.wait_times.pop(0)
+ self.average_wait_time = (
+ statistics.mean(self.wait_times) if self.wait_times else 0.0
+ )
+
+
+class BaseConnectionPool(ABC):
+ @abstractmethod
+ def get_connection(self) -> SyncDatabaseConnection:
+ """Get connection from the pool"""
+ pass
+
+ @abstractmethod
+ def return_connection(self, conn: SyncDatabaseConnection) -> None:
+ """Return connection to the pool"""
+ pass
+
+ @abstractmethod
+ def close(self) -> None:
+ """Close all connections in the pool"""
+ pass
+
+ @property
+ @abstractmethod
+ def size(self) -> int:
+ """Current pool size"""
+ pass
+
+ @property
+ @abstractmethod
+ def available(self) -> int:
+ """Number of available connections in the pool"""
+ pass
+
+ @abstractmethod
+ def health_check(self) -> None:
+ """Perform health check on connections"""
+ pass
+
+
+class BaseAsyncConnectionPool(ABC):
+ @abstractmethod
+ async def get_connection(self) -> AsyncDatabaseConnection:
+ """Get a connection from the pool"""
+ pass
+
+ @abstractmethod
+ async def return_connection(self, conn: AsyncDatabaseConnection) -> None:
+ """Return a connection to the pool"""
+ pass
+
+ @abstractmethod
+ async def close(self) -> None:
+ """Close all connections in the pool"""
+ pass
+
+ @property
+ @abstractmethod
+ async def size(self) -> int:
+ """Current pool size"""
+ pass
+
+ @property
+ @abstractmethod
+ async def available(self) -> int:
+ """Number of available connections in the pool"""
+ pass
+
+ @abstractmethod
+ async def health_check(self) -> None:
+ pass
diff --git a/nexios/orm/pool/connection_pool.py b/nexios/orm/pool/connection_pool.py
new file mode 100644
index 00000000..a841a1e4
--- /dev/null
+++ b/nexios/orm/pool/connection_pool.py
@@ -0,0 +1,389 @@
+import logging
+import threading
+import time
+from typing import Callable, Dict, Optional, Tuple, Deque, List
+from contextlib import contextmanager
+from nexios.orm.pool.base import BaseConnectionPool, PoolConfig, PoolEvent
+from nexios.orm.connection import SyncDatabaseConnection
+import statistics
+import weakref
+from collections import deque
+
+class ConnectionPool(BaseConnectionPool):
+ """
+ Production-ready connection pool with active maintenance like psycopg
+ """
+
+ def __init__(
+ self,
+ create_connection: Callable[[], SyncDatabaseConnection],
+ config: Optional[PoolConfig] = None,
+ logger: Optional[logging.Logger] = None,
+ ) -> None:
+ self._create_connection = create_connection
+ self.config = config or PoolConfig()
+ self.logger = logger or logging.getLogger(__name__)
+
+ if self.logger.level == logging.NOTSET:
+ self.logger.setLevel(logging.INFO)
+
+ # Connection storage with last-used timestamps
+ self._available: Deque[Tuple[SyncDatabaseConnection, float]] = deque()
+ self._in_use: Dict[SyncDatabaseConnection, float] = {}
+ self._all_connections: weakref.WeakSet[SyncDatabaseConnection] = weakref.WeakSet()
+
+ # Threading
+ self._lock = threading.RLock()
+ self._condition = threading.Condition(self._lock)
+
+ # Tracking
+ self._connection_times: Dict[SyncDatabaseConnection, float] = {}
+ self._connection_usage: Dict[SyncDatabaseConnection, int] = {}
+ self._closed = False
+
+ # Event callbacks
+ self._event_handlers: Dict[PoolEvent, List[Callable]] = {}
+
+ # Background workers
+ self._maintenance_thread: Optional[threading.Thread] = None
+ self._shrink_thread: Optional[threading.Thread] = None
+ self._stop_event = threading.Event()
+
+ # Statistics
+ self._stats = {
+ 'connections_created': 0,
+ 'connections_closed': 0,
+ 'acquire_requests': 0,
+ 'acquire_timeouts': 0,
+ }
+
+ self._initialize_pool()
+ self._start_background_workers()
+
+ # self.logger.info(
+ # f"Connection pool initialized: min={self.config.min_size}, max={self.config.max_size}"
+ # )
+
+ def _initialize_pool(self):
+ """Initialize with minimum connections"""
+ for _ in range(self.config.min_size):
+ try:
+ conn = self._create_connection()
+ self._available.append((conn, time.monotonic()))
+ self._all_connections.add(conn)
+ self._connection_times[conn] = time.monotonic()
+ self._connection_usage[conn] = 0
+ self._stats['connections_created'] += 1
+ self._fire_event(PoolEvent.CONNECTION_CREATED, conn)
+ except Exception as e:
+ self.logger.error(f"Initial connection creation failed: {e}")
+
+ def _start_background_workers(self):
+ """Start maintenance and shrinking workers"""
+ def maintenance_worker():
+ while not self._stop_event.is_set():
+ try:
+ time.sleep(self.config.health_check_interval)
+ if not self._stop_event.is_set():
+ self._background_health_check()
+ except Exception as e:
+ self.logger.error(f"Maintenance worker error: {e}")
+
+ def shrink_worker():
+ while not self._stop_event.is_set():
+ try:
+ time.sleep(self.config.shrink_interval)
+ if not self._stop_event.is_set():
+ self._background_shrink()
+ except Exception as e:
+ self.logger.error(f"Shrink worker error: {e}")
+
+ self._maintenance_thread = threading.Thread(
+ target=maintenance_worker, daemon=True, name="pool-maintenance"
+ )
+ self._shrink_thread = threading.Thread(
+ target=shrink_worker, daemon=True, name="pool-shrink"
+ )
+
+ self._maintenance_thread.start()
+ self._shrink_thread.start()
+
+ def _background_health_check(self):
+ """Background health check of idle connections"""
+ with self._lock:
+ if self._closed:
+ return
+
+ current_time = time.monotonic()
+ bad_connections = []
+
+ # Check idle connections
+ for conn, last_used in list(self._available):
+ # Check if connection is too old
+ if current_time - self._connection_times.get(conn, 0) > self.config.max_lifetime:
+ bad_connections.append(conn)
+ continue
+
+ # Check if connection needs health validation
+ if current_time - last_used > 60: # Validate if idle > 1 minute
+ try:
+ cursor = conn.cursor()
+ cursor.execute("SELECT 1")
+ except Exception:
+ bad_connections.append(conn)
+ self._fire_event(PoolEvent.CONNECTION_INVALID, conn)
+
+ # Remove bad connections
+ for conn in bad_connections:
+ self._available = deque([
+ (c, t) for c, t in self._available if c != conn
+ ])
+ self._safe_close_connection(conn)
+ self._stats['connections_closed'] += 1
+
+ def _background_shrink(self):
+ """Shrink pool by closing excess idle connections"""
+ with self._lock:
+ if self._closed:
+ return
+
+ current_time = time.monotonic()
+ total_size = len(self._available) + len(self._in_use)
+
+ # Don't shrink below min_size
+ if total_size <= self.config.min_size:
+ return
+
+ # Calculate how many idle connections to keep
+ max_idle_to_keep = max(
+ self.config.min_size - len(self._in_use), # At least enough for current in_use
+ self.config.max_idle # But no more than max_idle
+ )
+
+ # Remove excess idle connections (oldest first)
+ while len(self._available) > max_idle_to_keep:
+ conn, last_used = self._available.popleft()
+ self._safe_close_connection(conn)
+ self._stats['connections_closed'] += 1
+ self._fire_event(PoolEvent.POOL_SHRINK, conn)
+
+ # Also remove connections that have been idle too long
+ current_available = list(self._available)
+ self._available.clear()
+
+ for conn, last_used in current_available:
+ if current_time - last_used > self.config.idle_timeout:
+ self._safe_close_connection(conn)
+ self._stats['connections_closed'] += 1
+ self._fire_event(PoolEvent.CONNECTION_CLOSED, conn)
+ else:
+ self._available.append((conn, last_used))
+
+ def get_connection(self) -> SyncDatabaseConnection:
+ """Optimized connection acquisition"""
+ if self._closed:
+ raise RuntimeError("Connection pool is closed")
+
+ self._stats['acquire_requests'] += 1
+ start_time = time.monotonic()
+
+ with self._condition:
+ # FAST PATH: Try to get available connection
+ while self._available:
+ conn, last_used = self._available.pop()
+ if self._quick_validate(conn, last_used):
+ self._in_use[conn] = start_time
+ self._connection_usage[conn] = self._connection_usage.get(conn, 0) + 1
+ return conn
+ else:
+ self._safe_close_connection(conn)
+ self._stats['connections_closed'] += 1
+
+ # MEDIUM PATH: Create new connection if under max
+ current_size = len(self._all_connections)
+ if current_size < self.config.max_size:
+ try:
+ conn = self._create_connection()
+ self._all_connections.add(conn)
+ self._in_use[conn] = start_time
+ self._connection_times[conn] = start_time
+ self._connection_usage[conn] = 1
+ self._stats['connections_created'] += 1
+ self._fire_event(PoolEvent.CONNECTION_CREATED, conn)
+ self._fire_event(PoolEvent.POOL_GROW, conn)
+ return conn
+ except Exception as e:
+ self.logger.error(f"Failed to create connection: {e}")
+
+ # SLOW PATH: Wait for connection with timeout
+ timeout = self.config.connection_timeout - (time.monotonic() - start_time)
+ if timeout <= 0:
+ self._stats['acquire_timeouts'] += 1
+ raise TimeoutError("Connection timeout exceeded")
+
+ # Wait for connection to become available
+ end_time = time.monotonic() + timeout
+ while time.monotonic() < end_time:
+ remaining = end_time - time.monotonic()
+ if remaining <= 0:
+ break
+
+ self._condition.wait(remaining)
+
+ # Check if connection became available
+ while self._available:
+ conn, last_used = self._available.pop()
+ if self._quick_validate(conn, last_used):
+ self._in_use[conn] = time.monotonic()
+ self._connection_usage[conn] = self._connection_usage.get(conn, 0) + 1
+ return conn
+ else:
+ self._safe_close_connection(conn)
+ self._stats['connections_closed'] += 1
+
+ self._stats['acquire_timeouts'] += 1
+ raise TimeoutError(f"Timeout waiting for connection after {timeout:.1f}s")
+
+ def return_connection(self, conn: SyncDatabaseConnection) -> None:
+ """Return connection to pool"""
+ if self._closed:
+ self._safe_close_connection(conn)
+ return
+
+ return_time = time.monotonic()
+
+ with self._condition:
+ # Remove from in_use
+ if conn in self._in_use:
+ del self._in_use[conn]
+ else:
+ self._safe_close_connection(conn)
+ return
+
+ # Validate before returning to pool
+ if self._quick_validate(conn):
+ self._reset_connection(conn)
+ self._available.append((conn, return_time))
+ # Notify waiting threads
+ self._condition.notify()
+ else:
+ self._safe_close_connection(conn)
+ self._stats['connections_closed'] += 1
+ self._fire_event(PoolEvent.CONNECTION_INVALID, conn)
+
+ def _quick_validate(self, conn: SyncDatabaseConnection, last_used: Optional[float] = None) -> bool:
+ """Fast connection validation"""
+ try:
+ if conn.is_connection_open:
+ if last_used is not None:
+ conn_time = self._connection_times.get(conn, 0)
+ current_time = time.monotonic()
+ return (current_time - conn_time) <= self.config.max_lifetime
+ return True
+ return False
+
+ except Exception:
+ return False
+
+ def _reset_connection(self, conn: SyncDatabaseConnection) -> None:
+ """Reset connection state"""
+ try:
+ if hasattr(conn, 'rollback'):
+ conn.rollback()
+ except Exception:
+ pass
+
+ def _safe_close_connection(self, conn: SyncDatabaseConnection) -> None:
+ """Safely close a connection"""
+ try:
+ if conn in self._all_connections:
+ self._all_connections.remove(conn)
+ if conn in self._connection_times:
+ del self._connection_times[conn]
+ if conn in self._connection_usage:
+ del self._connection_usage[conn]
+ conn.close()
+ self._fire_event(PoolEvent.CONNECTION_CLOSED, conn)
+ except Exception:
+ pass
+
+ def _fire_event(self, event: PoolEvent, conn: SyncDatabaseConnection) -> None:
+ """Fire event to registered handlers"""
+ if event in self._event_handlers:
+ for handler in self._event_handlers[event]:
+ try:
+ handler(conn)
+ except Exception as e:
+ self.logger.error(f"Event handler error: {e}")
+
+ def add_event_handler(self, event: PoolEvent, handler: Callable) -> None:
+ """Add event handler"""
+ if event not in self._event_handlers:
+ self._event_handlers[event] = []
+ self._event_handlers[event].append(handler)
+
+ def close(self) -> None:
+ """Close pool and all background workers"""
+ if self._closed:
+ return
+
+ self._closed = True
+ self._stop_event.set()
+
+ with self._condition:
+ # Close all connections
+ for conn, _ in self._available:
+ self._safe_close_connection(conn)
+ self._available.clear()
+
+ for conn in self._in_use:
+ self._safe_close_connection(conn)
+ self._in_use.clear()
+
+ # Wake any waiting threads
+ self._condition.notify_all()
+
+ def health_check(self) -> None:
+ """Manual health check"""
+ self._background_health_check()
+
+ @property
+ def size(self) -> int:
+ """Total pool size"""
+ with self._lock:
+ return len(self._all_connections)
+
+ @property
+ def available(self) -> int:
+ """Available connections"""
+ with self._lock:
+ return len(self._available)
+
+ def get_stats(self) -> Dict:
+ """Get pool statistics"""
+ with self._lock:
+ return {
+ **self._stats,
+ 'total_connections': len(self._all_connections),
+ 'idle_connections': len(self._available),
+ 'in_use_connections': len(self._in_use),
+ 'avg_usage_per_conn': statistics.mean(self._connection_usage.values()) if self._connection_usage else 0,
+ }
+
+ @contextmanager
+ def connection(self):
+ """Context manager for connections"""
+ conn = None
+ try:
+ conn = self.get_connection()
+ yield conn
+ except Exception:
+ if conn:
+ with self._lock:
+ if conn in self._in_use:
+ del self._in_use[conn]
+ self._safe_close_connection(conn)
+ raise
+ finally:
+ if conn:
+ self.return_connection(conn)
\ No newline at end of file
diff --git a/nexios/orm/pool/factory.py b/nexios/orm/pool/factory.py
new file mode 100644
index 00000000..3518446d
--- /dev/null
+++ b/nexios/orm/pool/factory.py
@@ -0,0 +1,45 @@
+from typing import Callable, Awaitable
+
+from nexios.orm.connection import SyncDatabaseConnection, AsyncDatabaseConnection
+from nexios.orm.pool.async_connection_pool import AsyncConnectionPool
+from nexios.orm.pool.base import (
+ BaseConnectionPool,
+ BaseAsyncConnectionPool,
+ PoolConfig,
+)
+from nexios.orm.pool.connection_pool import ConnectionPool
+
+
+class ConnectionPoolFactory:
+ @staticmethod
+ def config(min_size, max_size, **kwargs) -> PoolConfig:
+ return PoolConfig(
+ max_size=max_size,
+ min_size=min_size,
+ connection_timeout=kwargs.get("connection_timeout", 5.0),
+ max_lifetime=kwargs.get("max_lifetime", 7200),
+ idle_timeout=kwargs.get("idle_timeout", 300),
+ health_check_interval=kwargs.get("health_check_interval", 60),
+ shrink_interval=kwargs.get("shrink_interval", 30),
+ max_idle=kwargs.get("max_idle", 10),
+ )
+
+ @staticmethod
+ def create_sync_pool(
+ connection: Callable[[], SyncDatabaseConnection],
+ max_size: int = 10,
+ min_size: int = 1,
+ **kwargs,
+ ) -> BaseConnectionPool:
+ config = ConnectionPoolFactory.config(min_size, max_size, **kwargs)
+ return ConnectionPool(connection, config)
+
+ @staticmethod
+ def create_async_pool(
+ connection: Callable[[], Awaitable[AsyncDatabaseConnection]],
+ max_size: int = 10,
+ min_size: int = 1,
+ **kwargs,
+ ) -> BaseAsyncConnectionPool:
+ config = ConnectionPoolFactory.config(min_size, max_size, **kwargs)
+ return AsyncConnectionPool(connection, config)
diff --git a/nexios/orm/pytest.ini b/nexios/orm/pytest.ini
new file mode 100644
index 00000000..c1fb21bb
--- /dev/null
+++ b/nexios/orm/pytest.ini
@@ -0,0 +1,21 @@
+[pytest]
+testpaths = tests
+python_files = test_*.py
+python_classes = Test*
+python_functions = test_*
+addopts =
+ -v
+ --tb=short
+ --strict-markers
+ --disable-warnings
+
+markers =
+ asyncio: marks tests as async/await tests
+ slow: marks tests as slow (deselect with '-m "not slow"')
+ integration: marks tests as integration tests
+ postgres: tests specific to PostgreSQL
+ mysql: tests specific to MySQL
+ sqlite: tests specific to SQLite
+
+# Optional: Configure asyncio mode (newer versions)
+asyncio_mode = auto
\ No newline at end of file
diff --git a/nexios/orm/query/__init__.py b/nexios/orm/query/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/nexios/orm/query/builder.py b/nexios/orm/query/builder.py
new file mode 100644
index 00000000..6431aa63
--- /dev/null
+++ b/nexios/orm/query/builder.py
@@ -0,0 +1,1108 @@
+from __future__ import annotations
+
+import re
+
+from collections import defaultdict
+from typing import TypeAlias, cast, Callable, Any
+
+from typing_extensions import (
+ Type,
+ TypeVar,
+ Tuple,
+ List,
+ Union,
+ Optional,
+ Generic,
+ Literal,
+ Self,
+ Dict,
+ overload,
+ TYPE_CHECKING,
+)
+from pydantic_core import PydanticUndefined as Undefined
+from nexios.orm.query.expressions import ColumnExpression, BinaryExpression, AlwaysTrueExpression, AlwaysFalseExpression
+from nexios.orm.sessions import Session, AsyncSession
+from nexios.orm.relationships import RelationshipType
+
+if TYPE_CHECKING:
+ from nexios.orm.config import Dialect
+ from nexios.orm.model import NexiosModel
+ from nexios.orm.utils import InstanceOrType
+
+_T = TypeVar("_T", bound="NexiosModel")
+_M = TypeVar("_M", bound="NexiosModel")
+
+
+_BinaryExpressionTuple: TypeAlias = Tuple[BinaryExpression]
+_UnionBinaryExpression: TypeAlias = Union[BinaryExpression, str, None]
+
+
+class Select(Generic[_T]):
+ def __init__(self, *entities: Any):
+ self.entities = entities
+ self._where: List[BinaryExpression] = []
+ self._params: List[Any] = []
+ self._order_by: List[Union[ColumnExpression, str]] = []
+ self._limit: Optional[int] = None
+ self._offset: Optional[int] = None
+ self._session: Union[Session, AsyncSession, None] = None
+ self._joins: List[
+ Tuple[str, Type[NexiosModel], Optional[str], Optional[BinaryExpression]]
+ ] = []
+ self._distinct: bool = False
+ self._group_by: List[Union[ColumnExpression, str]] = []
+ self._having: List[BinaryExpression] = []
+ self._table_aliases: Dict[Type[NexiosModel], str] = {}
+ self._alias_counter = 0
+
+ self._eager_load: Dict[str, List[str]] = {}
+ self._joined_load: Dict[str, List[str]] = {}
+
+ def _generate_alias(self, model: Type[NexiosModel]) -> str:
+ """Generate unique table alias"""
+ if model in self._table_aliases:
+ return self._table_aliases[model]
+
+ # Try to use model name as alias
+ base_alias = model.__name__.lower()[:10]
+ alias = base_alias
+
+ # Ensure uniqueness
+ counter = 1
+ while alias in self._table_aliases.values():
+ alias = f"{base_alias}{counter}"
+ counter += 1
+
+ self._table_aliases[model] = alias
+ return alias
+
+ def _bind(self, session: Any):
+ self._session = session
+
+ def where(self, *conditions: bool) -> Self:
+ expr = cast(_BinaryExpressionTuple, cast(object, conditions))
+
+ for condition in expr:
+ if condition is True:
+ self._where.append(AlwaysTrueExpression()) # type: ignore
+ elif condition is False:
+ self._where.append(AlwaysFalseExpression()) # type: ignore
+ else:
+ self._where.extend(expr)
+ return self
+
+ def and_where(self, *conditions: bool) -> Self:
+ return self.where(*conditions)
+
+ def order_by(self, *fields: Union[ColumnExpression[_T], str]) -> Self:
+ self._order_by.extend(fields)
+ return self
+
+ def limit(self, n: int) -> Self:
+ self._limit = n
+ return self
+
+ def offset(self, n: int) -> Self:
+ self._offset = n
+ return self
+
+ def distinct(self) -> Self:
+ """Add DISTINCT clause"""
+ self._distinct = True
+ return self
+
+ def group_by(self, *fields: Union[ColumnExpression, str]) -> Self:
+ """Add GROUP BY clause"""
+ self._group_by.extend(fields)
+ return self
+
+ def having(self, *conditions: BinaryExpression) -> Self:
+ """Add HAVING clause"""
+ self._having.extend(conditions)
+ return self
+
+ def _transform_count(self, rows: List[Tuple[Any, ...]]) -> int:
+ return rows[0][0] if rows else 0
+
+ async def _async_count(self) -> int:
+ count_select = self._clone()
+ primary_model = self._get_primary_model()
+ count_select.entities = ("COUNT(*)", primary_model)
+ count_select._order_by = [] # Remove ordering for count
+ count_select._limit = None
+ count_select._offset = None
+
+ rows = await count_select._execute_async()
+ return self._transform_count(rows)
+
+ def _count(self) -> int:
+ """Return count of records matching the query"""
+ count_select = self._clone()
+ primary_model = self._get_primary_model()
+ count_select.entities = ("COUNT(*)", primary_model)
+ count_select._order_by = [] # Remove ordering for count
+ count_select._limit = None
+ count_select._offset = None
+
+ rows = count_select._execute_sync()
+ return self._transform_count(rows)
+
+ def _transform_exists(self, rows: List[Tuple[Any, ...]]) -> bool:
+ return len(rows) > 0
+
+ async def _async_exists(self) -> bool:
+ exists_select = self._clone()
+ primary_model = self._get_primary_model()
+ exists_select._limit = 1
+ exists_select.entities = ("1", primary_model)
+
+ rows = await exists_select._execute_async()
+ return self._transform_exists(rows)
+
+ def _exists(self) -> bool:
+ """Check if any records match the query"""
+ exists_select = self._clone()
+ primary_model = self._get_primary_model()
+ exists_select._limit = 1
+ exists_select.entities = ("1", primary_model)
+
+ rows = exists_select._execute_sync()
+ return self._transform_exists(rows)
+
+ def join(
+ self,
+ right_model: Type[NexiosModel],
+ on_condition: Optional[bool] = None,
+ join_type: Literal["inner", "left", "right", "full", "cross"] = "left",
+ alias: Optional[str] = None,
+ ) -> Self:
+ """Add JOIN clause on the query"""
+ expression = cast(_UnionBinaryExpression, on_condition)
+ join_alias = alias or self._generate_alias(right_model)
+
+ self._joins.append((join_type, right_model, join_alias, expression)) # type: ignore
+ return self
+
+ def left_join(
+ self,
+ right_model: Type[NexiosModel],
+ on_condition: Optional[bool] = None,
+ alias: Optional[str] = None,
+ ) -> Self:
+ return self.join(right_model, on_condition, "left", alias)
+
+ def inner_join(
+ self,
+ right_model: Type[NexiosModel],
+ on_condition: Optional[bool] = None,
+ alias: Optional[str] = None,
+ ) -> Self:
+ """Convenience method for INNER JOIN"""
+ return self.join(right_model, on_condition, "inner", alias)
+
+ def right_join(
+ self,
+ right_model: Type[NexiosModel],
+ on_condition: Optional[bool] = None,
+ alias: Optional[str] = None,
+ ) -> Self:
+ """Convenience method for RIGHT JOIN"""
+ return self.join(right_model, on_condition, "right", alias)
+
+ def eager_load(self, *relationships: str) -> Self:
+ """Add eager loading to the query"""
+ for rel_path in relationships:
+ print(f"Relationship path: {rel_path}")
+ parts = rel_path.split(".") or rel_path.split("_")
+ if len(parts) == 1:
+ self._eager_load.setdefault("*", []).append(rel_path)
+ else:
+ parent = parts[0]
+ child = ".".join(parts[1:])
+ self._eager_load.setdefault(parent, []).append(child)
+ return self
+
+ def joined_load(self, *relationships: str) -> Self:
+ """Add joined loading to the query"""
+ for rel_path in relationships:
+ parts = rel_path.split(".") or rel_path.split("_")
+ if len(parts) == 1:
+ self._joined_load.setdefault("*", []).append(rel_path)
+ else:
+ parent = parts[0]
+ child = ".".join(parts[1:]) or "_".join(parts[1:])
+ self._joined_load.setdefault(parent, []).append(child)
+ return self
+
+ def _infer_join_condition(
+ self, left_model: Type[NexiosModel], right_model: Type[NexiosModel]
+ ) -> BinaryExpression:
+ """Try to infer condition based on field names"""
+ left_fields = left_model.get_fields()
+ right_fields = right_model.get_fields()
+
+ # 1. Check foreign keys in left model pointing to right model
+ for left_field_name, left_info in left_fields.items():
+ left_fk = getattr(left_info, "foreign_key", Undefined)
+ if left_fk is not Undefined:
+ fk_parts = left_info.foreign_key.split(
+ "."
+ ) or left_info.foreign_key.split("_")
+ if len(fk_parts) == 2 and fk_parts[0] == right_model.__name__:
+ left_col = ColumnExpression(left_model, left_field_name)
+ right_col = ColumnExpression(right_model, fk_parts[1])
+ return BinaryExpression(left_col, "=", right_col)
+
+ # 2. Check foreign keys in right model pointing to left model
+ for right_field_name, right_info in right_fields.items():
+ right_fk = getattr(right_info, "foreign_key", Undefined)
+ if right_fk is not Undefined:
+ fk_parts = right_info.foreign_key.split(
+ "."
+ ) or right_info.foreign_key.split("_")
+ if len(fk_parts) == 2 and fk_parts[0] == left_model.__name__:
+ left_col = ColumnExpression(left_model, fk_parts[1])
+ right_col = ColumnExpression(right_model, right_field_name)
+ return BinaryExpression(left_col, "=", right_col)
+
+ # 3. Check relationships defined in ORM config
+ left_relationships = left_model.get_relationships()
+ for rel_name, rel_info in left_relationships.items():
+ if rel_info.related_model == right_model:
+ fk = rel_info.foreign_key
+ if fk:
+ fk_parts = re.split(r"[._]", fk)
+ if len(fk_parts) == 2:
+ left_col = ColumnExpression(left_model, fk_parts[1])
+ right_col = ColumnExpression(right_model, fk_parts[0])
+ return BinaryExpression(left_col, "=", right_col)
+
+ # 4. Common naming conventions
+ possible_fks = [
+ f"{left_model.__name__.lower()}_id",
+ f"{left_model.__tablename__}_id",
+ f"{left_model.__name__}Id",
+ f"{left_model.__name__}ID",
+ ]
+
+ for fk_name in possible_fks:
+ if fk_name in right_fields:
+ left_pk = self._get_primary_key_field(left_model)
+ if left_pk:
+ left_col = ColumnExpression(left_model, left_pk)
+ right_col = ColumnExpression(right_model, fk_name)
+ return BinaryExpression(left_col, "=", right_col)
+
+ # 5. Reverse
+ reverse_fks = [
+ f"{right_model.__name__.lower()}_id",
+ f"{right_model.__tablename__}_id",
+ f"{right_model.__name__}Id",
+ f"{right_model.__name__}ID",
+ ]
+
+ for fk_name in reverse_fks:
+ if fk_name in left_model.get_fields():
+ right_pk = self._get_primary_key_field(right_model)
+ if right_pk:
+ left_col = ColumnExpression(left_model, fk_name)
+ right_col = ColumnExpression(right_model, right_pk)
+ return BinaryExpression(left_col, "=", right_col)
+
+ # Lastly: if models have same primary key name
+ left_pk = self._get_primary_key_field(left_model)
+ right_pk = self._get_primary_key_field(right_model)
+
+ if left_pk and right_pk and left_pk == right_pk:
+ left_col = ColumnExpression(left_model, left_pk)
+ right_col = ColumnExpression(right_model, right_pk)
+ return BinaryExpression(left_col, "=", right_col)
+
+ raise ValueError(
+ f"Cannot infer join condition between {left_model.__name__} "
+ f"and {right_model.__name__}. Please provide explicit condition.\n"
+ f"Left model fields: {list(left_model.get_fields().keys())}\n"
+ f"Right model fields: {list(right_model.get_fields().keys())}\n"
+ f"Left model relationships: {list(left_model.get_relationships().keys())}"
+ )
+
+ def _get_primary_model(self) -> Optional[Type["NexiosModel"]]:
+ from nexios.orm.model import NexiosModel
+
+ for entity in self.entities:
+ if isinstance(entity, type) and issubclass(entity, NexiosModel):
+ return entity
+ elif isinstance(entity, ColumnExpression):
+ return entity.model_cls
+ return None
+
+ def _build_sql(self, dialect: Dialect, driver):
+ from nexios.orm.model import NexiosModel
+ from nexios.orm.config import get_param_placeholder
+
+ param_placeholder = get_param_placeholder(driver)
+ params: List[Any] = []
+ primary_model = self._get_primary_model()
+
+ if not primary_model:
+ raise ValueError("Could not determine primary model from selected entities")
+
+ select_parts = []
+ map_to_model = False
+
+ has_count = any(
+ isinstance(entity, str) and entity.startswith("COUNT")
+ for entity in self.entities
+ )
+ has_exists = any(
+ isinstance(entity, str) and entity.startswith("1")
+ for entity in self.entities
+ )
+
+ for entity in self.entities:
+ if isinstance(entity, type) and issubclass(entity, NexiosModel):
+ if has_count or has_exists:
+ continue
+
+ if entity == primary_model:
+ table_name = entity.__tablename__
+ assert table_name is not None
+ quoted_table = dialect.quote_identifier(table_name)
+
+ for field_name in entity.get_fields().keys():
+ quoted_field = dialect.quote_identifier(field_name)
+ select_parts.append(f"{quoted_table}.{quoted_field}")
+ # map_to_model = True
+ else:
+ alias = self._table_aliases.get(entity, entity.__tablename__)
+ quoted_alias = dialect.quote_identifier(alias) # type: ignore
+
+ for field_name in entity.get_fields().keys():
+ quoted_field = dialect.quote_identifier(field_name)
+ select_alias = dialect.quote_identifier(f"{alias}_{field_name}")
+ select_parts.append(
+ f"{quoted_alias}.{quoted_field} AS {select_alias}"
+ )
+ elif isinstance(entity, ColumnExpression):
+ # select_parts.append(str(entity))
+ table_name = entity.model_cls.__tablename__
+ table_alias = self._table_aliases.get(entity.model_cls, table_name)
+
+ select_parts.append(entity.to_sql(dialect, table_alias))
+ elif isinstance(entity, str):
+ select_parts.append(entity)
+
+ model_entities = [
+ e
+ for e in self.entities
+ if isinstance(e, type) and issubclass(e, NexiosModel)
+ ]
+ if len(model_entities) == 1 and not has_count and not has_exists:
+ map_to_model = True
+
+ # Build SELECT clause with distinct
+ distinct_clause = "DISTINCT " if self._distinct else ""
+ select_clause = f"SELECT {distinct_clause}{', '.join(select_parts)}"
+
+ primary_table = primary_model.__tablename__
+ primary_alias = self._table_aliases.get(primary_model, primary_table)
+ assert primary_alias is not None
+ assert primary_table is not None
+ from_clause = f"FROM {dialect.quote_identifier(primary_table)}"
+ if primary_alias != primary_table:
+ from_clause += f" AS {dialect.quote_identifier(primary_alias)}"
+
+ sql_parts = [select_clause, from_clause]
+
+ # JOIN clauses
+ for join_type, right_model, alias, condition in self._joins:
+ right_table = right_model.__tablename__
+ quoted_right_table = dialect.quote_identifier(right_table) # type: ignore
+ quoted_alias = dialect.quote_identifier(alias) # type: ignore
+
+ if condition is None:
+ condition = self._infer_join_condition(primary_model, right_model)
+
+ if isinstance(condition, BinaryExpression):
+ left_col = condition.column
+ right_val = condition.value
+
+ left_table_alias = self._table_aliases.get(
+ left_col.model_cls, left_col.model_cls.__tablename__
+ )
+
+ quoted_left = (
+ dialect.quote_identifier(left_table_alias) # type: ignore
+ + "."
+ + dialect.quote_identifier( # type: ignore
+ left_col.field_name
+ )
+ )
+
+ if isinstance(right_val, ColumnExpression):
+ right_table_alias = self._table_aliases.get(
+ right_val.model_cls, right_val.model_cls.__tablename__
+ )
+ quoted_right = (
+ dialect.quote_identifier(right_table_alias) # type: ignore
+ + "."
+ + dialect.quote_identifier( # type: ignore
+ right_val.field_name
+ )
+ )
+ on_sql = f"{quoted_left} {condition.operator} {quoted_right}"
+ on_params = []
+ else:
+ on_sql = f"{quoted_left} {condition.operator} {param_placeholder}"
+ on_params = [right_val] if right_val is not None else []
+ # on_sql, on_params = condition.to_sql(param_placeholder, dialect)
+ params.extend(on_params)
+ elif isinstance(condition, str):
+ on_sql = condition
+ else:
+ on_sql = "1=1"
+
+ join_sql = (
+ f"{join_type} JOIN {quoted_right_table} AS {quoted_alias} ON {on_sql}"
+ )
+ sql_parts.append(join_sql)
+
+ # WHERE
+ if self._where:
+ where_parts = []
+ for cond in self._where:
+ if self._joins:
+ left_col = cond.column
+ left_table_alias = self._table_aliases.get(
+ left_col.model_cls, left_col.model_cls.__tablename__
+ )
+ quoted_left = (
+ dialect.quote_identifier(left_table_alias) # type: ignore
+ + "."
+ + dialect.quote_identifier( # type: ignore
+ left_col.field_name
+ )
+ )
+ if isinstance(cond.value, ColumnExpression):
+ right_col = cond.value
+ right_table_alias = self._table_aliases.get(
+ right_col.model_cls, right_col.model_cls.__tablename__
+ )
+ quoted_right = (
+ dialect.quote_identifier(right_table_alias) # type: ignore
+ + "."
+ + dialect.quote_identifier( # type: ignore
+ right_col.field_name
+ )
+ )
+ sql_part = f"{quoted_left} {cond.operator} {quoted_right}"
+ p = []
+ else:
+ # Value comparison
+ sql_part = f"{quoted_left} {cond.operator} {param_placeholder}"
+ p = [cond.value] if cond.value is not None else []
+ else:
+ sql_part, p = cond.to_sql(param_placeholder, dialect, driver)
+
+ where_parts.append(sql_part)
+ params.extend(p)
+ where_clause = " AND ".join(where_parts)
+ sql_parts.append(f"WHERE {where_clause}")
+
+ # GROUP BY clause
+ if self._group_by:
+ group_parts = []
+ for field in self._group_by:
+ if isinstance(field, ColumnExpression):
+ # group_parts.append(str(field))
+ table_name = field.model_cls.__tablename__
+ table_alias = self._table_aliases.get(field.model_cls, table_name)
+ group_parts.append(field.to_sql(dialect, table_alias))
+ else:
+ group_parts.append(field)
+ group_clause = "GROUP BY " + ", ".join(group_parts)
+ sql_parts.append(group_clause)
+
+ # HAVING clause
+ if self._having:
+ having_parts = []
+ for condition in self._having:
+ cond_sql, cond_params = condition.to_sql(param_placeholder, dialect)
+ params.extend(cond_params)
+ having_parts.append(cond_sql)
+ having_clause = "HAVING " + " AND ".join(having_parts)
+ sql_parts.append(having_clause)
+
+ # ORDER BY
+ if self._order_by:
+ order_parts = []
+ for field in self._order_by:
+ if isinstance(field, ColumnExpression):
+ # order_parts.append(str(field))
+ table_name = field.model_cls.__tablename__
+ table_alias = self._table_aliases.get(field.model_cls, table_name)
+
+ sql = field.to_sql(dialect, table_alias)
+ if getattr(field, "_order_desc", False):
+ sql += " DESC"
+ order_parts.append(sql)
+ else:
+ order_parts.append(field)
+
+ order_clause = "ORDER BY " + ", ".join(order_parts)
+ sql_parts.append(order_clause)
+
+ # LIMIT and OFFSET
+ limit_sql = dialect.get_limit_offset_sql(self._limit, self._offset)
+ if limit_sql:
+ sql_parts.append(limit_sql)
+
+ sql = " ".join(sql_parts)
+
+ return sql, params, primary_model, map_to_model
+
+ def _execute_sync(self) -> List[Tuple[Any, ...]]:
+ """Execute query synchronously"""
+ if not self._session or not isinstance(self._session, Session):
+ raise ValueError("No sync session bound to this query")
+
+ sql, params, model, map_to_model = self._build_sql(
+ self._session.engine.dialect, self._session.engine.driver
+ )
+ return self._session.execute(sql, tuple(params)).fetchall()
+
+ async def _execute_async(self) -> List[Tuple[Any, ...]]:
+ """Execute query asynchronously"""
+ if not self._session or not isinstance(self._session, AsyncSession):
+ raise ValueError("No async session bound to this query")
+
+ sql, params, model, map_to_model = self._build_sql(
+ self._session.engine.dialect, self._session.engine.driver
+ )
+ return await (await self._session.execute(sql, tuple(params))).fetchall()
+
+ def _execute_with_eager_loading(self, rows_func: Callable):
+ """Execute query with eager loading support"""
+ # First, get the main results
+ results = rows_func()
+
+ if not results or not self._eager_load:
+ return results
+
+ # Load eager relationships
+ self._load_eager_relationships(results)
+
+ return results
+
+ def _load_eager_relationships(self, instances: List[Any]):
+ """Load eager relationships for a list of instances"""
+ if not instances:
+ return
+
+ session = self._session
+ if not session:
+ return
+
+ # Group relationships by type for batch loading
+ relationships_to_load = defaultdict(list)
+
+ for rel_name in self._eager_load.get("*", []):
+ for instance in instances:
+ relationships_to_load[rel_name].append(instance)
+
+ # Batch load each relationship
+ for rel_name, rel_instances in relationships_to_load.items():
+ if not rel_instances:
+ continue
+
+ # Get relationship info from first instance
+ first_instance = rel_instances[0]
+ model_class = type(first_instance)
+
+ if rel_name not in model_class.__relationships__:
+ continue
+
+ rel_info = model_class.__relationships__[rel_name]
+
+ # Batch load based on relationship type
+ if rel_info.relationship_type in (RelationshipType.MANY_TO_ONE, RelationshipType.ONE_TO_ONE):
+ self._batch_load_many_to_one(rel_instances, rel_name, rel_info, session)
+ elif rel_info.relationship_type == RelationshipType.ONE_TO_MANY:
+ self._batch_load_one_to_many(rel_instances, rel_name, rel_info, session)
+
+ def _batch_load_many_to_one(self, instances, rel_name, rel_info, session: Any):
+ """Batch load many-to-one relationships to avoid N+1"""
+ # Collect foreign key values
+ fk_values = []
+ instance_map = {}
+
+ is_inverse_one_to_one = rel_info.relationship_type == RelationshipType.ONE_TO_ONE and not rel_info.foreign_key
+
+ if is_inverse_one_to_one:
+ # For inverse 1:1, we load like a 1:M but expect single result
+ return self._batch_load_one_to_many(instances, rel_name, rel_info, session)
+
+ if not rel_info.foreign_key:
+ return
+
+ for instance in instances:
+ fk_value = getattr(instance, rel_info.foreign_key, None)
+ if fk_value is not None:
+ fk_values.append(fk_value)
+ instance_map[fk_value] = instance
+
+ if not fk_values:
+ return
+
+ # Fetch all related objects in one query
+ related_model = rel_info.related_model
+ pk_field = self._get_primary_key_field(related_model)
+
+ query = select(related_model).where(
+ getattr(related_model, pk_field).in_(fk_values)
+ )
+ query._bind(session)
+
+ related_objects = query._all()
+
+ # Map back to instances
+ for obj in related_objects:
+ pk_value = getattr(obj, pk_field)
+ if pk_value in instance_map:
+ instance = instance_map[pk_value]
+ self._set_relationship_cache(instance, rel_name, obj)
+
+ def _batch_load_one_to_many(self, instances, rel_name, rel_info, session):
+ """Batch load one-to-many relationships to avoid N+1"""
+ if not rel_info.foreign_key:
+ return
+
+ # Collect primary key values
+ pk_values = []
+ instance_map = {}
+
+ for instance in instances:
+ pk_name = self._get_primary_key_field(instance)
+ pk_value = getattr(instance, pk_name)
+ pk_values.append(pk_value)
+ instance_map[pk_value] = instance
+
+ if not pk_values:
+ return
+
+ # Fetch all related objects in one query
+ related_model = rel_info.related_model
+
+ query = select(related_model).where(
+ getattr(related_model, rel_info.foreign_key).in_(pk_values)
+ )
+ query._bind(session)
+
+ all_related = query._all()
+
+ # Group by foreign key
+ related_by_fk = defaultdict(list)
+ for obj in all_related:
+ fk_value = getattr(obj, rel_info.foreign_key)
+ related_by_fk[fk_value].append(obj)
+
+ # Map back to instances
+ for pk_value, instance in instance_map.items():
+ self._set_relationship_cache(
+ instance, rel_name, related_by_fk.get(pk_value, [])
+ )
+
+ async def _async_load_eager_relationships(self, instances: List[Any]):
+ if not instances or not self._eager_load:
+ return
+ relationships_to_load = defaultdict(list)
+
+ for rel_name in self._eager_load.get("*", []):
+ for instance in instances:
+ relationships_to_load[rel_name].append(instance)
+
+ for parent_rel, child_rels in self._eager_load.items():
+ if parent_rel == "*":
+ continue
+ for instance in instances:
+ pass
+ if not self._session:
+ raise ValueError("Session not bound to query.")
+
+ for rel_name, rel_instances in relationships_to_load.items():
+ if not rel_instances:
+ continue
+
+ firs_instance = rel_instances[0]
+ model_class = type(firs_instance)
+
+ if rel_name not in model_class.__relationships__:
+ continue
+
+ rel_info = model_class.__relationships__[rel_name]
+
+ if rel_info.relationship_type in (RelationshipType.MANY_TO_ONE, RelationshipType.ONE_TO_ONE):
+ await self._async_batch_load_many_to_one(
+ rel_instances, rel_name, rel_info, self._session
+ )
+ elif rel_info.relationship_type == RelationshipType.ONE_TO_MANY:
+ await self._async_batch_load_one_to_many(
+ rel_instances, rel_name, rel_info, self._session
+ )
+ elif rel_info.relationship_type == RelationshipType.MANY_TO_MANY:
+ await self._async_batch_load_many_to_many(
+ rel_instances, rel_name, rel_info, self._session
+ )
+
+ async def _async_batch_load_many_to_one(
+ self, instances, rel_name, rel_info, session
+ ):
+ is_inverse_one_to_one = rel_info.relationship_type == RelationshipType.ONE_TO_ONE and not rel_info.foreign_key
+
+ if is_inverse_one_to_one:
+ await self._async_batch_load_one_to_many(instances, rel_name, rel_info, session)
+ return
+
+ if not rel_info.foreign_key:
+ return
+
+ fk_values = []
+ instance_map = {}
+
+ for instance in instances:
+ fk_value = getattr(instance, rel_info.foreign_key, None)
+ if fk_value is not None:
+ fk_values.append(fk_value)
+ instance_map[fk_value] = instance
+
+ if not fk_values:
+ return
+
+ related_model = rel_info.related_model
+ pk_field = self._get_primary_key_field(related_model)
+
+ query = select(related_model).where(
+ getattr(related_model, pk_field).in_(fk_values)
+ )
+ related_objects = await query._all_async()
+
+ for obj in related_objects:
+ pk_value = getattr(obj, pk_field)
+ if pk_value in instance_map:
+ instance = instance_map[pk_value]
+ self._set_relationship_cache(instance, rel_name, obj)
+
+ async def _async_batch_load_one_to_many(
+ self, instances, rel_name, rel_info, session
+ ):
+ if not rel_info.foreign_key:
+ return
+
+ pk_values = []
+ instance_map = {}
+
+ for instance in instances:
+ pk_name = self._get_primary_key_field(instance)
+ pk_value = getattr(instance, pk_name)
+ pk_values.append(pk_value)
+ instance_map[pk_value] = instance
+
+ if not pk_values:
+ return
+
+ related_model = rel_info.related_model
+
+ query = select(related_model).where(
+ getattr(related_model, rel_info.foreign_key).in_(pk_values)
+ )
+
+ all_related = await query._all_async()
+
+ related_by_fk = defaultdict(list)
+ for obj in all_related:
+ fk_value = getattr(obj, rel_info.foreign_key)
+ related_by_fk[fk_value].append(obj)
+
+ for pk_value, instance in instance_map.items():
+ self._set_relationship_cache(
+ instance, rel_name, related_by_fk.get(pk_value, [])
+ )
+
+ async def _async_batch_load_many_to_many(
+ self, instances, rel_name, rel_info, session
+ ):
+ through_model = rel_info.through_model
+ related_model = rel_info.related_model
+
+ if not through_model:
+ return
+
+ pk_values = []
+ instance_map = {}
+
+ for instance in instances:
+ pk_name = self._get_primary_key_field(instance)
+ pk_value = getattr(instance, pk_name)
+ pk_values.append(pk_value)
+ instance_map[pk_value] = instance
+
+ if not pk_values:
+ return
+
+ local_col = rel_info.local_column or f"{type(instances[0]).__name__.lower()}_id"
+ foreign_col = rel_info.foreign_column or f"{related_model.__name__.lower()}_id"
+
+ query1 = select(getattr(through_model, foreign_col)).where(
+ getattr(through_model, local_col).in_(pk_values)
+ )
+ rows = await query1._all_async()
+
+ for row in rows:
+ source_id = row[0] # Assuming first column is local ID
+ related_id = row[1] if len(row) > 1 else row[0]
+
+ all_related_ids = set()
+ id_mapping = defaultdict(list)
+
+ for row in rows:
+ if len(row) >= 2:
+ source_id = row[0]
+ related_id = row[1]
+ all_related_ids.add(related_id)
+ id_mapping[source_id].append(related_id)
+
+ if not all_related_ids:
+ return
+
+ query2 = select(related_model).where(
+ getattr(related_model, "id").in_(list(all_related_ids))
+ )
+
+ all_related = await query2._all_async()
+
+ # Create mapping of ID -> related object
+ related_by_id = {getattr(obj, "id"): obj for obj in all_related}
+
+ # Map back to instances
+ for pk_value, instance in instance_map.items():
+ related_ids = id_mapping.get(pk_value, [])
+ related_objects = [
+ related_by_id.get(rid) for rid in related_ids if rid in related_by_id
+ ]
+ self._set_relationship_cache(instance, rel_name, related_objects)
+
+ # Sync methods
+ def _all(self) -> List[_T]:
+ """Execute and return all results"""
+ from nexios.orm.model import NexiosModel
+
+ if self._eager_load:
+ return self._execute_with_eager_loading(self._execute_sync)
+
+ rows = self._execute_sync()
+ if self._session:
+ sql, params, model, map_to_model = self._build_sql(
+ self._session.engine.dialect, self._session.engine.driver
+ )
+ model_entities = [
+ e
+ for e in self.entities
+ if isinstance(e, type) and issubclass(e, NexiosModel)
+ ]
+ return self._rows_to_models(rows, model, map_to_model, model_entities)
+ else:
+ raise ValueError("Session not bound to query.")
+
+ def _first(self) -> Optional[_T]:
+ """Execute and return first result - OPTIMIZED"""
+ from nexios.orm.model import NexiosModel
+
+ original_limit = self._limit
+ self._limit = 1
+
+ try:
+ rows = self._execute_sync()
+ if not self._session:
+ raise ValueError("Session not bound to query.")
+ sql, params, model, map_to_model = self._build_sql(
+ self._session.engine.dialect, self._session.engine.driver
+ )
+ model_entities = [
+ e
+ for e in self.entities
+ if isinstance(e, type) and issubclass(e, NexiosModel)
+ ]
+ results = self._rows_to_models(rows, model, map_to_model, model_entities)
+ return results[0] if results else None
+ finally:
+ self._limit = original_limit
+
+ # Async methods
+ async def _all_async(self) -> List[_T]:
+ """Execute and return all results asynchronously"""
+ from nexios.orm.model import NexiosModel
+
+ rows = await self._execute_async()
+ if not self._session:
+ raise ValueError("Session not bound to query.")
+
+ sql, params, model, map_to_model = self._build_sql(
+ self._session.engine.dialect, self._session.engine.driver
+ )
+ model_entities = [
+ a
+ for a in self.entities
+ if isinstance(a, type) and issubclass(a, NexiosModel)
+ ]
+ results = self._rows_to_models(rows, model, map_to_model, model_entities)
+
+ if self._eager_load:
+ await self._async_load_eager_relationships(results)
+ return results
+
+ async def _first_async(self) -> Optional[_T]:
+ """Execute and return first result asynchronously - OPTIMIZED"""
+ from nexios.orm.model import NexiosModel
+
+ original_limit = self._limit
+ self._limit = 1
+
+ try:
+ rows = await self._execute_async()
+ if not self._session:
+ raise ValueError("Session not bound to query.")
+ sql, params, model, map_to_model = self._build_sql(
+ self._session.engine.dialect, self._session.engine.driver
+ )
+ model_entities = [
+ a
+ for a in self.entities
+ if isinstance(a, type) and issubclass(a, NexiosModel)
+ ]
+ results = self._rows_to_models(rows, model, map_to_model, model_entities)
+ if self._eager_load:
+ await self._async_load_eager_relationships(results)
+ return results[0] if results else None
+ finally:
+ self._limit = original_limit
+
+ def _rows_to_models(
+ self,
+ rows: List[Tuple[Any, ...]],
+ model: Type[NexiosModel],
+ map_to_model: bool,
+ model_entities: List[Any],
+ ) -> List[Any]:
+ """Convert DB rows into model instances (uses pydantic parsing when available)."""
+
+ results: List[Any] = []
+
+ if not rows:
+ return results
+
+ if map_to_model:
+ # assume columns for the primary model come first in the result set
+ field_names = list(model.get_fields().keys())
+ n = len(field_names)
+ for row in rows:
+ values = row[:n]
+ data = {name: val for name, val in zip(field_names, values)}
+ # prefer pydantic-style parsing if provided
+ if hasattr(model, "model_validate"):
+ inst = model.model_validate(data)
+ else:
+ inst = model(**data)
+ results.append(inst)
+ return results
+ elif len(model_entities) > 1:
+ model_field_counts = []
+ for model_cls in model_entities:
+ model_field_counts.append(len(model_cls.get_fields()))
+
+ for row in rows:
+ model_instances = []
+ start_idx = 0
+
+ for i, model_cls in enumerate(model_entities):
+ field_count = model_field_counts[i]
+ model_slice = row[start_idx : start_idx + field_count]
+
+ field_names = list(model_cls.get_fields().keys())
+ data = {name: val for name, val in zip(field_names, model_slice)}
+
+ if hasattr(model_cls, "model_validate"):
+ inst = model_cls.model_validate(data)
+ else:
+ inst = model_cls(**data)
+
+ model_instances.append(inst)
+ start_idx += field_count
+ results.append(tuple(model_instances))
+ return results
+ else:
+ # Not mapping to a model: return scalar for single-column selects, else tuples
+ for row in rows:
+ if len(row) == 1:
+ results.append(row[0])
+ else:
+ results.append(row)
+ return results
+
+ def _get_primary_key_field(self, model: InstanceOrType[NexiosModel]) -> Any:
+ pk_field = model.get_primary_key()
+ return pk_field[0] if isinstance(pk_field, (list, tuple)) else pk_field
+
+ def _set_relationship_cache(self, instance: Any, rel_name: str, value: Any):
+ """Set the relationship cache for an instance"""
+ if "__relationship_cache__" not in instance.__dict__:
+ instance.__dict__["__relationship_cache__"] = {}
+ instance.__dict__["__relationship_cache__"][rel_name] = value
+
+ def _clone(self) -> "Select[_T]":
+ """Create a copy of the current Select instance"""
+ new_select = Select(*self.entities)
+ new_select._where = self._where[:]
+ new_select._params = self._params[:]
+ new_select._order_by = self._order_by[:]
+ new_select._limit = self._limit
+ new_select._offset = self._offset
+ new_select._session = self._session
+ new_select._joins = self._joins[:]
+ new_select._distinct = self._distinct
+ new_select._group_by = self._group_by[:]
+ new_select._having = self._having[:]
+ new_select._table_aliases = self._table_aliases.copy()
+ new_select._alias_counter = self._alias_counter
+ return new_select
+
+
+@overload
+def select(entity: Type[_T]) -> Select[_T]:
+ """Single model select"""
+
+
+@overload
+def select(*entities: Type[_M]) -> Select[_M]:
+ """"""
+
+
+@overload
+def select(*entities: ColumnExpression[Any]) -> Select[Any]: ...
+
+
+@overload
+def select(*entities: Union[Type[_M], ColumnExpression[Any], str]) -> Select[Any]: ...
+
+
+def select(
+ entity: Union[Type[_T], ColumnExpression[Any], str],
+ *entities: Union[Type[_M], ColumnExpression[Any], str],
+):
+ """Create a SELECT query.
+
+ Examples:
+ >>> query = select(User) # Select[User]
+ >>> query = select(User, Post) # Select[Any] (tuple/union)
+ >>> query = select(User.name, User.email) # Select[Any] (tuple)
+ """
+ return Select(entity, *entities)
diff --git a/nexios/orm/query/expressions.py b/nexios/orm/query/expressions.py
new file mode 100644
index 00000000..67e7019f
--- /dev/null
+++ b/nexios/orm/query/expressions.py
@@ -0,0 +1,295 @@
+from __future__ import annotations
+
+from typing import Any, Generic, List, Optional, Self, Tuple, Type, TypeVar, Union, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from nexios.orm.config import Dialect
+ from nexios.orm.model import NexiosModel
+ from nexios.orm.query.builder import Select
+
+
+Expression = Union["BinaryExpression", "CompoundExpression"]
+_T = TypeVar("_T", bound="NexiosModel")
+
+
+class CompoundExpression:
+ """Represents AND/OR combination of expressions"""
+
+ def __init__(self, left: Expression, operator: str, right: Expression):
+ self.left = left
+ self.operator = operator # 'AND' or 'OR'
+ self.right = right
+
+ def __and__(self, other: Expression) -> "CompoundExpression":
+ return CompoundExpression(self, "AND", other)
+
+ def __or__(self, other: Expression) -> "CompoundExpression":
+ return CompoundExpression(self, "OR", other)
+
+ def to_sql(
+ self, placeholder: str = "?", dialect: Optional[Any] = None, driver: Optional[Any] = None
+ ) -> Tuple[str, List[Any]]:
+ left_sql, left_params = self.left.to_sql(placeholder, dialect, driver)
+ right_sql, right_params = self.right.to_sql(placeholder, dialect, driver)
+
+ sql = f"({left_sql} {self.operator} {right_sql})"
+ params = left_params + right_params
+ return sql, params
+
+
+class TSVectorExpression:
+ def __init__(
+ self, column: ColumnExpression, query: str, config: Optional[str] = None
+ ):
+ self.column = column
+ self.query = query
+ self.config = config
+
+ def to_sql(
+ self, placeholder: str = "?", dialect: Optional[Any] = None, driver: Optional[Any] = None
+ ) -> Tuple[str, List[Any]]:
+ quoted_column = dialect.quote_identifier(self.column.field_name) # type: ignore
+ if self.config:
+ return (
+ f"to_tsvector('{self.config}',{quoted_column})@@ plainto_tsquery('{self.config}',{placeholder})",
+ [self.query],
+ )
+ else:
+ return f"{quoted_column}@@ plainto_tsquery({placeholder})", [self.query]
+
+
+class MatchExpression:
+ def __init__(self, column: ColumnExpression, query: str, mode: str = "BOOLEAN"):
+ self.column = column
+ self.query = query
+ self.mode = mode
+
+ def to_sql(
+ self, placeholder: str = "?", dialect: Optional[Any] = None, driver: Optional[Any] = None
+ ) -> Tuple[str, List[Any]]:
+ quoted_column = dialect.quote_identifier(self.column.field_name) # type: ignore
+ if self.mode == "BOOLEAN":
+ return f"MATCH({quoted_column}) AGAINST({placeholder} IN BOOLEAN MODE)", [
+ self.query
+ ]
+ else:
+ return f"MATCH({quoted_column}) AGAINST({placeholder})", [self.query]
+
+
+class BM25Expression:
+ def __init__(self, column: ColumnExpression, query: str):
+ self.column = column
+ self.query = query
+
+ def to_sql(
+ self, placeholder: str = "?", dialect: Optional[Any] = None, driver: Optional[Any] = None
+ ) -> Tuple[str, List[Any]]:
+ quoted_column = dialect.quote_identifier(self.column.field_name) # type: ignore
+ return f"{quoted_column} MATCH {placeholder}", [self.query]
+
+
+class AlwaysTrueExpression:
+ """An expression that's always true"""
+ def to_sql(self, placeholder: str = "?", dialect=None, driver=None) -> Tuple[str, List[Any]]:
+ return "1 = 1", []
+
+class AlwaysFalseExpression:
+ """An expression that's always false"""
+ def to_sql(self, placeholder: str = "?", dialect=None, driver=None) -> Tuple[str, List[Any]]:
+ return "1 = 0", []
+
+class BinaryExpression: # type: ignore
+ def __init__(self, column_expr: ColumnExpression, operator: str, value: Any):
+ self.column = column_expr
+ self.operator = operator
+ self.value = value
+
+ def __and__(self, other: Expression) -> CompoundExpression:
+ return CompoundExpression(self, "AND", other)
+
+ def __or__(self, other: Expression) -> CompoundExpression:
+ return CompoundExpression(self, "OR", other)
+
+ def to_sql(
+ self,
+ placeholder: str = "?",
+ dialect: Optional[Any] = None,
+ driver: Optional[Any] = None,
+ ) -> Tuple[str, List[Any]]:
+ from nexios.orm.query.builder import Select
+
+ col = self.column.field_name
+ if self.value is None:
+ if self.operator == "=":
+ return f"{col} IS NULL", []
+ if self.operator == "!=":
+ return f"{col} IS NOT NULL", []
+
+ if isinstance(self.value, ColumnExpression):
+ right_col = self.value.field_name
+ return f"{col} {self.operator} {right_col}", []
+
+ if self.operator in ("IN", "NOT IN"):
+ if isinstance(self.value, Select):
+ subquery_sql, subquery_params, _, _ = self.value._build_sql(
+ dialect, driver # type: ignore
+ )
+ return f"{col} {self.operator} ({subquery_sql})", subquery_params
+
+ if not isinstance(self.value, (list, tuple)):
+ raise ValueError(f"{self.operator} requires a list or tuple")
+
+ if not self.value:
+ if self.operator == "IN":
+ return "1 = 0", []
+ else:
+ return "1 = 1", []
+
+ placeholders = ", ".join([placeholder] * len(self.value))
+ return f"{col} {self.operator} ({placeholders})", list(self.value)
+
+ if self.operator == "BETWEEN":
+ if not isinstance(self.value, (list, tuple)) or len(self.value) != 2:
+ raise ValueError("BETWEEN requires a 2-tuple (lower, upper)")
+
+ return f"{col} BETWEEN {placeholder} AND {placeholder}", list(self.value)
+
+ if self.operator in ("LIKE", "ILIKE"):
+ value = str(self.value)
+ if "%" not in value and "_" not in value:
+ value = f"%{value}%"
+
+ return f"{col} {self.operator} {placeholder}", [value]
+
+ return f"{col} {self.operator} {placeholder}", [self.value]
+
+
+class ColumnExpression(Generic[_T]):
+ """Represents a column in a select expression"""
+
+ def __init__(self, model_cls: Type[_T], field_name: str):
+ self.model_cls = model_cls
+ self.field_name = field_name
+ self._alias = None
+ self._order_desc = False
+
+ def _binary(self, operator: str, value: Any) -> BinaryExpression:
+ return BinaryExpression(self, operator, value)
+
+ def __eq__(self, other: Any) -> BinaryExpression: # type: ignore[override]
+ return self._binary("=", other)
+
+ def __ne__(self, other: Any) -> BinaryExpression: # type: ignore[override]
+ return self._binary("!=", other)
+
+ def __lt__(self, other: Any) -> BinaryExpression:
+ return self._binary("<", other)
+
+ def __le__(self, other: Any) -> BinaryExpression:
+ return self._binary("<=", other)
+
+ def __gt__(self, other: Any) -> BinaryExpression:
+ return self._binary(">", other)
+
+ def __ge__(self, other: Any) -> BinaryExpression:
+ return self._binary(">=", other)
+
+ def like(self, value: str):
+ return self._binary("LIKE", value)
+
+ def ilike(self, value):
+ return self._binary("ILIKE", value)
+
+ def in_(self, values: Union[List[Any], Select]):
+ """IN operator for list values or subquery"""
+ return self._binary("IN", values)
+
+ def not_in(self, values: Union[List[Any], Select]):
+ return self._binary("NOT IN", values)
+
+ def between(self, lower: Any, upper: Any) -> BinaryExpression:
+ """BETWEEN operator"""
+ return self._binary("BETWEEN", (lower, upper))
+
+ def is_null(self) -> BinaryExpression:
+ """IS NULL operator"""
+ return self._binary("IS", None)
+
+ def is_not_null(self) -> BinaryExpression:
+ """IS NULL operator"""
+ return self._binary("IS NOT", None)
+
+ def asc(self) -> Self:
+ """Order by ascending"""
+ # return f"{self} ASC"
+ import copy
+
+ new_expr = copy.copy(self)
+ new_expr._order_desc = False
+ return new_expr
+
+ def desc(self) -> Self:
+ """Order by descending"""
+ # return f"{self} DESC"
+ import copy
+
+ new_expr = copy.copy(self)
+ new_expr._order_desc = True
+ return new_expr
+
+ def label(self, alias: str) -> ColumnExpression[_T]:
+ """Create an aliased column expression"""
+ # For simplicity, we'll handle aliasing in the SQL builder
+ self._alias = alias
+ return self
+
+ def to_sql(self, dialect: Dialect, table_alias: Optional[str] = None) -> str:
+ """Generate SQL with proper quoting and table qualification"""
+ quoted_field = dialect.quote_identifier(self.field_name)
+
+ if table_alias:
+ quoted_table = dialect.quote_identifier(table_alias)
+ sql = f"{quoted_table}.{quoted_field}"
+ else:
+ table_name = self.model_cls.__tablename__
+ quoted_table = dialect.quote_identifier(table_name) # type: ignore
+ sql = f"{quoted_table}.{quoted_field}"
+
+ if self._alias:
+ quoted_alias = dialect.quote_identifier(self._alias)
+ sql += f" AS {quoted_alias}"
+
+ return sql
+
+ def match(self, query: str, mode: str = "NATURAL"):
+ from nexios.orm.config import PostgreSQLDialect, MySQLDialect, SQLiteDialect
+
+ class GenericMatch:
+ def __init__(self, c, q, m):
+ self._column = c
+ self._query = q
+ self._mode = m
+
+ def to_sql(
+ self, placeholder: str = "?", dialect: Optional[Dialect] = None
+ ) -> tuple[str, list[Any]]:
+ if isinstance(dialect, PostgreSQLDialect):
+ return TSVectorExpression(self._column, self._query).to_sql(
+ placeholder, dialect
+ )
+ elif isinstance(dialect, MySQLDialect):
+ return MatchExpression(self._column, self._query).to_sql(
+ placeholder, dialect
+ )
+ elif isinstance(dialect, SQLiteDialect):
+ return BM25Expression(self._column, self._query).to_sql(
+ placeholder, dialect
+ )
+ else:
+ raise NotImplementedError()
+
+ return GenericMatch(self, query, mode)
+
+ def __str__(self) -> str:
+ return f"{self.field_name}"
+
diff --git a/nexios/orm/query/result.py b/nexios/orm/query/result.py
new file mode 100644
index 00000000..9ea38148
--- /dev/null
+++ b/nexios/orm/query/result.py
@@ -0,0 +1,84 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Generic, List, Optional, TypeVar
+from nexios.orm.query.builder import Select
+from nexios.orm.sessions import Session, AsyncSession
+
+if TYPE_CHECKING:
+ from nexios.orm.model import NexiosModel
+
+
+_T = TypeVar("_T", bound="NexiosModel")
+
+class ResultSet(Generic[_T]):
+ """Base class for result sets"""
+
+ def __init__(self, statement: Select[_T]) -> None:
+ self.statement = statement
+
+ def all(self) -> List[_T]:
+ raise NotImplementedError
+
+ def first(self) -> Optional[_T]:
+ raise NotImplementedError
+
+ def one(self) -> _T:
+ raise NotImplementedError
+
+ def count(self) -> int:
+ raise NotImplementedError
+
+ def exists(self) -> bool:
+ raise NotImplementedError
+
+
+class SyncResultSet(ResultSet[_T]):
+ """Synchronous result set"""
+
+ def __init__(self, statement: Select[_T], session: Session) -> None:
+ super().__init__(statement)
+ self.statement._bind(session)
+
+ def all(self) -> List[_T]:
+ return self.statement._all()
+
+ def first(self) -> Optional[_T]:
+ return self.statement._first()
+
+ def one(self) -> _T:
+ result = self.first()
+ if result is None:
+ raise ValueError("No row was found for one()")
+ return result
+
+ def count(self) -> int:
+ return self.statement._count()
+
+ def exists(self) -> bool:
+ return self.statement._exists()
+
+
+class AsyncResultSet(ResultSet[_T]):
+ """Asynchronous result set"""
+
+ def __init__(self, statement: Select[_T], session: AsyncSession) -> None:
+ super().__init__(statement)
+ self.statement._bind(session)
+
+ async def all(self) -> List[_T]: # type: ignore[override]
+ return await self.statement._all_async()
+
+ async def first(self) -> Optional[_T]: # type: ignore[override]
+ return await self.statement._first_async()
+
+ async def one(self) -> _T: # type: ignore[override]
+ result = await self.first()
+ if result is None:
+ raise ValueError("No row was found for one()")
+ return result
+
+ async def count(self) -> int: # type: ignore[override]
+ return await self.statement._async_count()
+
+ async def exists(self) -> bool: # type: ignore[override]
+ return await self.statement._async_exists()
diff --git a/nexios/orm/relationships.py b/nexios/orm/relationships.py
new file mode 100644
index 00000000..1e639cb8
--- /dev/null
+++ b/nexios/orm/relationships.py
@@ -0,0 +1,170 @@
+from __future__ import annotations
+
+from enum import Enum
+from typing import TYPE_CHECKING, Any, Optional, Type, Union, overload, Literal, Dict
+from dataclasses import dataclass, field
+from pydantic._internal._repr import Representation
+if TYPE_CHECKING:
+ from nexios.orm.model import NexiosModel
+ from nexios.orm.utils import OnDeleteOrUpdate, LazyOp
+
+
+class RelationshipType(Enum):
+ ONE_TO_ONE = "one_to_one"
+ ONE_TO_MANY = "one_to_many"
+ MANY_TO_ONE = "many_to_one"
+ MANY_TO_MANY = "many_to_many"
+
+@dataclass
+class RelationshipInfo(Representation):
+ """Information about a relationship between models."""
+
+ field_name: str
+ related_model_name: str # Store as string to avoid circular imports
+ relationship_type: Optional[RelationshipType] = None
+ foreign_key: Optional[str] = None
+ related_field_name: Optional[str] = None
+ through: Optional[str] = None # Store as string
+ ondelete: Optional[OnDeleteOrUpdate] = None
+ onupdate: Optional[OnDeleteOrUpdate] = None
+ nullable: bool = False
+ unique: bool = False
+ back_populates: Optional[str] = None
+ lazy: LazyOp = "select"
+
+ # Database constraints
+ deferrable: Optional[bool] = None
+ initially_deferred: Optional[bool] = None
+
+ # For many-to-many relationships
+ association_table: Optional[str] = None
+ local_column: Optional[str] = None
+ foreign_column: Optional[str] = None
+
+ # For tracking
+ is_resolved: bool = False
+ metadata: Dict[str, Any] = field(default_factory=dict)
+
+ # Cache for resolved models
+ _related_model: Optional[Type["NexiosModel"]] = None
+ _through_model: Optional[Type["NexiosModel"]] = None
+
+ @property
+ def through_model(self) -> Optional[Type["NexiosModel"]]:
+ if self._through_model is None and self.through:
+ from nexios.orm.model import NexiosModel
+ try:
+ if (
+ hasattr(NexiosModel, "__registry__")
+ and self.through in NexiosModel.__registry__
+ ):
+ self._through_model = NexiosModel.__registry__[self.through]
+ except (AttributeError, KeyError):
+ pass
+ return self._through_model
+
+ @property
+ def related_model(self) -> Optional[Type["NexiosModel"]]:
+ if self._related_model is None and self.related_model_name:
+ from nexios.orm.model import NexiosModel
+ try:
+ if (
+ hasattr(NexiosModel, "__registry__")
+ and self.related_model_name in NexiosModel.__registry__
+ ):
+ self._related_model = NexiosModel.__registry__[
+ self.related_model_name
+ ]
+ except (AttributeError, KeyError):
+ pass
+ return self._related_model
+
+
+# For one-to-one, one-to-many and many-to-one
+@overload
+def Relationship(
+ related_model: Union[Type[NexiosModel], str, None] = None,
+ relationship_type: Literal[
+ RelationshipType.MANY_TO_ONE,
+ RelationshipType.ONE_TO_MANY,
+ RelationshipType.ONE_TO_ONE,
+ ] = RelationshipType.MANY_TO_ONE,
+ *,
+ foreign_key: Optional[str] = None,
+ related_field_name: Optional[str] = None,
+ **kwargs: Any,
+) -> Any: ...
+
+
+# For many-to-many with through model
+@overload
+def Relationship(
+ related_model: Union[Type[NexiosModel], str, None] = None,
+ relationship_type: Literal[
+ RelationshipType.MANY_TO_MANY
+ ] = RelationshipType.MANY_TO_MANY,
+ *,
+ through: Union[Type["NexiosModel"], str, None] = None,
+ local_column: Optional[str] = None,
+ foreign_column: Optional[str] = None,
+ **kwargs: Any,
+) -> Any: ...
+
+
+def Relationship(
+ related_model: Union[Type[NexiosModel], str, None] = None,
+ relationship_type: Optional[RelationshipType] = None,
+ *,
+ foreign_key: Optional[str] = None,
+ related_field_name: Optional[str] = None,
+ through: Optional[Union[Type["NexiosModel"], str]] = None,
+ ondelete: Optional[OnDeleteOrUpdate] = None,
+ onupdate: Optional[OnDeleteOrUpdate] = None,
+ nullable: bool = False,
+ unique: bool = False,
+ back_populates: Optional[str] = None,
+ lazy: LazyOp = "select",
+ deferrable: Optional[bool] = None,
+ initially_deferred: Optional[bool] = None,
+ local_column: Optional[str] = None,
+ foreign_column: Optional[str] = None,
+ **kwargs: Any,
+) -> Any:
+ rel_model_str = None
+ if related_model:
+ rel_model_str = (
+ related_model if isinstance(related_model, str) else related_model.__name__
+ )
+ through_model_str = None
+ if through:
+ through_model_str = (
+ through
+ if isinstance(through, str)
+ else through.__name__
+ if through
+ else None
+ )
+
+ rel_type = relationship_type
+ if rel_type is None and through:
+ rel_type = RelationshipType.MANY_TO_MANY
+
+ return RelationshipInfo(
+ field_name="",
+ related_model_name=rel_model_str or "",
+ relationship_type=rel_type,
+ foreign_key=foreign_key,
+ related_field_name=related_field_name,
+ through=through_model_str,
+ ondelete=ondelete,
+ onupdate=onupdate,
+ nullable=nullable,
+ unique=unique,
+ back_populates=back_populates,
+ lazy=lazy,
+ deferrable=deferrable,
+ initially_deferred=initially_deferred,
+ local_column=local_column,
+ foreign_column=foreign_column,
+ metadata=kwargs,
+ )
diff --git a/nexios/orm/sessions.py b/nexios/orm/sessions.py
new file mode 100644
index 00000000..72cdfbe7
--- /dev/null
+++ b/nexios/orm/sessions.py
@@ -0,0 +1,354 @@
+from __future__ import annotations
+
+import logging
+from typing import TYPE_CHECKING, Any, Tuple, TypeVar, Optional, Type, List
+
+from nexios.orm.connection import (
+ AsyncDatabaseConnection,
+ SyncCursor,
+ AsyncCursor,
+ SyncDatabaseConnection,
+)
+from nexios.orm.misc.context import set_context_data, reset_context_data
+
+if TYPE_CHECKING:
+ from nexios.orm.engine import Engine
+ from nexios.orm.model import NexiosModel
+ from nexios.orm.query.builder import Select
+
+_T = TypeVar("_T", bound="NexiosModel")
+
+class Session:
+ """Synchronous session managing a database transaction."""
+
+ def __init__(self, engine: Engine, logger: Optional[logging.Logger] = None):
+ from nexios.orm.config import DDLGenerator
+
+ self.engine = engine
+ self.connection: Optional[SyncDatabaseConnection] = None
+ self._cursor: Optional[SyncCursor] = None
+ self.logger = logger or logging.getLogger(__name__)
+ self._ddl = DDLGenerator(engine.dialect, self.engine.driver)
+
+ self._token = None
+
+ @property
+ def cursor(self) -> SyncCursor:
+ """Return the active cursor or raise a helpful error if none exists."""
+ if self._cursor is None:
+ raise RuntimeError("No active DB cursor. Use 'with Session(engine) as s' or call 'connect()' first.")
+ return self._cursor
+
+ def connect(self):
+ """Explicitly open a connection and cursor outside a context manager."""
+ if self.connection is None:
+ self.connection = self.engine.connect()
+ self._cursor = self.engine.sync_cursor()
+ return self
+
+ def close(self):
+ """Close any opened connection and clear the cursor."""
+ if self.connection:
+ self.engine.return_connection(self.connection)
+ self.connection = None
+ self._cursor = None
+
+ def __enter__(self):
+ sess = self.connect()
+ self._token = set_context_data("session", sess)
+ return sess
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ try:
+ if exc_type is not None:
+ self.rollback()
+ else:
+ self.commit()
+ except Exception as e:
+ self.logger.error(f"Failed to commit or rollback: {e}")
+ finally:
+ if self.connection:
+ self.close()
+
+ if self._token:
+ reset_context_data("session", self._token)
+
+ def exec(self, statement: Select[_T]):
+ from nexios.orm.query.result import SyncResultSet
+ return SyncResultSet(statement, self)
+
+ def execute(self, sql: str, params: tuple = ()):
+ if self.engine.echo:
+ print("SQL:", sql, "params:", params)
+ return self.cursor.execute(sql, params)
+
+ def executemany(self, sql: str, params: List[Tuple[Any, ...]]):
+ if self.engine.echo:
+ print("SQL:", sql, "params:", params)
+ return self.cursor.executemany(sql, params)
+
+ def commit(self):
+ if self.connection:
+ self.connection.commit()
+
+ def rollback(self):
+ if self.connection:
+ self.connection.rollback()
+
+ def add(self, instance: _T):
+ from nexios.orm.query.expressions import ColumnExpression
+ from nexios.orm.config import MySQLDialect, SQLiteDialect, PostgreSQLDialect
+
+ sql, params = self._ddl.upsert(instance)
+
+ primary_key = self._ddl._get_primary_key(instance)
+
+ if isinstance(primary_key, ColumnExpression):
+ primary_key_field = primary_key.field_name
+ else:
+ primary_key_field = primary_key
+
+ fields = instance.get_fields()
+ field_info = fields.get(primary_key_field)
+ auto_increment = getattr(field_info, 'auto_increment', False) if field_info else False
+ if auto_increment:
+ if isinstance(self._ddl.dialect, SQLiteDialect):
+ self.execute(sql, params)
+ result = self.execute("SELECT last_insert_rowid()").fetchone()
+ last_id = result[0] if result else None
+ setattr(instance, primary_key_field, last_id)
+ elif isinstance(self._ddl.dialect, PostgreSQLDialect):
+ result = self.execute(sql, params).fetchone()
+ last_id = result[0] if result else None
+ setattr(instance, primary_key_field, last_id)
+ elif isinstance(self._ddl.dialect, MySQLDialect):
+ self.execute(sql, params)
+ result = self.execute("SELECT LAST_INSERT_ID()").fetchone()
+ setattr(instance, primary_key_field, result[0]) if result else None
+ else:
+ self.execute(sql, params)
+ else:
+ self.execute(sql, params)
+
+ def delete(self, model: _T):
+ sql, params = self._ddl.delete(model)
+ self.execute(sql, params)
+
+ def create_all(self, *models: Type[_T]):
+ """Create all tables for given models."""
+ try:
+ for model in models:
+ sql = self._ddl.create_table(model)
+ # Create tables
+ self.execute(sql)
+ # Create indexes
+ index_sql = self._ddl.create_indexes(model)
+ for idx in index_sql:
+ self.execute(idx)
+ self.commit()
+ except Exception as e:
+ self.rollback()
+ raise e
+
+ def drop(self, model: Type[_T]):
+ sql = self._ddl.drop_table(model)
+ self.execute(sql)
+
+ def update(self):
+ pass
+
+ def refresh(self, model: _T):
+ """
+ Refresh the given instance wit the current database state
+
+ Args:
+ model: Model instance to refresh
+ """
+ from nexios.orm.query.expressions import ColumnExpression
+ from nexios.orm.query.builder import select
+
+
+ primary_key = self._ddl._get_primary_key(model)
+ if isinstance(primary_key, ColumnExpression):
+ primary_key_field = primary_key.field_name
+ else:
+ primary_key_field = primary_key
+
+ model_class = type(model)
+ pk_value = getattr(model, primary_key_field, None)
+ if pk_value is None:
+ raise ValueError(f"Cannot refresh {model_class.__name__}:primary key is None")
+
+ query = select(model_class).where(getattr(model_class, primary_key_field) == pk_value)
+ refreshed = self.exec(query).first()
+
+ if refreshed is None:
+ raise ValueError(f"{model_class.__name__} with {primary_key_field}={pk_value} no longer exists")
+
+ for field_name in model.get_fields().keys():
+ new_value = getattr(refreshed, field_name, None)
+ setattr(model, field_name, new_value)
+
+
+class AsyncSession:
+ """Asynchronous session for async database operations."""
+
+ def __init__(self, engine: Engine, logger: Optional[logging.Logger] = None):
+ from nexios.orm.config import DDLGenerator
+
+ self.engine = engine
+ self.connection: Optional[AsyncDatabaseConnection] = None
+ self._cursor: Optional[AsyncCursor] = None
+ self.logger = logger or logging.getLogger(__name__)
+ self._ddl = DDLGenerator(engine.dialect, self.engine.driver)
+
+ self._token = None
+
+ @property
+ def cursor(self) -> AsyncCursor:
+ """Return the active async cursor or raise a helpful error if none exists."""
+ if self._cursor is None:
+ raise RuntimeError("No active async DB cursor. Use 'async with AsyncSession(engine) as s' or call 'await connect()' first.")
+ return self._cursor
+
+ async def connect(self):
+ """Explicitly open an async connection and cursor outside of an async context manager."""
+ if self.connection is None:
+ self.connection = await self.engine.async_connect()
+ self._cursor = await self.connection.cursor()
+ return self
+
+ async def close(self):
+ """Close any opened async connection and clear the cursor."""
+ if self.connection:
+ await self.engine.return_async_connection(self.connection)
+ self.connection = None
+ self._cursor = None
+
+ async def __aenter__(self):
+ sess = await self.connect()
+ self._token = set_context_data("session", sess)
+ return sess
+
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
+ try:
+ if exc_type is not None:
+ await self.rollback()
+ else:
+ await self.commit()
+ finally:
+ if self.connection:
+ await self.close()
+
+ if self._token:
+ reset_context_data("session", self._token)
+
+ def exec(self, statement: Select[_T]):
+ from nexios.orm.query.result import AsyncResultSet
+ return AsyncResultSet(statement, self)
+
+ async def execute(self, sql: str, params: tuple = ()):
+ if self.engine.echo:
+ print("SQL:", sql, "params:", params)
+ return await self.cursor.execute(sql, params)
+
+ async def executemany(self, sql: str, params: List[Tuple[Any, ...]]):
+ if self.engine.echo:
+ print("SQL:", sql, "params:", params)
+ return await self.cursor.executemany(sql, params)
+
+ async def commit(self):
+ if self.connection:
+ await self.connection.commit()
+
+ async def rollback(self):
+ if self.connection:
+ await self.connection.rollback()
+
+ async def add(self, instance: NexiosModel):
+ from nexios.orm.query.expressions import ColumnExpression
+ from nexios.orm.config import MySQLDialect, SQLiteDialect, PostgreSQLDialect
+
+ sql, params = self._ddl.upsert(instance)
+
+ primary_key = self._ddl._get_primary_key(instance)
+ if isinstance(primary_key, ColumnExpression):
+ primary_key_field = primary_key.field_name
+ else:
+ primary_key_field = primary_key
+
+ fields = instance.get_fields()
+ field_info = fields.get(primary_key_field)
+ auto_increment = getattr(field_info, 'auto_increment', False) if field_info else False
+ if auto_increment:
+ if isinstance(self._ddl.dialect, SQLiteDialect):
+ await self.execute(sql, params)
+ result = await (await self.execute("SELECT last_insert_rowid()")).fetchone()
+ last_id = result[0] if result else None
+ setattr(instance, primary_key_field, last_id)
+ elif isinstance(self._ddl.dialect, PostgreSQLDialect):
+ result = await (await self.execute(sql, params)).fetchone()
+ last_id = result[0] if result else None
+ setattr(instance, primary_key_field, last_id)
+ elif isinstance(self._ddl.dialect, MySQLDialect):
+ exec_stmt = await self.execute(sql, params)
+ last_id = getattr(exec_stmt, 'last_id', None)
+ setattr(instance, primary_key_field, last_id)
+ else:
+ await self.execute(sql, params)
+ else:
+ await self.execute(sql, params)
+
+ async def delete(self, model: _T):
+ sql, params = self._ddl.delete(model)
+ await self.execute(sql, params)
+
+ async def create_all(self, *models: Type[_T]):
+ try:
+ for nexiosmodel in models:
+ sql = self._ddl.create_table(nexiosmodel)
+ # Create tables
+ await self.execute(sql)
+ # Create indexes
+ index_sql = self._ddl.create_indexes(nexiosmodel)
+ for idx in index_sql:
+ await self.execute(idx)
+ await self.commit()
+ except Exception as e:
+ await self.rollback()
+ raise e
+
+ async def drop(self, model: Type[_T]):
+ sql = self._ddl.drop_table(model)
+ await self.execute(sql)
+
+ async def refresh(self, model: _T):
+ """
+ Refresh the given instance wit the current database state
+
+ Args:
+ model: Model instance to refresh
+ """
+ from nexios.orm.query.expressions import ColumnExpression
+ from nexios.orm.query.builder import select
+
+ primary_key = self._ddl._get_primary_key(model)
+ if isinstance(primary_key, ColumnExpression):
+ primary_key_field = primary_key.field_name
+ else:
+ primary_key_field = primary_key
+
+ model_class = type(model)
+ pk_value = getattr(model, primary_key_field, None)
+ if pk_value is None:
+ raise ValueError(f"Cannot refresh {model_class.__name__}:primary key is None")
+
+ query = select(model_class).where(getattr(model_class, primary_key_field) == pk_value)
+ refreshed = await self.exec(query).first()
+
+ if refreshed is None:
+ raise ValueError(f"{model_class.__name__} with {primary_key_field}={pk_value} no longer exists")
+
+ for field_name in model.get_fields().keys():
+ new_value = getattr(refreshed, field_name, None)
+ setattr(model, field_name, new_value)
diff --git a/nexios/orm/tests/__init__.py b/nexios/orm/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/nexios/orm/tests/aiosqlite_connection.py b/nexios/orm/tests/aiosqlite_connection.py
new file mode 100644
index 00000000..46b967d0
--- /dev/null
+++ b/nexios/orm/tests/aiosqlite_connection.py
@@ -0,0 +1,19 @@
+import asyncio
+
+import aiosqlite
+
+
+async def test_aiosqlite_connection() -> None:
+ conn = await aiosqlite.connect(":memory:")
+ cursor = await conn.cursor()
+ try:
+ # Test if connection exists
+ await cursor.execute("SELECT 1")
+ print("Connected")
+ except Exception as err:
+ print(f"Error occurred during test: {err}")
+ finally:
+ await conn.close()
+
+if __name__ == "__main__":
+ asyncio.run(test_aiosqlite_connection())
\ No newline at end of file
diff --git a/nexios/orm/tests/conftest.py b/nexios/orm/tests/conftest.py
new file mode 100644
index 00000000..44d280c3
--- /dev/null
+++ b/nexios/orm/tests/conftest.py
@@ -0,0 +1,150 @@
+from typing import Optional
+
+import pytest
+
+from nexios.orm.engine import create_engine, Engine
+from nexios.orm.model import NexiosModel
+from nexios.orm.sessions import Session, AsyncSession
+
+DATABASE_CONFIGS = {
+ 'postgres': {
+ 'host': 'localhost',
+ 'port': 5432,
+ 'user': 'vickram',
+ 'password': 'Vickram9038',
+ 'dbname': 'nexios'
+ },
+ 'mysql': {
+ 'host': 'localhost',
+ 'port': 3306,
+ 'user': 'vickram',
+ 'password': 'Vickram9038',
+ 'database': 'nexios'
+ },
+ 'sqlite': {
+ 'database': 'nexios.db'
+ }
+}
+
+@pytest.fixture(params=['postgres', 'sqlite', 'mysql'])
+def sync_session(request: pytest.FixtureRequest):
+
+ import sys
+ sys.stdout.flush()
+
+ engine: Optional[Engine] = None
+
+ from nexios.orm.config import PostgreSQLDialect, MySQLDialect
+
+ config = DATABASE_CONFIGS[request.param]
+
+ if request.param == 'postgres':
+ try:
+ # import psycopg
+ import asyncpg
+ engine = create_engine(echo=True, **config, use_pool=False)
+ except ImportError:
+ pytest.skip('psycopg not installed')
+ except Exception as e:
+ pytest.skip(str(e))
+ elif request.param == 'mysql':
+ try:
+ import pymysql # noqa
+ engine = create_engine(echo=True, **config, use_pool=False)
+ except ImportError:
+ pytest.skip("MySQL driver not installed")
+ except Exception as e:
+ pytest.skip(f"MySQL not available: {e}")
+ else: # sqlite
+ engine = create_engine(echo=True, **config)
+
+
+ with Session(engine) as sess:
+ for table in ('posts', 'addresses', 'profiles', 'users'):
+
+ dialect = engine.dialect
+
+ if isinstance(dialect, PostgreSQLDialect):
+ sess.execute(f"DROP TABLE IF EXISTS {table} CASCADE")
+ elif isinstance(dialect, MySQLDialect):
+ sess.execute("SET FOREIGN_KEY_CHECKS = 0")
+ sess.execute(f"DROP TABLE IF EXISTS {table}")
+ sess.execute(f"SET FOREIGN_KEY_CHECKS = 1")
+ else:
+ sess.execute("PRAGMA foreign_keys = 1")
+ sess.execute(f"DROP TABLE IF EXISTS {table}")
+ sess.execute("PRAGMA foreign_keys = 1")
+ sess.commit()
+
+ yield sess
+
+
+@pytest.fixture(params=['postgres', 'mysql', 'sqlite'])
+async def async_session(request):
+ """Create an async session for different databases"""
+ from nexios.orm.config import PostgreSQLDialect, MySQLDialect
+
+ engine: Optional[Engine] = None
+
+ config = DATABASE_CONFIGS[request.param]
+
+ if request.param == 'postgres':
+ try:
+ # import asyncpg
+ import psycopg
+ engine = create_engine(echo=True, **config, use_pool=False)
+ except ImportError:
+ pytest.skip("asyncpg not installed")
+ except Exception as e:
+ pytest.skip(f"PostgreSQL not available: {e}")
+ elif request.param == 'mysql':
+ try:
+ import aiomysql
+ engine = create_engine(echo=True, **config, use_pool=False)
+ except ImportError:
+ pytest.skip("AIOMySQL driver not installed")
+ except Exception as e:
+ pytest.skip(f"AIOMySQL not available: {e}")
+ else: # sqlite
+ try:
+ import aiosqlite
+ engine = create_engine(echo=True, **config, use_pool=False)
+ except ImportError:
+ pytest.skip("aiosqlite not installed")
+
+ async with AsyncSession(engine) as session:
+ # Clean up
+ for table in ('posts', 'addresses', 'profiles', 'users'):
+ dialect = engine.dialect
+
+ if isinstance(dialect, PostgreSQLDialect):
+ await session.execute(f"DROP TABLE IF EXISTS {table} CASCADE")
+ elif isinstance(dialect, MySQLDialect):
+ await session.execute("SET FOREIGN_KEY_CHECKS = 0")
+ await session.execute(f"DROP TABLE IF EXISTS {table}")
+ await session.execute(f"SET FOREIGN_KEY_CHECKS = 1")
+ else:
+ await session.execute("PRAGMA foreign_keys = 1")
+ await session.execute(f"DROP TABLE IF EXISTS {table}")
+ await session.execute("PRAGMA foreign_keys = 1")
+
+ await session.commit()
+
+ yield session
+
+class BaseTestModel(NexiosModel):
+ """Base model for testing with common functionality"""
+
+ @classmethod
+ def create_table(cls, session):
+ session.create_all([cls])
+
+ @classmethod
+ def drop_table(cls, session):
+ session.drop(cls)
+
+ @classmethod
+ def count(cls, session):
+ from nexios.orm.query.builder import select
+ query = select(cls)
+ return session.exec(query).count()
diff --git a/nexios/orm/tests/mysql_benchmark.py b/nexios/orm/tests/mysql_benchmark.py
new file mode 100644
index 00000000..2b94b587
--- /dev/null
+++ b/nexios/orm/tests/mysql_benchmark.py
@@ -0,0 +1,62 @@
+from nexios.orm.benchmark.mysql import MySQLConnectionPoolBenchmark
+
+def run_benchmarks():
+ """Run comprehensive benchmarks
+ - MySQL connector only supports a maximum of 32 connections,
+ Scenario from high concurrency will throw an exception
+ """
+
+ kwargs = {
+ 'host': 'localhost',
+ 'user': 'vickram',
+ 'password': 'Vickram9038',
+ 'database': 'test_db'
+ }
+
+ benchmark = MySQLConnectionPoolBenchmark(**kwargs)
+
+ # Define test scenarios
+ scenarios = {
+ 'low_concurrency': {
+ 'min_size': 2, 'max_size': 10, 'operations': 1000, 'concurrency': 5
+ },
+ 'high_concurrency': {
+ 'min_size': 5, 'max_size': 50, 'operations': 5000, 'concurrency': 50
+ },
+ 'burst_load': {
+ 'min_size': 2, 'max_size': 20, 'operations': 2000, 'concurrency': 100
+ },
+ 'sustained_load': {
+ 'min_size': 10, 'max_size': 30, 'operations': 10000, 'concurrency': 25
+ },
+ 'overload': {
+ 'min_size': 5, 'max_size': 15, 'operations': 5000, 'concurrency': 100
+ }
+ }
+
+ print("Starting Connection Pool Benchmark...")
+ print("This will compare mysql.connetor's built-in pool vs your custom pool")
+ print("=" * 60)
+
+ # Run comparisons
+ results = benchmark.compare_pools(scenarios)
+
+ # Generate report
+ report = benchmark.generate_report(results)
+ print("\n" + report)
+
+ # Plot results
+ benchmark.plot_results(results)
+
+ # Test under sustained load
+ print("\nTesting under sustained variable load...")
+ psycopg_metrics = benchmark.benchmark_under_load('mysql', duration=30)
+ custom_metrics = benchmark.benchmark_under_load('custom', duration=30)
+
+ print("\nSustained Load Metrics (mysql connector pool):", psycopg_metrics)
+ print("Sustained Load Metrics (custom pool):", custom_metrics)
+
+ return results
+
+if __name__ == "__main__":
+ results = run_benchmarks()
\ No newline at end of file
diff --git a/nexios/orm/tests/pool_running.py b/nexios/orm/tests/pool_running.py
new file mode 100644
index 00000000..ac10b23e
--- /dev/null
+++ b/nexios/orm/tests/pool_running.py
@@ -0,0 +1,46 @@
+from psycopg_pool import ConnectionPool as PsycopgPool
+import psycopg
+
+def quick_test():
+ """Quick test to verify both pools work"""
+ dsn = "postgresql://vickram:Vickram9038@localhost:5432/p_orm"
+
+ # Test psycopg3 pool
+ print("Testing psycopg3 pool...")
+ with PsycopgPool(dsn, min_size=2, max_size=5) as psy_pool:
+ with psy_pool.connection() as psy_conn:
+ with psy_conn.cursor() as psy_cur:
+ psy_cur.execute("SELECT 1")
+ result = psy_cur.fetchone()
+ print(f"psycopg3 pool works: {result}")
+
+ # Test custom pool
+ print("Testing custom pool...")
+ from nexios.orm.pool.base import PoolConfig
+ from nexios.orm.pool.connection_pool import ConnectionPool
+ from nexios.orm.dbapi.postgres.psycopg_ import PsycopgConnection
+
+ config = PoolConfig(min_size=2, max_size=5)
+
+ pool = ConnectionPool(
+ lambda: PsycopgConnection(psycopg.connect(dsn)),
+ config
+ )
+
+ import time
+ time.sleep(0.5)
+
+ print("Getting connection from custom pool...")
+ with pool.connection() as conn:
+ print("Got connection, creating cursor...")
+ cur = conn.cursor()
+ print("Executing query...")
+ cur.execute("SELECT 1")
+ result = cur.fetchone()
+ print(f"Custom pool works: {result}")
+
+ pool.close()
+ print("Both pools working correctly!")
+
+# Run quick test first
+quick_test()
\ No newline at end of file
diff --git a/nexios/orm/tests/postgres_benchmark.py b/nexios/orm/tests/postgres_benchmark.py
new file mode 100644
index 00000000..1451bde1
--- /dev/null
+++ b/nexios/orm/tests/postgres_benchmark.py
@@ -0,0 +1,54 @@
+from nexios.orm.benchmark.postgres import ConnectionPoolBenchmark
+
+def run_benchmarks():
+ """Run comprehensive benchmarks"""
+ dsn = "postgresql://vickram:Vickram9038@localhost:5432/p_orm"
+
+
+ benchmark = ConnectionPoolBenchmark(dsn)
+
+ # Define test scenarios
+ scenarios = {
+ 'low_concurrency': {
+ 'min_size': 2, 'max_size': 10, 'operations': 1000, 'concurrency': 5
+ },
+ 'high_concurrency': {
+ 'min_size': 5, 'max_size': 50, 'operations': 5000, 'concurrency': 50
+ },
+ 'burst_load': {
+ 'min_size': 2, 'max_size': 20, 'operations': 2000, 'concurrency': 100
+ },
+ 'sustained_load': {
+ 'min_size': 10, 'max_size': 30, 'operations': 10000, 'concurrency': 25
+ },
+ 'overload': {
+ 'min_size': 5, 'max_size': 15, 'operations': 5000, 'concurrency': 100
+ }
+ }
+
+ print("Starting Connection Pool Benchmark...")
+ print("This will compare psycopg3's built-in pool vs your custom pool")
+ print("=" * 60)
+
+ # Run comparisons
+ results = benchmark.compare_pools(scenarios)
+
+ # Generate report
+ report = benchmark.generate_report(results)
+ print("\n" + report)
+
+ # Plot results
+ benchmark.plot_results(results)
+
+ # Test under sustained load
+ print("\nTesting under sustained variable load...")
+ psycopg_metrics = benchmark.benchmark_under_load('psycopg3', duration=30)
+ custom_metrics = benchmark.benchmark_under_load('custom', duration=30)
+
+ print("\nSustained Load Metrics (psycopg3 pool):", psycopg_metrics)
+ print("Sustained Load Metrics (custom pool):", custom_metrics)
+
+ return results
+
+if __name__ == "__main__":
+ results = run_benchmarks()
\ No newline at end of file
diff --git a/nexios/orm/tests/run_tests.py b/nexios/orm/tests/run_tests.py
new file mode 100644
index 00000000..5328db71
--- /dev/null
+++ b/nexios/orm/tests/run_tests.py
@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+"""Run the test suite"""
+
+import sys
+import pytest
+from pathlib import Path
+
+# Add project root to path
+project_root = Path(__file__).parent.parent
+sys.path.insert(0, str(project_root))
+
+
+def main():
+ """Run pytest with custom arguments"""
+ # Run tests
+ args = [
+ "-v", # Verbose
+ "--tb=short", # Short traceback
+ "-x", # Stop on first failure
+ # "--cov=nexios", # Coverage (if pytest-cov installed)
+ # "--cov-report=html", # HTML coverage report
+ "nexios/orm/tests"
+ ]
+
+ # Add specific test file/class if provided
+ if len(sys.argv) > 1:
+ args.extend(sys.argv[1:])
+
+ return pytest.main(args)
+
+
+if __name__ == "__main__":
+ sys.exit(main())
\ No newline at end of file
diff --git a/nexios/orm/tests/test_advanced_queries.py b/nexios/orm/tests/test_advanced_queries.py
new file mode 100644
index 00000000..552d2814
--- /dev/null
+++ b/nexios/orm/tests/test_advanced_queries.py
@@ -0,0 +1,234 @@
+from datetime import datetime, timedelta
+
+import pytest
+
+from nexios.orm.query.builder import select
+from .test_models import User, Post
+
+
+class TestAdvancedQueries:
+ """Test advanced query operations"""
+ def test_join_operations(self, sync_session):
+ """Test JOIN operations"""
+
+ sync_session.create_all(User, Post)
+
+ # Create test data
+ users = [
+ User(username="joinuser1", email="join1@example.com", password_hash="hash"),
+ User(username="joinuser2", email="join2@example.com", password_hash="hash"),
+ ]
+
+ for user in users:
+ sync_session.add(user)
+ sync_session.commit()
+
+ posts = [
+ Post(user_id=users[0].id, title="User1 Post 1", content="Content"),
+ Post(user_id=users[0].id, title="User1 Post 2", content="Content"),
+ Post(user_id=users[1].id, title="User2 Post 1", content="Content")
+ ]
+
+ for post in posts:
+ sync_session.add(post)
+ sync_session.commit()
+
+ # Test INNER JOIN
+ query = (
+ select(User, Post)
+ .inner_join(Post, Post.user_id == User.id)
+ .where(User.username == "joinuser1")
+ )
+ results = sync_session.exec(query).all()
+
+ print(f"Results in test join==========: {results}")
+
+ # Should get 2 results (user1 has 2 posts)
+ assert len(results) == 2
+ for user, post in results:
+ assert user.username == "joinuser1"
+ assert post.user_id == user.id
+
+ # Test LEFT JOIN
+ query = (
+ select(User, Post)
+ .left_join(Post, Post.user_id == User.id)
+ .order_by(User.username, Post.title)
+ )
+ results = sync_session.exec(query).all()
+
+ # Should get 3 results (user1: 2 posts, user2: 1 post)
+ assert len(results) == 3
+
+ def test_group_by_and_having(self, sync_session):
+ """Test GROUP BY and HAVING clauses"""
+
+ sync_session.create_all(User, Post)
+
+ # Create test data
+ users = [
+ User(username="groupuser1", email="g1@example.com", password_hash="hash"),
+ User(username="groupuser2", email="g2@example.com", password_hash="hash"),
+ User(username="groupuser3", email="g3@example.com", password_hash="hash"),
+ ]
+
+ for user in users:
+ sync_session.add(user)
+ sync_session.commit()
+
+ # User1: 3 posts, User2: 2 posts, User3: 0 posts
+ for i in range(3):
+ post = Post(user_id=users[0].id, title=f"Post {i}", content="Content")
+ sync_session.add(post)
+
+ for i in range(2):
+ post = Post(user_id=users[1].id, title=f"Post {i}", content="Content")
+ sync_session.add(post)
+
+ sync_session.commit()
+
+ # Test GROUP BY with COUNT
+ # This depends on your GROUP BY implementation
+ # Example:
+ query = (
+ select(User.username, "COUNT(*) AS post_count")
+ .left_join(Post, Post.user_id == User.id)
+ .group_by(User.username)
+ )
+ results = sync_session.exec(query).all()
+
+ assert len(results) == 3
+ # Sort by username for consistent ordering
+ results_sorted = sorted(results, key=lambda x: x[0])
+
+ assert results_sorted[0][0] == "groupuser1"
+ assert results_sorted[0][1] == 3 # 3 posts
+
+ assert results_sorted[1][0] == "groupuser2"
+ assert results_sorted[1][1] == 2 # 2 posts
+
+ assert results_sorted[2][0] == "groupuser3"
+ # assert results_sorted[2][1] == 0 # 0 posts
+
+ def test_subqueries(self, sync_session):
+ """Test subquery operations"""
+
+ sync_session.create_all(User, Post)
+
+ # Create test data
+ active_user = User(
+ username="activeuser",
+ email="active@example.com",
+ password_hash="hash",
+ is_active=True
+ )
+
+ inactive_user = User(
+ username="inactiveuser",
+ email="inactive@example.com",
+ password_hash="hash",
+ is_active=False
+ )
+
+ sync_session.add(active_user)
+ sync_session.add(inactive_user)
+ sync_session.commit()
+
+ # Create posts for active user
+ for i in range(3):
+ post = Post(user_id=active_user.id, title=f"Active Post {i}", content="Content")
+ sync_session.add(post)
+
+ sync_session.commit()
+
+ # Test subquery in WHERE
+ # Get users who have posts
+ subquery = select(Post.user_id).distinct()
+ query = select(User).where(User.id.in_(subquery))
+ results = sync_session.exec(query).all()
+
+ assert len(results) == 1
+ assert results[0].username == "activeuser"
+
+ def test_transactions(self, sync_session):
+ """Test transaction support"""
+
+ sync_session.create_all(User)
+
+ # Test successful transaction
+ user1 = User(
+ username="txuser1",
+ email="tx1@example.com",
+ password_hash="hash"
+ )
+
+ sync_session.add(user1)
+ sync_session.commit()
+
+ # Verify committed
+ query = select(User).where(User.username == "txuser1")
+ assert sync_session.exec(query).first() is not None
+
+ # Test rollback
+ try:
+ user2 = User(
+ username="txuser2",
+ email="tx2@example.com",
+ password_hash="hash"
+ )
+ sync_session.add(user2)
+ # Intentionally cause an error
+ sync_session.execute("INVALID SQL")
+ sync_session.commit()
+ except Exception:
+ sync_session.rollback()
+
+ # User2 should not exist
+ query = select(User).where(User.username == "txuser2")
+ assert sync_session.exec(query).first() is None
+
+ def test_datetime_operations(self, sync_session):
+ """Test datetime field operations"""
+
+ sync_session.create_all(User)
+
+ now = datetime.now().replace(microsecond=0)
+
+ users = [
+ User(
+ username="timeuser1",
+ email="time1@example.com",
+ password_hash="hash",
+ created_at=now - timedelta(days=2)
+ ),
+ User(
+ username="timeuser2",
+ email="time2@example.com",
+ password_hash="hash",
+ created_at=now - timedelta(days=1)
+ ),
+ User(
+ username="timeuser3",
+ email="time3@example.com",
+ password_hash="hash",
+ created_at=now
+ ),
+ ]
+
+ for user in users:
+ sync_session.add(user)
+ sync_session.commit()
+
+ # Test ordering by datetime
+ query = select(User).order_by(User.created_at)
+ results = sync_session.exec(query).all()
+
+ assert [u.username for u in results] == ["timeuser1", "timeuser2", "timeuser3"]
+
+ # Test filtering by datetime (if supported)
+ yesterday = now - timedelta(days=1)
+ query = select(User).where(User.created_at > yesterday)
+ # query._bind(sync_session)
+ results = sync_session.exec(query).all()
+ assert len(results) == 1
+ assert results[0].username == "timeuser3"
\ No newline at end of file
diff --git a/nexios/orm/tests/test_async.py b/nexios/orm/tests/test_async.py
new file mode 100644
index 00000000..cbde58a9
--- /dev/null
+++ b/nexios/orm/tests/test_async.py
@@ -0,0 +1,134 @@
+import pytest
+from .test_models import User, Post, Address, Profile
+from nexios.orm.query.builder import select
+
+
+class TestAsyncOperations:
+ """Test async operations"""
+
+ @pytest.mark.asyncio
+ async def test_async_create_table(self, async_session):
+ """Test async table creation"""
+ await async_session.create_all(User, Post, Profile, Address)
+
+ # Verify by inserting data
+ user = User(
+ username="asyncuser",
+ email="async@example.com",
+ password_hash="hash"
+ )
+
+ await async_session.add(user)
+ await async_session.commit()
+
+ assert user.id is not None
+
+ @pytest.mark.asyncio
+ async def test_async_insert_and_query(self, async_session):
+ """Test async insert and query"""
+ await async_session.create_all(User)
+
+ # Insert multiple users
+ for i in range(3):
+ user = User(
+ username=f"asyncuser{i}",
+ email=f"async{i}@example.com",
+ password_hash="hash"
+ )
+ await async_session.add(user)
+
+ await async_session.commit()
+
+ query_all = select(User)
+ all_users = await async_session.exec(query_all).all()
+
+ # Query asynchronously
+ query = select(User).where(User.username.like("asyncuser%"))
+ users = await async_session.exec(query).all()
+
+ assert len(users) == 3
+
+ # Test async first
+ query = select(User).where(User.username == "asyncuser0")
+ user = await async_session.exec(query).first()
+
+ assert user is not None
+ assert user.username == "asyncuser0"
+
+ @pytest.mark.asyncio
+ async def test_async_count(self, async_session):
+ """Test async count operation"""
+ await async_session.create_all(User)
+
+ # Insert data
+ for i in range(5):
+ user = User(
+ username=f"countuser{i}",
+ email=f"count{i}@example.com",
+ password_hash="hash"
+ )
+ await async_session.add(user)
+
+ await async_session.commit()
+
+ # Test async count
+ query = select(User)
+ count = await async_session.exec(query).count()
+
+ assert count == 5
+
+ @pytest.mark.asyncio
+ async def test_async_exists(self, async_session):
+ """Test async exists operation"""
+ await async_session.create_all(User)
+
+ # Empty table
+ query = select(User).where(User.username == "nonexistent")
+ exists = await async_session.exec(query).exists()
+ assert exists is False
+
+ # Insert user
+ user = User(
+ username="existuser",
+ email="exist@example.com",
+ password_hash="hash"
+ )
+ await async_session.add(user)
+ await async_session.commit()
+
+ # Should exist now
+ query = select(User).where(User.username == "existuser")
+ exists = await async_session.exec(query).exists()
+ assert exists is True
+
+ @pytest.mark.asyncio
+ async def test_async_relationships(self, async_session):
+ """Test async relationship loading"""
+ await async_session.create_all(User, Post)
+
+ # Create user with posts
+ user = User(
+ username="reluser",
+ email="rel@example.com",
+ password_hash="hash"
+ )
+ await async_session.add(user)
+ await async_session.commit()
+
+ post1 = Post(user_id=user.id, title="Async Post 1", content="Content 1")
+ post2 = Post(user_id=user.id, title="Async Post 2", content="Content 2")
+
+ await async_session.add(post1)
+ await async_session.add(post2)
+ await async_session.commit()
+
+ # Query user with eager loading
+ # query = select(User).where(User.username == "reluser").eager_load("posts")
+ query = select(User).where(User.username == "reluser").eager_load("posts")
+ fetched_user = await async_session.exec(query).first()
+ fetched_posts = await async_session.exec(fetched_user.posts).all()
+ # fetched_posts = fetched_user.posts
+ print(f"Fetched posts======================={fetched_posts}")
+
+ # Posts should be loaded
+ assert len(fetched_posts) == 2
\ No newline at end of file
diff --git a/nexios/orm/tests/test_crud.py b/nexios/orm/tests/test_crud.py
new file mode 100644
index 00000000..aaa1dfa6
--- /dev/null
+++ b/nexios/orm/tests/test_crud.py
@@ -0,0 +1,285 @@
+import pytest
+
+from nexios.orm.config import PostgreSQLDialect
+from nexios.orm.tests.test_models import User, Profile, Address, Post
+
+class TestCRUDOperations:
+ """Test basic create, read, update, delete operations."""
+
+ def test_create_table(self, sync_session):
+ sync_session.create_all(User, Profile, Address, Post)
+
+ # Verify
+ if isinstance(sync_session.engine.dialect, PostgreSQLDialect):
+ results = sync_session.execute(
+ "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'"
+ ).fetchall()
+ tables = [row[0] for row in results]
+ assert 'users' in tables
+ assert 'profiles' in tables
+ assert 'addresses' in tables
+ assert 'posts' in tables
+
+ def test_insert_user(self, sync_session):
+ """Test inserting a single user"""
+ # Create tables
+ from nexios.orm.query.builder import select
+
+ sync_session.create_all(User)
+
+ # Create user
+ user = User(
+ username="testuser",
+ email="test@example.com",
+ password_hash="hashed_password"
+ )
+
+ # Insert
+ sync_session.add(user)
+ sync_session.commit()
+
+ # Verify insertion
+ assert user.id is not None
+ assert user.created_at is not None
+
+ # Query back
+ query = select(User).where(User.username == "testuser")
+ fetched_user = sync_session.exec(query).first()
+
+ print(f"Fetched user in insert test function: {fetched_user}")
+
+ assert fetched_user is not None
+ assert fetched_user.id == user.id
+ assert fetched_user.username == "testuser"
+ assert fetched_user.email == "test@example.com"
+
+ def test_insert_multiple_users(self, sync_session):
+ """Test inserting multiple users"""
+ from nexios.orm.query.builder import select
+
+ sync_session.create_all(User)
+
+ users = [
+ User(username=f"user{i}", email=f"user{i}@example.com", password_hash="hash")
+ for i in range(5)
+ ]
+
+ for user in users:
+ sync_session.add(user)
+ sync_session.commit()
+
+ # Verify all inserted
+ assert User.count(sync_session) == 5
+
+ # Query with limit
+ query = select(User).limit(3)
+ results = sync_session.exec(query).all()
+ assert len(results) == 3
+
+ def test_update_user(self, sync_session):
+ """Test updating a user"""
+ from nexios.orm.query.builder import select
+
+ sync_session.create_all(User)
+
+ # Create user
+ user = User(
+ username="original",
+ email="original@example.com",
+ password_hash="hash"
+ )
+ sync_session.add(user)
+ sync_session.commit()
+
+ # Update
+ user.username = "updated"
+ user.email = "updated@example.com"
+ sync_session.add(user) # Should trigger update
+ sync_session.commit()
+
+ # Verify update
+ query = select(User).where(User.id == user.id)
+ updated_user = sync_session.exec(query).first()
+
+ assert updated_user.username == "updated"
+ assert updated_user.email == "updated@example.com"
+
+ def test_delete_user(self, sync_session):
+ """Test deleting a user"""
+ from nexios.orm.query.builder import select
+
+ sync_session.create_all(User)
+
+ # Create users
+ user1 = User(username="user1", email="user1@example.com", password_hash="hash")
+ user2 = User(username="user2", email="user2@example.com", password_hash="hash")
+
+ sync_session.add(user1)
+ sync_session.add(user2)
+ sync_session.commit()
+
+ # Delete one user
+ sync_session.delete(user1)
+ sync_session.commit()
+
+ # Verify deletion
+ assert User.count(sync_session) == 1
+
+ query = select(User).where(User.username == "user1")
+ deleted_user = sync_session.exec(query).first()
+ assert deleted_user is None
+
+ # Other user should still exist
+ query = select(User).where(User.username == "user2")
+ existing_user = sync_session.exec(query).first()
+ assert existing_user is not None
+
+ def test_where_clause(self, sync_session):
+ """Test WHERE clause with various conditions"""
+ from nexios.orm.query.builder import select
+
+ sync_session.create_all(User)
+
+ users = [
+ User(username="alice", email="alice@example.com", password_hash="hash", is_active=True),
+ User(username="bob", email="bob@example.com", password_hash="hash", is_active=False),
+ User(username="charlie", email="charlie@example.com", password_hash="hash", is_active=True),
+ ]
+
+ for user in users:
+ sync_session.add(user)
+ sync_session.commit()
+
+ # Test equality
+ query = select(User).where(User.username == "alice")
+ result = sync_session.exec(query).first()
+ assert result.username == "alice"
+
+ # Test inequality
+ query = select(User).where(User.username != "alice")
+ results = sync_session.exec(query).all()
+ assert len(results) == 2
+
+ # Test boolean
+ query = select(User).where(User.is_active == True)
+ results = sync_session.exec(query).all()
+ assert len(results) == 2
+
+ # Test multiple conditions
+ query = select(User).where(
+ (User.is_active == True) & (User.username != "charlie")
+ )
+ results = sync_session.exec(query).all()
+ assert len(results) == 1
+ assert results[0].username == "alice"
+
+ def test_order_by(self, sync_session):
+ """Test ORDER BY clause"""
+ from nexios.orm.query.builder import select
+
+ sync_session.create_all(User)
+
+ users = [
+ User(username="zack", email="z@example.com", password_hash="hash"),
+ User(username="alice", email="a@example.com", password_hash="hash"),
+ User(username="bob", email="b@example.com", password_hash="hash"),
+ ]
+
+ for user in users:
+ sync_session.add(user)
+ sync_session.commit()
+
+ # Ascending order
+ query = select(User).order_by(User.username)
+ results = sync_session.exec(query).all()
+ assert [u.username for u in results] == ["alice", "bob", "zack"]
+
+ # Descending order
+ query = select(User).order_by(User.username.desc())
+ results = sync_session.exec(query).all()
+ assert [u.username for u in results] == ["zack", "bob", "alice"]
+
+ def test_limit_offset(self, sync_session):
+ """Test LIMIT and OFFSET"""
+ from nexios.orm.query.builder import select
+
+ sync_session.create_all(User)
+
+ for i in range(10):
+ user = User(
+ username=f"user{i}",
+ email=f"user{i}@example.com",
+ password_hash="hash"
+ )
+ sync_session.add(user)
+ sync_session.commit()
+
+ # Test limit
+ query = select(User).limit(3)
+ results = sync_session.exec(query).all()
+ assert len(results) == 3
+
+ # Test offset
+ query = select(User).offset(5)
+ results = sync_session.exec(query).all()
+ assert len(results) == 5
+
+ # Test limit + offset
+ query = select(User).limit(2).offset(3)
+ results = sync_session.exec(query).all()
+ assert len(results) == 2
+ assert results[0].username == "user3"
+ assert results[1].username == "user4"
+
+ def test_count(self, sync_session):
+ """Test COUNT operation"""
+ from nexios.orm.query.builder import select
+
+ sync_session.create_all(User)
+
+ # Insert 5 users
+ for i in range(5):
+ user = User(
+ username=f"countuser{i}",
+ email=f"count{i}@example.com",
+ password_hash="hash"
+ )
+ sync_session.add(user)
+ sync_session.commit()
+
+ # Test total count
+ query = select(User)
+ count = sync_session.exec(query).count()
+ assert count == 5
+
+ # Test filtered count
+ query = select(User).where(User.username.like("countuser%"))
+ count = sync_session.exec(query).count()
+ assert count == 5
+
+ # Test count with condition
+ query = select(User).where(User.username == "countuser0")
+ count = sync_session.exec(query).count()
+ assert count == 1
+
+ def test_exists(self, sync_session):
+ """Test EXISTS operation"""
+ from nexios.orm.query.builder import select
+
+ sync_session.create_all(User)
+
+ # Empty table
+ query = select(User).where(User.username == "nonexistent")
+ # print(f"User is======: {sync_session.exec(query).one().username}")
+ exists = sync_session.exec(query).exists()
+ print(f"User exists in database: {exists}")
+ assert exists is False
+
+ # Insert user
+ user = User(username="existsuser", email="exists@example.com", password_hash="hash")
+ sync_session.add(user)
+ sync_session.commit()
+
+ # Should exist now
+ query = select(User).where(User.username == "existsuser")
+ assert sync_session.exec(query).exists() is True
\ No newline at end of file
diff --git a/nexios/orm/tests/test_models.py b/nexios/orm/tests/test_models.py
new file mode 100644
index 00000000..319fc2f4
--- /dev/null
+++ b/nexios/orm/tests/test_models.py
@@ -0,0 +1,71 @@
+from datetime import datetime
+from typing import Optional, List
+
+from nexios.orm.relationships import Relationship
+from nexios.orm.fields import Field
+from nexios.orm.tests.conftest import BaseTestModel
+
+
+class Profile(BaseTestModel):
+ __tablename__ = 'profiles'
+
+ id: Optional[int] = Field(primary_key=True, auto_increment=True, default=None)
+ user_id: int = Field(foreign_key="User.id", unique=True)
+ bio: Optional[str] = Field(nullable=True, max_length=500)
+ website: Optional[str] = Field(nullable=True)
+ created_at: datetime = Field(default_factory=datetime.now)
+
+ user: "User" = Relationship(back_populates="profile")
+
+
+class Address(BaseTestModel):
+ __tablename__ = "addresses"
+
+ id: Optional[int] = Field(primary_key=True, auto_increment=True, default=None)
+ user_id: int = Field(foreign_key="User.id")
+ street: str = Field(max_length=200)
+ city: str = Field(max_length=100)
+ country: str = Field(max_length=100)
+ zip_code: str = Field(max_length=20)
+
+ user: "User" = Relationship(back_populates="addresses")
+
+
+class Post(BaseTestModel):
+ __tablename__ = "posts"
+
+ id: Optional[int] = Field(primary_key=True, auto_increment=True, default=None)
+ user_id: int = Field(foreign_key="User.id")
+ title: str = Field(max_length=200)
+ content: str
+ published: bool = Field(default=False)
+ created_at: datetime = Field(default_factory=datetime.now)
+
+ user: "User" = Relationship(back_populates="posts")
+
+
+class User(BaseTestModel):
+ __tablename__ = "users"
+
+ id: Optional[int] = Field(primary_key=True, auto_increment=True, default=None)
+ username: str = Field(max_length=50, unique=True, index=True)
+ email: str = Field(max_length=100, unique=True)
+ password_hash: str = Field(max_length=255)
+ is_active: bool = Field(default=True)
+ created_at: datetime = Field(default_factory=datetime.now)
+
+ # Relationships
+ profile: Optional[Profile] = Relationship(
+ back_populates="user",
+ lazy="select"
+ )
+
+ addresses: List[Address] = Relationship(
+ back_populates="user",
+ lazy="select"
+ )
+
+ posts: List[Post] = Relationship(
+ back_populates="user",
+ lazy="dynamic" # Test dynamic loading
+ )
diff --git a/nexios/orm/tests/test_relationships.py b/nexios/orm/tests/test_relationships.py
new file mode 100644
index 00000000..f185ff45
--- /dev/null
+++ b/nexios/orm/tests/test_relationships.py
@@ -0,0 +1,256 @@
+import pytest
+from .test_models import User, Profile, Address, Post
+from nexios.orm.query.builder import select
+
+
+class TestRelationshipOperations:
+ """Test relationship loading and operations"""
+
+ def test_one_to_one_relationship(self, sync_session):
+ """Test one-to-one relationship (User <-> Profile)"""
+ sync_session.create_all(User, Profile)
+
+ # Create user with profile
+ user = User(
+ username="profileuser",
+ email="profile@example.com",
+ password_hash="hash"
+ )
+
+ sync_session.add(user)
+ sync_session.commit()
+
+ profile = Profile(
+ user_id=user.id,
+ bio="Test bio",
+ website="https://example.com"
+ )
+
+ sync_session.add(profile)
+ sync_session.commit()
+
+ print(f"User profile: {user.profile}")
+
+ # Test forward relationship (User -> Profile)
+ assert user.profile is not None # fails, profile is None
+ assert user.profile.bio == "Test bio"
+ assert user.profile.website == "https://example.com"
+
+ # Test backward relationship (Profile -> User)
+ assert profile.user is not None
+ assert profile.user.username == "profileuser"
+ assert profile.user.email == "profile@example.com"
+
+ # Test foreign key is set
+ assert profile.user_id == user.id
+
+ def test_one_to_many_relationship(self, sync_session):
+ """Test one-to-many relationship (User <-> Address)"""
+ sync_session.create_all(User, Address, Post)
+
+ # Create user with multiple addresses
+ user = User(
+ username="addressuser",
+ email="address@example.com",
+ password_hash="hash"
+ )
+ sync_session.add(user)
+ sync_session.commit()
+
+ addresses = [
+ Address(user_id=user.id, street="123 Main St", city="City1", country="Country1", zip_code="12345"),
+ Address(user_id=user.id, street="456 Oak Ave", city="City2", country="Country2", zip_code="67890"),
+ Address(user_id=user.id, street="789 Pine Rd", city="City3", country="Country3", zip_code="11223"),
+ ]
+
+ for addr in addresses:
+ sync_session.add(addr)
+ sync_session.commit()
+
+ posts = [
+ Post(user_id=user.id, title="Post 1", content="Content 1"),
+ Post(user_id=user.id, title="Post 2", content="Content 2"),
+ ]
+
+ for post in posts:
+ sync_session.add(post)
+ sync_session.commit()
+
+ print(f"all addresses: {sync_session.exec(select(Address)).all()}") # This works
+
+ print(f"User addresses: {user.addresses}")
+
+ print(f"User posts: {user.posts}")
+
+ # Test forward relationship (User -> Addresses)
+ assert len(user.addresses) == 3
+ assert {addr.street for addr in user.addresses} == {
+ "123 Main St", "456 Oak Ave", "789 Pine Rd"
+ }
+
+ # Test backward relationship (Address -> User)
+ for addr in addresses:
+ assert addr.user is not None
+ assert addr.user.username == "addressuser"
+ assert addr.user_id == user.id
+
+ # Test querying through relationship
+ address_query = select(Address).where(Address.user_id == user.id)
+ user_addresses = sync_session.exec(address_query).all()
+ assert len(user_addresses) == 3
+
+ def test_many_to_one_relationship(self, sync_session):
+ """Test many-to-one relationship (Post -> User)"""
+ sync_session.create_all(User, Post)
+
+ user = User(
+ username="postuser",
+ email="post@example.com",
+ password_hash="hash"
+ )
+ sync_session.add(user)
+ sync_session.commit()
+
+ posts = [
+ Post(user_id=user.id, title="Post 1", content="Content 1"),
+ Post(user_id=user.id, title="Post 2", content="Content 2"),
+ Post(user_id=user.id, title="Post 3", content="Content 3"),
+ ]
+
+ for post in posts:
+ sync_session.add(post)
+ sync_session.commit()
+
+ # Test forward relationship (Post -> User)
+ for post in posts:
+ assert post.user is not None
+ assert post.user.username == "postuser"
+ assert post.user_id == user.id
+
+ # Test backward relationship (User -> Posts) with dynamic loading
+ posts_query = user.posts # This should return a query object
+
+ # Apply filters to the dynamic query
+ posts_query = posts_query.where(Post.title.like("Post %"))
+ user_posts = sync_session.exec(posts_query).all()
+
+ assert len(user_posts) == 3
+ assert {post.title for post in user_posts} == {"Post 1", "Post 2", "Post 3"}
+
+ def test_eager_loading(self, sync_session):
+ """Test eager loading of relationships"""
+ sync_session.create_all(User, Profile, Address, Post)
+
+ # Create test data
+ user = User(
+ username="eageruser",
+ email="eager@example.com",
+ password_hash="hash"
+ )
+ sync_session.add(user)
+ sync_session.commit()
+
+ profile = Profile(user_id=user.id, bio="Eager bio", website="https://example.com")
+ address1 = Address(user_id=user.id, street="123 St", city="City", country="Country", zip_code="12345")
+ address2 = Address(user_id=user.id, street="456 St", city="City", country="Country", zip_code="67890")
+ post = Post(user_id=user.id, title="Eager Post", content="Content")
+
+ sync_session.add(profile)
+ sync_session.add(address1)
+ sync_session.add(address2)
+ sync_session.add(post)
+ sync_session.commit()
+
+ # Test eager loading with .eager_load()
+ query = select(User).where(User.username == "eageruser").eager_load("profile", "addresses")
+ eager_user = sync_session.exec(query).first()
+
+ # Profile and addresses should be loaded
+ assert eager_user.profile is not None # fails, profile is None
+ assert eager_user.profile.bio == "Eager bio"
+ assert len(eager_user.addresses) == 2
+
+ def test_lazy_loading_strategies(self, sync_session):
+ """Test different lazy loading strategies"""
+ sync_session.create_all(User, Post)
+
+ user = User(
+ username="lazyuser",
+ email="lazy@example.com",
+ password_hash="hash"
+ )
+
+ sync_session.add(user)
+ sync_session.commit()
+
+ # Test dynamic lazy loading
+ posts_query = user.posts # Returns query, not results
+ assert hasattr(posts_query, 'where') # Should be a query object
+
+ # Test select lazy loading (default)
+ # When we access the relationship, it should load
+ new_posts = Post(user_id=user.id, title="Lazy Post", content="Content")
+ sync_session.add(new_posts)
+ sync_session.commit()
+
+ # The posts should be accessible
+ assert len(sync_session.exec(user.posts).all()) == 1 # all() on the query
+
+ def test_cascade_operations(self, sync_session):
+ """Test cascade operations (if implemented)"""
+ sync_session.create_all(User, Profile)
+
+ user = User(
+ username="cascadeuser",
+ email="cascade@example.com",
+ password_hash="hash"
+ )
+ sync_session.add(user)
+ sync_session.commit()
+
+ profile = Profile(user_id=user.id, bio="Cascade bio", website="https://example.com")
+
+ sync_session.add(profile)
+ sync_session.commit()
+
+ # Test that profile has user_id set
+ assert profile.user_id == user.id
+
+ # If cascade delete is implemented:
+ # sync_session.delete(user)
+ # sync_session.commit()
+ #
+ # # Profile should also be deleted
+ # query = select(Profile).where(Profile.id == profile.id)
+ # query._bind(sync_session)
+ # assert query._first() is None
+
+ def test_relationship_setter(self, sync_session):
+ """Test setting relationships"""
+ sync_session.create_all(User, Profile)
+
+ user = User(
+ username="setteruser",
+ email="setter@example.com",
+ password_hash="hash"
+ )
+
+ sync_session.add(user)
+ sync_session.commit()
+
+ profile = Profile(
+ user_id=user.id or 1,
+ bio="Setter bio",
+ website="https://example.com"
+ )
+
+ sync_session.add(profile)
+ sync_session.commit()
+
+ # Set relationship
+ user.profile = profile
+ # Profile should automatically get user reference
+ # assert profile.user is user
+
+ # Verify foreign key is set
+ assert profile.user_id == user.id
\ No newline at end of file
diff --git a/nexios/orm/utils.py b/nexios/orm/utils.py
new file mode 100644
index 00000000..25488261
--- /dev/null
+++ b/nexios/orm/utils.py
@@ -0,0 +1,80 @@
+from __future__ import annotations
+import re
+from typing import Literal, Optional, TypeVar, Any, Type, Union, Dict
+from packaging import version
+from pydantic import ConfigDict, BaseModel as PydanticBaseModel
+from pydantic.fields import FieldInfo as PydanticFieldInfo
+import pydantic
+
+
+_TNexiosModel = TypeVar("_TNexiosModel", bound=Any)
+
+PYDANTIC_VERSION = version.parse(pydantic.__version__)
+
+IS_PYDANTIC_V2 = PYDANTIC_VERSION.major >= 2
+
+OnDeleteOrUpdate = Literal[
+ "CASCADE", "SET NULL", "SET DEFAULT", "RESTRICT", "NO ACTION"
+]
+LazyOp = Literal["select", "joined", "subquery", "dynamic"]
+T = TypeVar("T")
+InstanceOrType = Union[T, Type[T]]
+
+def to_snake_case(name: str) -> str:
+ """Converts CamelCase to snake_case."""
+ s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
+ return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
+
+class NexiosModelConfig(ConfigDict, total=False):
+ table: Optional[bool]
+
+
+def get_config_value(
+ *, model: InstanceOrType[_TNexiosModel], parameter: str, default: Any = None
+) -> Any:
+ if IS_PYDANTIC_V2:
+ return model.model_config.get(parameter, default)
+ else:
+ return getattr(model.__config__, parameter, default)
+
+
+def set_config_value(
+ *, model: InstanceOrType[_TNexiosModel], parameter: str, value: Any
+) -> None:
+ if IS_PYDANTIC_V2:
+ model.model_config[parameter] = value
+ else:
+ setattr(model.__config__, parameter, value)
+
+
+def get_tablename_for_class(cls: Any) -> Optional[str]:
+ if hasattr(cls, "__tablename__") and cls.__tablename__ is not None:
+ return cls.__tablename__
+
+ table = get_config_value(model=cls, parameter="table", default=False)
+ if table is True and (hasattr(cls, "model_config") or hasattr(cls, '__config__')):
+ tablename = (
+ f"{cls.__name__.lower()}" if cls.__name__.lower().endswith('s')
+ else f"{cls.__name__.lower()}s"
+ )
+ return tablename
+
+ return None
+
+def get_model_fields(
+ model: InstanceOrType[PydanticBaseModel],
+) -> Dict[str, PydanticFieldInfo]:
+ if IS_PYDANTIC_V2:
+ return model.model_fields # type: ignore
+ else:
+ return model.__fields__ # type: ignore
+
+
+def get_tablename(model_class: InstanceOrType[Any]) -> str:
+ if not isinstance(model_class, type):
+ model_class = model_class.__class__
+
+ tablename = getattr(model_class, "__tablename__", None)
+ if tablename is None and hasattr(model_class, "__tablename__"):
+ tablename = model_class.__tablename__
+ return tablename if tablename else ""
\ No newline at end of file
diff --git a/nexios/utils/concurrency.py b/nexios/utils/concurrency.py
index 815171e5..22b8c6c7 100644
--- a/nexios/utils/concurrency.py
+++ b/nexios/utils/concurrency.py
@@ -15,6 +15,7 @@
Set,
TypeVar,
)
+task_group = asyncio.TaskGroup
import anyio
diff --git a/pool_comparison.png b/pool_comparison.png
new file mode 100644
index 00000000..b0999b05
Binary files /dev/null and b/pool_comparison.png differ
diff --git a/pyproject.toml b/pyproject.toml
index f6c36f18..040f452d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -40,6 +40,22 @@ dependencies = [
"python-multipart>=0.0.6",
"pydantic>=2.0,<3.0",
"typing-extensions>=4.12.2; python_version<'3.10'",
+ "aiosqlite>=0.21.0",
+ "asyncpg>=0.30.0",
+ "psycopg>=3.2.9",
+ "pg8000>=1.31.5",
+ "mysql-connector-python>=9.4.0",
+ "pymysql>=1.1.1",
+ "aiomysql>=0.2.0",
+ "psycopg-pool>=3.2.7",
+ "psutil>=7.1.3",
+ "uvloop>=0.22.1",
+ "nest-asyncio>=1.6.0",
+ "aiopg>=1.4.0",
+ "mysqlclient>=2.2.7",
+ "asyncmy>=0.2.10",
+ "apsw>=3.51.0.0",
+ "mariadb>=1.1.14",
]
diff --git a/uv.lock b/uv.lock
index 1f41dfd0..290d6b47 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2,6 +2,43 @@ version = 1
revision = 3
requires-python = ">=3.10"
+[[package]]
+name = "aiomysql"
+version = "0.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pymysql" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/67/76/2c5b55e4406a1957ffdfd933a94c2517455291c97d2b81cec6813754791a/aiomysql-0.2.0.tar.gz", hash = "sha256:558b9c26d580d08b8c5fd1be23c5231ce3aeff2dadad989540fee740253deb67", size = 114706, upload-time = "2023-06-11T19:57:53.608Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/42/87/c982ee8b333c85b8ae16306387d703a1fcdfc81a2f3f15a24820ab1a512d/aiomysql-0.2.0-py3-none-any.whl", hash = "sha256:b7c26da0daf23a5ec5e0b133c03d20657276e4eae9b73e040b72787f6f6ade0a", size = 44215, upload-time = "2023-06-11T19:57:51.09Z" },
+]
+
+[[package]]
+name = "aiopg"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "async-timeout" },
+ { name = "psycopg2-binary" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b0/0a/aba75a9ffcb1704b98c39986344230eaa70c40ac28e5ca635df231db912f/aiopg-1.4.0.tar.gz", hash = "sha256:116253bef86b4d954116716d181e9a0294037f266718b2e1c9766af995639d71", size = 35593, upload-time = "2022-10-26T09:31:49.478Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9b/2f/ab8690bf995171b9a8b60b98a2ca91d4108a42422abf10bf622397437d26/aiopg-1.4.0-py3-none-any.whl", hash = "sha256:aea46e8aff30b039cfa818e6db4752c97656e893fc75e5a5dc57355a9e9dedbd", size = 34770, upload-time = "2022-10-26T09:31:48.019Z" },
+]
+
+[[package]]
+name = "aiosqlite"
+version = "0.21.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/13/7d/8bca2bf9a247c2c5dfeec1d7a5f40db6518f88d314b8bca9da29670d2671/aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3", size = 13454, upload-time = "2025-02-03T07:30:16.235Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792, upload-time = "2025-02-03T07:30:13.6Z" },
+]
+
[[package]]
name = "annotated-types"
version = "0.7.0"
@@ -34,6 +71,111 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" },
]
+[[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/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" },
+ { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" },
+ { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" },
+ { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" },
+ { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
+ { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
+ { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
+ { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
+ { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
+ { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
+ { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
+ { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
+ { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
+ { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
+ { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
+ { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
+ { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
+ { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
+ { 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/46/7c/0c4760bccf082737ca7ab84a4c2034fcc06b1f21cf3032ea98bd6feb1725/charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", size = 209609, upload-time = "2025-10-14T04:42:10.922Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/a4/69719daef2f3d7f1819de60c9a6be981b8eeead7542d5ec4440f3c80e111/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", size = 149029, upload-time = "2025-10-14T04:42:12.38Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/21/8d4e1d6c1e6070d3672908b8e4533a71b5b53e71d16828cc24d0efec564c/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608", size = 144580, upload-time = "2025-10-14T04:42:13.549Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/0a/a616d001b3f25647a9068e0b9199f697ce507ec898cacb06a0d5a1617c99/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", size = 162340, upload-time = "2025-10-14T04:42:14.892Z" },
+ { url = "https://files.pythonhosted.org/packages/85/93/060b52deb249a5450460e0585c88a904a83aec474ab8e7aba787f45e79f2/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", size = 159619, upload-time = "2025-10-14T04:42:16.676Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/21/0274deb1cc0632cd587a9a0ec6b4674d9108e461cb4cd40d457adaeb0564/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", size = 153980, upload-time = "2025-10-14T04:42:17.917Z" },
+ { url = "https://files.pythonhosted.org/packages/28/2b/e3d7d982858dccc11b31906976323d790dded2017a0572f093ff982d692f/charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", size = 152174, upload-time = "2025-10-14T04:42:19.018Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/ff/4a269f8e35f1e58b2df52c131a1fa019acb7ef3f8697b7d464b07e9b492d/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", size = 151666, upload-time = "2025-10-14T04:42:20.171Z" },
+ { url = "https://files.pythonhosted.org/packages/da/c9/ec39870f0b330d58486001dd8e532c6b9a905f5765f58a6f8204926b4a93/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", size = 145550, upload-time = "2025-10-14T04:42:21.324Z" },
+ { url = "https://files.pythonhosted.org/packages/75/8f/d186ab99e40e0ed9f82f033d6e49001701c81244d01905dd4a6924191a30/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", size = 163721, upload-time = "2025-10-14T04:42:22.46Z" },
+ { url = "https://files.pythonhosted.org/packages/96/b1/6047663b9744df26a7e479ac1e77af7134b1fcf9026243bb48ee2d18810f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", size = 152127, upload-time = "2025-10-14T04:42:23.712Z" },
+ { url = "https://files.pythonhosted.org/packages/59/78/e5a6eac9179f24f704d1be67d08704c3c6ab9f00963963524be27c18ed87/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", size = 161175, upload-time = "2025-10-14T04:42:24.87Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/43/0e626e42d54dd2f8dd6fc5e1c5ff00f05fbca17cb699bedead2cae69c62f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", size = 155375, upload-time = "2025-10-14T04:42:27.246Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/91/d9615bf2e06f35e4997616ff31248c3657ed649c5ab9d35ea12fce54e380/charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", size = 99692, upload-time = "2025-10-14T04:42:28.425Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a9/6c040053909d9d1ef4fcab45fddec083aedc9052c10078339b47c8573ea8/charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", size = 107192, upload-time = "2025-10-14T04:42:29.482Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/c6/4fa536b2c0cd3edfb7ccf8469fa0f363ea67b7213a842b90909ca33dd851/charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", size = 100220, upload-time = "2025-10-14T04:42:30.632Z" },
+ { 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.2.1"
@@ -55,6 +197,266 @@ 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 = "commitizen"
+version = "4.10.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "argcomplete" },
+ { name = "charset-normalizer" },
+ { name = "colorama" },
+ { name = "decli" },
+ { name = "deprecated" },
+ { name = "importlib-metadata", marker = "python_full_version < '3.10'" },
+ { name = "jinja2" },
+ { name = "packaging" },
+ { name = "prompt-toolkit" },
+ { name = "pyyaml" },
+ { name = "questionary" },
+ { name = "termcolor", version = "3.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "termcolor", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "tomlkit" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ab/b3/cc29794fc2ecd7aa7353105773ca18ecd761c3ba5b38879bd106b3fc8840/commitizen-4.10.0.tar.gz", hash = "sha256:cc58067403b9eff21d0423b3d9a29bda05254bd51ad5bdd1fd0594bff31277e1", size = 56820, upload-time = "2025-11-10T14:08:49.365Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/5d/2bd8881737d6a5652ae3ebc37736893b9a7425f0eb16e605d1ff2957267e/commitizen-4.10.0-py3-none-any.whl", hash = "sha256:3fe56c168b30b30b84b8329cba6b132e77b4eb304a5460bbe2186aad0f78c966", size = 81269, upload-time = "2025-11-10T14:08:48.001Z" },
+]
+
+[[package]]
+name = "contourpy"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f5/f6/31a8f28b4a2a4fa0e01085e542f3081ab0588eff8e589d39d775172c9792/contourpy-1.3.0.tar.gz", hash = "sha256:7ffa0db17717a8ffb127efd0c95a4362d996b892c2904db72428d5b52e1938a4", size = 13464370, upload-time = "2024-08-27T21:00:03.328Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6c/e0/be8dcc796cfdd96708933e0e2da99ba4bb8f9b2caa9d560a50f3f09a65f3/contourpy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7", size = 265366, upload-time = "2024-08-27T20:50:09.947Z" },
+ { url = "https://files.pythonhosted.org/packages/50/d6/c953b400219443535d412fcbbc42e7a5e823291236bc0bb88936e3cc9317/contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42", size = 249226, upload-time = "2024-08-27T20:50:16.1Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/b4/6fffdf213ffccc28483c524b9dad46bb78332851133b36ad354b856ddc7c/contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7", size = 308460, upload-time = "2024-08-27T20:50:22.536Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/6c/118fc917b4050f0afe07179a6dcbe4f3f4ec69b94f36c9e128c4af480fb8/contourpy-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36f965570cff02b874773c49bfe85562b47030805d7d8360748f3eca570f4cab", size = 347623, upload-time = "2024-08-27T20:50:28.806Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/a4/30ff110a81bfe3abf7b9673284d21ddce8cc1278f6f77393c91199da4c90/contourpy-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cacd81e2d4b6f89c9f8a5b69b86490152ff39afc58a95af002a398273e5ce589", size = 317761, upload-time = "2024-08-27T20:50:35.126Z" },
+ { url = "https://files.pythonhosted.org/packages/99/e6/d11966962b1aa515f5586d3907ad019f4b812c04e4546cc19ebf62b5178e/contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41", size = 322015, upload-time = "2024-08-27T20:50:40.318Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/e3/182383743751d22b7b59c3c753277b6aee3637049197624f333dac5b4c80/contourpy-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a52040312b1a858b5e31ef28c2e865376a386c60c0e248370bbea2d3f3b760d", size = 1262672, upload-time = "2024-08-27T20:50:55.643Z" },
+ { url = "https://files.pythonhosted.org/packages/78/53/974400c815b2e605f252c8fb9297e2204347d1755a5374354ee77b1ea259/contourpy-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3faeb2998e4fcb256542e8a926d08da08977f7f5e62cf733f3c211c2a5586223", size = 1321688, upload-time = "2024-08-27T20:51:11.293Z" },
+ { url = "https://files.pythonhosted.org/packages/52/29/99f849faed5593b2926a68a31882af98afbeac39c7fdf7de491d9c85ec6a/contourpy-1.3.0-cp310-cp310-win32.whl", hash = "sha256:36e0cff201bcb17a0a8ecc7f454fe078437fa6bda730e695a92f2d9932bd507f", size = 171145, upload-time = "2024-08-27T20:51:15.2Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/97/3f89bba79ff6ff2b07a3cbc40aa693c360d5efa90d66e914f0ff03b95ec7/contourpy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:87ddffef1dbe5e669b5c2440b643d3fdd8622a348fe1983fad7a0f0ccb1cd67b", size = 216019, upload-time = "2024-08-27T20:51:19.365Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/1f/9375917786cb39270b0ee6634536c0e22abf225825602688990d8f5c6c19/contourpy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fa4c02abe6c446ba70d96ece336e621efa4aecae43eaa9b030ae5fb92b309ad", size = 266356, upload-time = "2024-08-27T20:51:24.146Z" },
+ { url = "https://files.pythonhosted.org/packages/05/46/9256dd162ea52790c127cb58cfc3b9e3413a6e3478917d1f811d420772ec/contourpy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:834e0cfe17ba12f79963861e0f908556b2cedd52e1f75e6578801febcc6a9f49", size = 250915, upload-time = "2024-08-27T20:51:28.683Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/5d/3056c167fa4486900dfbd7e26a2fdc2338dc58eee36d490a0ed3ddda5ded/contourpy-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbc4c3217eee163fa3984fd1567632b48d6dfd29216da3ded3d7b844a8014a66", size = 310443, upload-time = "2024-08-27T20:51:33.675Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/c2/1a612e475492e07f11c8e267ea5ec1ce0d89971be496c195e27afa97e14a/contourpy-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4865cd1d419e0c7a7bf6de1777b185eebdc51470800a9f42b9e9decf17762081", size = 348548, upload-time = "2024-08-27T20:51:39.322Z" },
+ { url = "https://files.pythonhosted.org/packages/45/cf/2c2fc6bb5874158277b4faf136847f0689e1b1a1f640a36d76d52e78907c/contourpy-1.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:303c252947ab4b14c08afeb52375b26781ccd6a5ccd81abcdfc1fafd14cf93c1", size = 319118, upload-time = "2024-08-27T20:51:44.717Z" },
+ { url = "https://files.pythonhosted.org/packages/03/33/003065374f38894cdf1040cef474ad0546368eea7e3a51d48b8a423961f8/contourpy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637f674226be46f6ba372fd29d9523dd977a291f66ab2a74fbeb5530bb3f445d", size = 323162, upload-time = "2024-08-27T20:51:49.683Z" },
+ { url = "https://files.pythonhosted.org/packages/42/80/e637326e85e4105a802e42959f56cff2cd39a6b5ef68d5d9aee3ea5f0e4c/contourpy-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76a896b2f195b57db25d6b44e7e03f221d32fe318d03ede41f8b4d9ba1bff53c", size = 1265396, upload-time = "2024-08-27T20:52:04.926Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/3b/8cbd6416ca1bbc0202b50f9c13b2e0b922b64be888f9d9ee88e6cfabfb51/contourpy-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e1fd23e9d01591bab45546c089ae89d926917a66dceb3abcf01f6105d927e2cb", size = 1324297, upload-time = "2024-08-27T20:52:21.843Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/2c/021a7afaa52fe891f25535506cc861c30c3c4e5a1c1ce94215e04b293e72/contourpy-1.3.0-cp311-cp311-win32.whl", hash = "sha256:d402880b84df3bec6eab53cd0cf802cae6a2ef9537e70cf75e91618a3801c20c", size = 171808, upload-time = "2024-08-27T20:52:25.163Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/2f/804f02ff30a7fae21f98198828d0857439ec4c91a96e20cf2d6c49372966/contourpy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:6cb6cc968059db9c62cb35fbf70248f40994dfcd7aa10444bbf8b3faeb7c2d67", size = 217181, upload-time = "2024-08-27T20:52:29.13Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/92/8e0bbfe6b70c0e2d3d81272b58c98ac69ff1a4329f18c73bd64824d8b12e/contourpy-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:570ef7cf892f0afbe5b2ee410c507ce12e15a5fa91017a0009f79f7d93a1268f", size = 267838, upload-time = "2024-08-27T20:52:33.911Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/04/33351c5d5108460a8ce6d512307690b023f0cfcad5899499f5c83b9d63b1/contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6", size = 251549, upload-time = "2024-08-27T20:52:39.179Z" },
+ { url = "https://files.pythonhosted.org/packages/51/3d/aa0fe6ae67e3ef9f178389e4caaaa68daf2f9024092aa3c6032e3d174670/contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639", size = 303177, upload-time = "2024-08-27T20:52:44.789Z" },
+ { url = "https://files.pythonhosted.org/packages/56/c3/c85a7e3e0cab635575d3b657f9535443a6f5d20fac1a1911eaa4bbe1aceb/contourpy-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c0da700bf58f6e0b65312d0a5e695179a71d0163957fa381bb3c1f72972537c", size = 341735, upload-time = "2024-08-27T20:52:51.05Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/8d/20f7a211a7be966a53f474bc90b1a8202e9844b3f1ef85f3ae45a77151ee/contourpy-1.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb8b141bb00fa977d9122636b16aa67d37fd40a3d8b52dd837e536d64b9a4d06", size = 314679, upload-time = "2024-08-27T20:52:58.473Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/be/524e377567defac0e21a46e2a529652d165fed130a0d8a863219303cee18/contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09", size = 320549, upload-time = "2024-08-27T20:53:06.593Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/96/fdb2552a172942d888915f3a6663812e9bc3d359d53dafd4289a0fb462f0/contourpy-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0dce35502151b6bd35027ac39ba6e5a44be13a68f55735c3612c568cac3805fd", size = 1263068, upload-time = "2024-08-27T20:53:23.442Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/25/632eab595e3140adfa92f1322bf8915f68c932bac468e89eae9974cf1c00/contourpy-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea348f053c645100612b333adc5983d87be69acdc6d77d3169c090d3b01dc35", size = 1322833, upload-time = "2024-08-27T20:53:39.243Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e3/69738782e315a1d26d29d71a550dbbe3eb6c653b028b150f70c1a5f4f229/contourpy-1.3.0-cp312-cp312-win32.whl", hash = "sha256:90f73a5116ad1ba7174341ef3ea5c3150ddf20b024b98fb0c3b29034752c8aeb", size = 172681, upload-time = "2024-08-27T20:53:43.05Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/89/9830ba00d88e43d15e53d64931e66b8792b46eb25e2050a88fec4a0df3d5/contourpy-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b11b39aea6be6764f84360fce6c82211a9db32a7c7de8fa6dd5397cf1d079c3b", size = 218283, upload-time = "2024-08-27T20:53:47.232Z" },
+ { url = "https://files.pythonhosted.org/packages/53/a1/d20415febfb2267af2d7f06338e82171824d08614084714fb2c1dac9901f/contourpy-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3e1c7fa44aaae40a2247e2e8e0627f4bea3dd257014764aa644f319a5f8600e3", size = 267879, upload-time = "2024-08-27T20:53:51.597Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/45/5a28a3570ff6218d8bdfc291a272a20d2648104815f01f0177d103d985e1/contourpy-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:364174c2a76057feef647c802652f00953b575723062560498dc7930fc9b1cb7", size = 251573, upload-time = "2024-08-27T20:53:55.659Z" },
+ { url = "https://files.pythonhosted.org/packages/39/1c/d3f51540108e3affa84f095c8b04f0aa833bb797bc8baa218a952a98117d/contourpy-1.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32b238b3b3b649e09ce9aaf51f0c261d38644bdfa35cbaf7b263457850957a84", size = 303184, upload-time = "2024-08-27T20:54:00.225Z" },
+ { url = "https://files.pythonhosted.org/packages/00/56/1348a44fb6c3a558c1a3a0cd23d329d604c99d81bf5a4b58c6b71aab328f/contourpy-1.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d51fca85f9f7ad0b65b4b9fe800406d0d77017d7270d31ec3fb1cc07358fdea0", size = 340262, upload-time = "2024-08-27T20:54:05.234Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/23/00d665ba67e1bb666152131da07e0f24c95c3632d7722caa97fb61470eca/contourpy-1.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:732896af21716b29ab3e988d4ce14bc5133733b85956316fb0c56355f398099b", size = 313806, upload-time = "2024-08-27T20:54:09.889Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/42/3cf40f7040bb8362aea19af9a5fb7b32ce420f645dd1590edcee2c657cd5/contourpy-1.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d73f659398a0904e125280836ae6f88ba9b178b2fed6884f3b1f95b989d2c8da", size = 319710, upload-time = "2024-08-27T20:54:14.536Z" },
+ { url = "https://files.pythonhosted.org/packages/05/32/f3bfa3fc083b25e1a7ae09197f897476ee68e7386e10404bdf9aac7391f0/contourpy-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c6c7c2408b7048082932cf4e641fa3b8ca848259212f51c8c59c45aa7ac18f14", size = 1264107, upload-time = "2024-08-27T20:54:29.735Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/1e/1019d34473a736664f2439542b890b2dc4c6245f5c0d8cdfc0ccc2cab80c/contourpy-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f317576606de89da6b7e0861cf6061f6146ead3528acabff9236458a6ba467f8", size = 1322458, upload-time = "2024-08-27T20:54:45.507Z" },
+ { url = "https://files.pythonhosted.org/packages/22/85/4f8bfd83972cf8909a4d36d16b177f7b8bdd942178ea4bf877d4a380a91c/contourpy-1.3.0-cp313-cp313-win32.whl", hash = "sha256:31cd3a85dbdf1fc002280c65caa7e2b5f65e4a973fcdf70dd2fdcb9868069294", size = 172643, upload-time = "2024-08-27T20:55:52.754Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/4a/fb3c83c1baba64ba90443626c228ca14f19a87c51975d3b1de308dd2cf08/contourpy-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4553c421929ec95fb07b3aaca0fae668b2eb5a5203d1217ca7c34c063c53d087", size = 218301, upload-time = "2024-08-27T20:55:56.509Z" },
+ { url = "https://files.pythonhosted.org/packages/76/65/702f4064f397821fea0cb493f7d3bc95a5d703e20954dce7d6d39bacf378/contourpy-1.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:345af746d7766821d05d72cb8f3845dfd08dd137101a2cb9b24de277d716def8", size = 278972, upload-time = "2024-08-27T20:54:50.347Z" },
+ { url = "https://files.pythonhosted.org/packages/80/85/21f5bba56dba75c10a45ec00ad3b8190dbac7fd9a8a8c46c6116c933e9cf/contourpy-1.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3bb3808858a9dc68f6f03d319acd5f1b8a337e6cdda197f02f4b8ff67ad2057b", size = 263375, upload-time = "2024-08-27T20:54:54.909Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/64/084c86ab71d43149f91ab3a4054ccf18565f0a8af36abfa92b1467813ed6/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:420d39daa61aab1221567b42eecb01112908b2cab7f1b4106a52caaec8d36973", size = 307188, upload-time = "2024-08-27T20:55:00.184Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/ff/d61a4c288dc42da0084b8d9dc2aa219a850767165d7d9a9c364ff530b509/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d63ee447261e963af02642ffcb864e5a2ee4cbfd78080657a9880b8b1868e18", size = 345644, upload-time = "2024-08-27T20:55:05.673Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/aa/00d2313d35ec03f188e8f0786c2fc61f589306e02fdc158233697546fd58/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:167d6c890815e1dac9536dca00828b445d5d0df4d6a8c6adb4a7ec3166812fa8", size = 317141, upload-time = "2024-08-27T20:55:11.047Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/6a/b5242c8cb32d87f6abf4f5e3044ca397cb1a76712e3fa2424772e3ff495f/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:710a26b3dc80c0e4febf04555de66f5fd17e9cf7170a7b08000601a10570bda6", size = 323469, upload-time = "2024-08-27T20:55:15.914Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/a6/73e929d43028a9079aca4bde107494864d54f0d72d9db508a51ff0878593/contourpy-1.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:75ee7cb1a14c617f34a51d11fa7524173e56551646828353c4af859c56b766e2", size = 1260894, upload-time = "2024-08-27T20:55:31.553Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/1e/1e726ba66eddf21c940821df8cf1a7d15cb165f0682d62161eaa5e93dae1/contourpy-1.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:33c92cdae89ec5135d036e7218e69b0bb2851206077251f04a6c4e0e21f03927", size = 1314829, upload-time = "2024-08-27T20:55:47.837Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/e3/b9f72758adb6ef7397327ceb8b9c39c75711affb220e4f53c745ea1d5a9a/contourpy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a11077e395f67ffc2c44ec2418cfebed032cd6da3022a94fc227b6faf8e2acb8", size = 265518, upload-time = "2024-08-27T20:56:01.333Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/22/19f5b948367ab5260fb41d842c7a78dae645603881ea6bc39738bcfcabf6/contourpy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8134301d7e204c88ed7ab50028ba06c683000040ede1d617298611f9dc6240c", size = 249350, upload-time = "2024-08-27T20:56:05.432Z" },
+ { url = "https://files.pythonhosted.org/packages/26/76/0c7d43263dd00ae21a91a24381b7e813d286a3294d95d179ef3a7b9fb1d7/contourpy-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12968fdfd5bb45ffdf6192a590bd8ddd3ba9e58360b29683c6bb71a7b41edca", size = 309167, upload-time = "2024-08-27T20:56:10.034Z" },
+ { url = "https://files.pythonhosted.org/packages/96/3b/cadff6773e89f2a5a492c1a8068e21d3fccaf1a1c1df7d65e7c8e3ef60ba/contourpy-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd2a0fc506eccaaa7595b7e1418951f213cf8255be2600f1ea1b61e46a60c55f", size = 348279, upload-time = "2024-08-27T20:56:15.41Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/86/158cc43aa549d2081a955ab11c6bdccc7a22caacc2af93186d26f5f48746/contourpy-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfb5c62ce023dfc410d6059c936dcf96442ba40814aefbfa575425a3a7f19dc", size = 318519, upload-time = "2024-08-27T20:56:21.813Z" },
+ { url = "https://files.pythonhosted.org/packages/05/11/57335544a3027e9b96a05948c32e566328e3a2f84b7b99a325b7a06d2b06/contourpy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68a32389b06b82c2fdd68276148d7b9275b5f5cf13e5417e4252f6d1a34f72a2", size = 321922, upload-time = "2024-08-27T20:56:26.983Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/e3/02114f96543f4a1b694333b92a6dcd4f8eebbefcc3a5f3bbb1316634178f/contourpy-1.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94e848a6b83da10898cbf1311a815f770acc9b6a3f2d646f330d57eb4e87592e", size = 1258017, upload-time = "2024-08-27T20:56:42.246Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/3b/bfe4c81c6d5881c1c643dde6620be0b42bf8aab155976dd644595cfab95c/contourpy-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d78ab28a03c854a873787a0a42254a0ccb3cb133c672f645c9f9c8f3ae9d0800", size = 1316773, upload-time = "2024-08-27T20:56:58.58Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/17/c52d2970784383cafb0bd918b6fb036d98d96bbf0bc1befb5d1e31a07a70/contourpy-1.3.0-cp39-cp39-win32.whl", hash = "sha256:81cb5ed4952aae6014bc9d0421dec7c5835c9c8c31cdf51910b708f548cf58e5", size = 171353, upload-time = "2024-08-27T20:57:02.718Z" },
+ { url = "https://files.pythonhosted.org/packages/53/23/db9f69676308e094d3c45f20cc52e12d10d64f027541c995d89c11ad5c75/contourpy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:14e262f67bd7e6eb6880bc564dcda30b15e351a594657e55b7eec94b6ef72843", size = 211817, upload-time = "2024-08-27T20:57:06.328Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/09/60e486dc2b64c94ed33e58dcfb6f808192c03dfc5574c016218b9b7680dc/contourpy-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe41b41505a5a33aeaed2a613dccaeaa74e0e3ead6dd6fd3a118fb471644fd6c", size = 261886, upload-time = "2024-08-27T20:57:10.863Z" },
+ { url = "https://files.pythonhosted.org/packages/19/20/b57f9f7174fcd439a7789fb47d764974ab646fa34d1790551de386457a8e/contourpy-1.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca7e17a65f72a5133bdbec9ecf22401c62bcf4821361ef7811faee695799779", size = 311008, upload-time = "2024-08-27T20:57:15.588Z" },
+ { url = "https://files.pythonhosted.org/packages/74/fc/5040d42623a1845d4f17a418e590fd7a79ae8cb2bad2b2f83de63c3bdca4/contourpy-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ec4dc6bf570f5b22ed0d7efba0dfa9c5b9e0431aeea7581aa217542d9e809a4", size = 215690, upload-time = "2024-08-27T20:57:19.321Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/24/dc3dcd77ac7460ab7e9d2b01a618cb31406902e50e605a8d6091f0a8f7cc/contourpy-1.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:00ccd0dbaad6d804ab259820fa7cb0b8036bda0686ef844d24125d8287178ce0", size = 261894, upload-time = "2024-08-27T20:57:23.873Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/db/531642a01cfec39d1682e46b5457b07cf805e3c3c584ec27e2a6223f8f6c/contourpy-1.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca947601224119117f7c19c9cdf6b3ab54c5726ef1d906aa4a69dfb6dd58102", size = 311099, upload-time = "2024-08-27T20:57:28.58Z" },
+ { url = "https://files.pythonhosted.org/packages/38/1e/94bda024d629f254143a134eead69e21c836429a2a6ce82209a00ddcb79a/contourpy-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6ec93afeb848a0845a18989da3beca3eec2c0f852322efe21af1931147d12cb", size = 215838, upload-time = "2024-08-27T20:57:32.913Z" },
+]
+
+[[package]]
+name = "contourpy"
+version = "1.3.2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" },
+ { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" },
+ { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" },
+ { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" },
+ { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" },
+ { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" },
+ { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" },
+ { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" },
+ { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" },
+ { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" },
+ { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" },
+ { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" },
+ { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" },
+ { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" },
+ { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" },
+ { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" },
+ { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" },
+ { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" },
+ { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" },
+ { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" },
+ { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" },
+ { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" },
+]
+
+[[package]]
+name = "contourpy"
+version = "1.3.3"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+]
+dependencies = [
+ { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" },
+ { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" },
+ { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" },
+ { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" },
+ { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" },
+ { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" },
+ { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" },
+ { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" },
+ { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" },
+ { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" },
+ { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" },
+ { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" },
+ { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" },
+ { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" },
+ { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" },
+ { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" },
+ { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" },
+ { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" },
+ { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" },
+ { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" },
+ { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" },
+ { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" },
+ { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" },
+ { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" },
+ { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" },
+ { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" },
+ { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" },
+ { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" },
+ { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" },
+ { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" },
+ { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" },
+ { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" },
+ { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" },
+]
+
[[package]]
name = "coverage"
version = "7.9.1"
@@ -119,18 +521,113 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/08/b8/7ddd1e8ba9701dea08ce22029917140e6f66a859427406579fd8d0ca7274/coverage-7.9.1-py3-none-any.whl", hash = "sha256:66b974b145aa189516b6bf2d8423e888b742517d37872f6ee4c5be0073bd9a3c", size = 204000, upload-time = "2025-06-13T13:02:27.173Z" },
]
+[[package]]
+name = "cycler"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
+]
+
+[[package]]
+name = "decli"
+version = "0.6.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0c/59/d4ffff1dee2c8f6f2dd8f87010962e60f7b7847504d765c91ede5a466730/decli-0.6.3.tar.gz", hash = "sha256:87f9d39361adf7f16b9ca6e3b614badf7519da13092f2db3c80ca223c53c7656", size = 7564, upload-time = "2025-06-01T15:23:41.25Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d8/fa/ec878c28bc7f65b77e7e17af3522c9948a9711b9fa7fc4c5e3140a7e3578/decli-0.6.3-py3-none-any.whl", hash = "sha256:5152347c7bb8e3114ad65db719e5709b28d7f7f45bdb709f70167925e55640f3", size = 7989, upload-time = "2025-06-01T15:23:40.228Z" },
+]
+
+[[package]]
+name = "deprecated"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "wrapt" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" },
+]
+
[[package]]
name = "exceptiongroup"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
]
+[[package]]
+name = "fonttools"
+version = "4.60.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4b/42/97a13e47a1e51a5a7142475bbcf5107fe3a68fc34aef331c897d5fb98ad0/fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9", size = 3559823, upload-time = "2025-09-29T21:13:27.129Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/26/70/03e9d89a053caff6ae46053890eba8e4a5665a7c5638279ed4492e6d4b8b/fonttools-4.60.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9a52f254ce051e196b8fe2af4634c2d2f02c981756c6464dc192f1b6050b4e28", size = 2810747, upload-time = "2025-09-29T21:10:59.653Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/41/449ad5aff9670ab0df0f61ee593906b67a36d7e0b4d0cd7fa41ac0325bf5/fonttools-4.60.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7420a2696a44650120cdd269a5d2e56a477e2bfa9d95e86229059beb1c19e15", size = 2346909, upload-time = "2025-09-29T21:11:02.882Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/18/e5970aa96c8fad1cb19a9479cc3b7602c0c98d250fcdc06a5da994309c50/fonttools-4.60.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee0c0b3b35b34f782afc673d503167157094a16f442ace7c6c5e0ca80b08f50c", size = 4864572, upload-time = "2025-09-29T21:11:05.096Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/20/9b2b4051b6ec6689480787d506b5003f72648f50972a92d04527a456192c/fonttools-4.60.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:282dafa55f9659e8999110bd8ed422ebe1c8aecd0dc396550b038e6c9a08b8ea", size = 4794635, upload-time = "2025-09-29T21:11:08.651Z" },
+ { url = "https://files.pythonhosted.org/packages/10/52/c791f57347c1be98f8345e3dca4ac483eb97666dd7c47f3059aeffab8b59/fonttools-4.60.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4ba4bd646e86de16160f0fb72e31c3b9b7d0721c3e5b26b9fa2fc931dfdb2652", size = 4843878, upload-time = "2025-09-29T21:11:10.893Z" },
+ { url = "https://files.pythonhosted.org/packages/69/e9/35c24a8d01644cee8c090a22fad34d5b61d1e0a8ecbc9945ad785ebf2e9e/fonttools-4.60.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0b0835ed15dd5b40d726bb61c846a688f5b4ce2208ec68779bc81860adb5851a", size = 4954555, upload-time = "2025-09-29T21:11:13.24Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/86/fb1e994971be4bdfe3a307de6373ef69a9df83fb66e3faa9c8114893d4cc/fonttools-4.60.1-cp310-cp310-win32.whl", hash = "sha256:1525796c3ffe27bb6268ed2a1bb0dcf214d561dfaf04728abf01489eb5339dce", size = 2232019, upload-time = "2025-09-29T21:11:15.73Z" },
+ { url = "https://files.pythonhosted.org/packages/40/84/62a19e2bd56f0e9fb347486a5b26376bade4bf6bbba64dda2c103bd08c94/fonttools-4.60.1-cp310-cp310-win_amd64.whl", hash = "sha256:268ecda8ca6cb5c4f044b1fb9b3b376e8cd1b361cef275082429dc4174907038", size = 2276803, upload-time = "2025-09-29T21:11:18.152Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/85/639aa9bface1537e0fb0f643690672dde0695a5bbbc90736bc571b0b1941/fonttools-4.60.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f", size = 2831872, upload-time = "2025-09-29T21:11:20.329Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/47/3c63158459c95093be9618794acb1067b3f4d30dcc5c3e8114b70e67a092/fonttools-4.60.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2", size = 2356990, upload-time = "2025-09-29T21:11:22.754Z" },
+ { url = "https://files.pythonhosted.org/packages/94/dd/1934b537c86fcf99f9761823f1fc37a98fbd54568e8e613f29a90fed95a9/fonttools-4.60.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914", size = 5042189, upload-time = "2025-09-29T21:11:25.061Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/d2/9f4e4c4374dd1daa8367784e1bd910f18ba886db1d6b825b12edf6db3edc/fonttools-4.60.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1", size = 4978683, upload-time = "2025-09-29T21:11:27.693Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/c4/0fb2dfd1ecbe9a07954cc13414713ed1eab17b1c0214ef07fc93df234a47/fonttools-4.60.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d", size = 5021372, upload-time = "2025-09-29T21:11:30.257Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/d5/495fc7ae2fab20223cc87179a8f50f40f9a6f821f271ba8301ae12bb580f/fonttools-4.60.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa", size = 5132562, upload-time = "2025-09-29T21:11:32.737Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/fa/021dab618526323c744e0206b3f5c8596a2e7ae9aa38db5948a131123e83/fonttools-4.60.1-cp311-cp311-win32.whl", hash = "sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258", size = 2230288, upload-time = "2025-09-29T21:11:35.015Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/78/0e1a6d22b427579ea5c8273e1c07def2f325b977faaf60bb7ddc01456cb1/fonttools-4.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf", size = 2278184, upload-time = "2025-09-29T21:11:37.434Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/f7/a10b101b7a6f8836a5adb47f2791f2075d044a6ca123f35985c42edc82d8/fonttools-4.60.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc", size = 2832953, upload-time = "2025-09-29T21:11:39.616Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/fe/7bd094b59c926acf2304d2151354ddbeb74b94812f3dc943c231db09cb41/fonttools-4.60.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877", size = 2352706, upload-time = "2025-09-29T21:11:41.826Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/ca/4bb48a26ed95a1e7eba175535fe5805887682140ee0a0d10a88e1de84208/fonttools-4.60.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c", size = 4923716, upload-time = "2025-09-29T21:11:43.893Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/9f/2cb82999f686c1d1ddf06f6ae1a9117a880adbec113611cc9d22b2fdd465/fonttools-4.60.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401", size = 4968175, upload-time = "2025-09-29T21:11:46.439Z" },
+ { url = "https://files.pythonhosted.org/packages/18/79/be569699e37d166b78e6218f2cde8c550204f2505038cdd83b42edc469b9/fonttools-4.60.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903", size = 4911031, upload-time = "2025-09-29T21:11:48.977Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/9f/89411cc116effaec5260ad519162f64f9c150e5522a27cbb05eb62d0c05b/fonttools-4.60.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed", size = 5062966, upload-time = "2025-09-29T21:11:54.344Z" },
+ { url = "https://files.pythonhosted.org/packages/62/a1/f888221934b5731d46cb9991c7a71f30cb1f97c0ef5fcf37f8da8fce6c8e/fonttools-4.60.1-cp312-cp312-win32.whl", hash = "sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6", size = 2218750, upload-time = "2025-09-29T21:11:56.601Z" },
+ { url = "https://files.pythonhosted.org/packages/88/8f/a55b5550cd33cd1028601df41acd057d4be20efa5c958f417b0c0613924d/fonttools-4.60.1-cp312-cp312-win_amd64.whl", hash = "sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383", size = 2267026, upload-time = "2025-09-29T21:11:58.852Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/5b/cdd2c612277b7ac7ec8c0c9bc41812c43dc7b2d5f2b0897e15fdf5a1f915/fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb", size = 2825777, upload-time = "2025-09-29T21:12:01.22Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/8a/de9cc0540f542963ba5e8f3a1f6ad48fa211badc3177783b9d5cadf79b5d/fonttools-4.60.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4", size = 2348080, upload-time = "2025-09-29T21:12:03.785Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/8b/371ab3cec97ee3fe1126b3406b7abd60c8fec8975fd79a3c75cdea0c3d83/fonttools-4.60.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c", size = 4903082, upload-time = "2025-09-29T21:12:06.382Z" },
+ { url = "https://files.pythonhosted.org/packages/04/05/06b1455e4bc653fcb2117ac3ef5fa3a8a14919b93c60742d04440605d058/fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77", size = 4960125, upload-time = "2025-09-29T21:12:09.314Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/37/f3b840fcb2666f6cb97038793606bdd83488dca2d0b0fc542ccc20afa668/fonttools-4.60.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199", size = 4901454, upload-time = "2025-09-29T21:12:11.931Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/9e/eb76f77e82f8d4a46420aadff12cec6237751b0fb9ef1de373186dcffb5f/fonttools-4.60.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c", size = 5044495, upload-time = "2025-09-29T21:12:15.241Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/b3/cede8f8235d42ff7ae891bae8d619d02c8ac9fd0cfc450c5927a6200c70d/fonttools-4.60.1-cp313-cp313-win32.whl", hash = "sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272", size = 2217028, upload-time = "2025-09-29T21:12:17.96Z" },
+ { url = "https://files.pythonhosted.org/packages/75/4d/b022c1577807ce8b31ffe055306ec13a866f2337ecee96e75b24b9b753ea/fonttools-4.60.1-cp313-cp313-win_amd64.whl", hash = "sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac", size = 2266200, upload-time = "2025-09-29T21:12:20.14Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/83/752ca11c1aa9a899b793a130f2e466b79ea0cf7279c8d79c178fc954a07b/fonttools-4.60.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3", size = 2822830, upload-time = "2025-09-29T21:12:24.406Z" },
+ { url = "https://files.pythonhosted.org/packages/57/17/bbeab391100331950a96ce55cfbbff27d781c1b85ebafb4167eae50d9fe3/fonttools-4.60.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85", size = 2345524, upload-time = "2025-09-29T21:12:26.819Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/2e/d4831caa96d85a84dd0da1d9f90d81cec081f551e0ea216df684092c6c97/fonttools-4.60.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537", size = 4843490, upload-time = "2025-09-29T21:12:29.123Z" },
+ { url = "https://files.pythonhosted.org/packages/49/13/5e2ea7c7a101b6fc3941be65307ef8df92cbbfa6ec4804032baf1893b434/fonttools-4.60.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003", size = 4944184, upload-time = "2025-09-29T21:12:31.414Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/2b/cf9603551c525b73fc47c52ee0b82a891579a93d9651ed694e4e2cd08bb8/fonttools-4.60.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08", size = 4890218, upload-time = "2025-09-29T21:12:33.936Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/2f/933d2352422e25f2376aae74f79eaa882a50fb3bfef3c0d4f50501267101/fonttools-4.60.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99", size = 4999324, upload-time = "2025-09-29T21:12:36.637Z" },
+ { url = "https://files.pythonhosted.org/packages/38/99/234594c0391221f66216bc2c886923513b3399a148defaccf81dc3be6560/fonttools-4.60.1-cp314-cp314-win32.whl", hash = "sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6", size = 2220861, upload-time = "2025-09-29T21:12:39.108Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/1d/edb5b23726dde50fc4068e1493e4fc7658eeefcaf75d4c5ffce067d07ae5/fonttools-4.60.1-cp314-cp314-win_amd64.whl", hash = "sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987", size = 2270934, upload-time = "2025-09-29T21:12:41.339Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/da/1392aaa2170adc7071fe7f9cfd181a5684a7afcde605aebddf1fb4d76df5/fonttools-4.60.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299", size = 2894340, upload-time = "2025-09-29T21:12:43.774Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/a7/3b9f16e010d536ce567058b931a20b590d8f3177b2eda09edd92e392375d/fonttools-4.60.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01", size = 2375073, upload-time = "2025-09-29T21:12:46.437Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/b5/e9bcf51980f98e59bb5bb7c382a63c6f6cac0eec5f67de6d8f2322382065/fonttools-4.60.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801", size = 4849758, upload-time = "2025-09-29T21:12:48.694Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/dc/1d2cf7d1cba82264b2f8385db3f5960e3d8ce756b4dc65b700d2c496f7e9/fonttools-4.60.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc", size = 5085598, upload-time = "2025-09-29T21:12:51.081Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/4d/279e28ba87fb20e0c69baf72b60bbf1c4d873af1476806a7b5f2b7fac1ff/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc", size = 4957603, upload-time = "2025-09-29T21:12:53.423Z" },
+ { url = "https://files.pythonhosted.org/packages/78/d4/ff19976305e0c05aa3340c805475abb00224c954d3c65e82c0a69633d55d/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed", size = 4974184, upload-time = "2025-09-29T21:12:55.962Z" },
+ { url = "https://files.pythonhosted.org/packages/63/22/8553ff6166f5cd21cfaa115aaacaa0dc73b91c079a8cfd54a482cbc0f4f5/fonttools-4.60.1-cp314-cp314t-win32.whl", hash = "sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259", size = 2282241, upload-time = "2025-09-29T21:12:58.179Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/cb/fa7b4d148e11d5a72761a22e595344133e83a9507a4c231df972e657579b/fonttools-4.60.1-cp314-cp314t-win_amd64.whl", hash = "sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c", size = 2345760, upload-time = "2025-09-29T21:13:00.375Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/7f/1c9a6cc6e7374ab59bbe91cb3b8a65ce0907c59f8f35368bb3bf941bd458/fonttools-4.60.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:122e1a8ada290423c493491d002f622b1992b1ab0b488c68e31c413390dc7eb2", size = 2816178, upload-time = "2025-09-29T21:13:02.915Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/ac/acb4dcf1932566c0b57b5261f93a8b97cb3ebae08d07aff1288e7c9d7faa/fonttools-4.60.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a140761c4ff63d0cb9256ac752f230460ee225ccef4ad8f68affc723c88e2036", size = 2349175, upload-time = "2025-09-29T21:13:05.432Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/ac/0b2f8d62c857adfe96551d56abbbc3d2eda2e4715a2e91c5eb7815bb38e1/fonttools-4.60.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eae96373e4b7c9e45d099d7a523444e3554360927225c1cdae221a58a45b856", size = 4840452, upload-time = "2025-09-29T21:13:08.679Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/e1/b2e2ae805f263507e050f1ebfc2fb3654124161f3bea466a1b2a4485c705/fonttools-4.60.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:596ecaca36367027d525b3b426d8a8208169d09edcf8c7506aceb3a38bfb55c7", size = 4774040, upload-time = "2025-09-29T21:13:11.424Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/91/05949ba6f757014f343632b142543576eb100aeb261c036b86e7d1fc50f0/fonttools-4.60.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2ee06fc57512144d8b0445194c2da9f190f61ad51e230f14836286470c99f854", size = 4823746, upload-time = "2025-09-29T21:13:14.08Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/cf/db9a1bd8d835dc17f09104f83b9d8c078d7bebbaaa9bd41378bf10f025de/fonttools-4.60.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b42d86938e8dda1cd9a1a87a6d82f1818eaf933348429653559a458d027446da", size = 4934001, upload-time = "2025-09-29T21:13:16.435Z" },
+ { url = "https://files.pythonhosted.org/packages/87/4a/c58503524f7e6c73eb33b944f27535460e1050a58c99bd5b441242fcca86/fonttools-4.60.1-cp39-cp39-win32.whl", hash = "sha256:8b4eb332f9501cb1cd3d4d099374a1e1306783ff95489a1026bde9eb02ccc34a", size = 1499091, upload-time = "2025-09-29T21:13:19.072Z" },
+ { url = "https://files.pythonhosted.org/packages/69/8f/3394936411aec5f26a1fdf8d7fdc1da7c276e0c627cd71b7b266b2431681/fonttools-4.60.1-cp39-cp39-win_amd64.whl", hash = "sha256:7473a8ed9ed09aeaa191301244a5a9dbe46fe0bf54f9d6cd21d83044c3321217", size = 1543835, upload-time = "2025-09-29T21:13:21.606Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/93/0dd45cd283c32dea1545151d8c3637b4b8c53cdb3a625aeb2885b184d74d/fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", size = 1143175, upload-time = "2025-09-29T21:13:24.134Z" },
+]
+
[[package]]
name = "granian"
version = "2.3.4"
@@ -252,6 +749,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]
+[[package]]
+name = "importlib-metadata"
+version = "8.6.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "zipp", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/33/08/c1395a292bb23fd03bdf572a1357c5a733d3eecbab877641ceacab23db6e/importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580", size = 55767, upload-time = "2025-01-20T22:21:30.429Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e", size = 26971, upload-time = "2025-01-20T22:21:29.177Z" },
+]
+
+[[package]]
+name = "importlib-resources"
+version = "6.5.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "zipp", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" },
+]
+
[[package]]
name = "iniconfig"
version = "2.1.0"
@@ -282,6 +803,245 @@ 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 = "kiwisolver"
+version = "1.4.7"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/85/4d/2255e1c76304cbd60b48cee302b66d1dde4468dc5b1160e4b7cb43778f2a/kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", size = 97286, upload-time = "2024-09-04T09:39:44.302Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/97/14/fc943dd65268a96347472b4fbe5dcc2f6f55034516f80576cd0dd3a8930f/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6", size = 122440, upload-time = "2024-09-04T09:03:44.9Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/46/e68fed66236b69dd02fcdb506218c05ac0e39745d696d22709498896875d/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17", size = 65758, upload-time = "2024-09-04T09:03:46.582Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/fa/65de49c85838681fc9cb05de2a68067a683717321e01ddafb5b8024286f0/kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9", size = 64311, upload-time = "2024-09-04T09:03:47.973Z" },
+ { url = "https://files.pythonhosted.org/packages/42/9c/cc8d90f6ef550f65443bad5872ffa68f3dee36de4974768628bea7c14979/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9", size = 1637109, upload-time = "2024-09-04T09:03:49.281Z" },
+ { url = "https://files.pythonhosted.org/packages/55/91/0a57ce324caf2ff5403edab71c508dd8f648094b18cfbb4c8cc0fde4a6ac/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c", size = 1617814, upload-time = "2024-09-04T09:03:51.444Z" },
+ { url = "https://files.pythonhosted.org/packages/12/5d/c36140313f2510e20207708adf36ae4919416d697ee0236b0ddfb6fd1050/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599", size = 1400881, upload-time = "2024-09-04T09:03:53.357Z" },
+ { url = "https://files.pythonhosted.org/packages/56/d0/786e524f9ed648324a466ca8df86298780ef2b29c25313d9a4f16992d3cf/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05", size = 1512972, upload-time = "2024-09-04T09:03:55.082Z" },
+ { url = "https://files.pythonhosted.org/packages/67/5a/77851f2f201e6141d63c10a0708e996a1363efaf9e1609ad0441b343763b/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407", size = 1444787, upload-time = "2024-09-04T09:03:56.588Z" },
+ { url = "https://files.pythonhosted.org/packages/06/5f/1f5eaab84355885e224a6fc8d73089e8713dc7e91c121f00b9a1c58a2195/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278", size = 2199212, upload-time = "2024-09-04T09:03:58.557Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/28/9152a3bfe976a0ae21d445415defc9d1cd8614b2910b7614b30b27a47270/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5", size = 2346399, upload-time = "2024-09-04T09:04:00.178Z" },
+ { url = "https://files.pythonhosted.org/packages/26/f6/453d1904c52ac3b400f4d5e240ac5fec25263716723e44be65f4d7149d13/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad", size = 2308688, upload-time = "2024-09-04T09:04:02.216Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/9a/d4968499441b9ae187e81745e3277a8b4d7c60840a52dc9d535a7909fac3/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895", size = 2445493, upload-time = "2024-09-04T09:04:04.571Z" },
+ { url = "https://files.pythonhosted.org/packages/07/c9/032267192e7828520dacb64dfdb1d74f292765f179e467c1cba97687f17d/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3", size = 2262191, upload-time = "2024-09-04T09:04:05.969Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/ad/db0aedb638a58b2951da46ddaeecf204be8b4f5454df020d850c7fa8dca8/kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc", size = 46644, upload-time = "2024-09-04T09:04:07.408Z" },
+ { url = "https://files.pythonhosted.org/packages/12/ca/d0f7b7ffbb0be1e7c2258b53554efec1fd652921f10d7d85045aff93ab61/kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c", size = 55877, upload-time = "2024-09-04T09:04:08.869Z" },
+ { url = "https://files.pythonhosted.org/packages/97/6c/cfcc128672f47a3e3c0d918ecb67830600078b025bfc32d858f2e2d5c6a4/kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a", size = 48347, upload-time = "2024-09-04T09:04:10.106Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/44/77429fa0a58f941d6e1c58da9efe08597d2e86bf2b2cce6626834f49d07b/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54", size = 122442, upload-time = "2024-09-04T09:04:11.432Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/20/8c75caed8f2462d63c7fd65e16c832b8f76cda331ac9e615e914ee80bac9/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95", size = 65762, upload-time = "2024-09-04T09:04:12.468Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/98/fe010f15dc7230f45bc4cf367b012d651367fd203caaa992fd1f5963560e/kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935", size = 64319, upload-time = "2024-09-04T09:04:13.635Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/1b/b5d618f4e58c0675654c1e5051bcf42c776703edb21c02b8c74135541f60/kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb", size = 1334260, upload-time = "2024-09-04T09:04:14.878Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/01/946852b13057a162a8c32c4c8d2e9ed79f0bb5d86569a40c0b5fb103e373/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02", size = 1426589, upload-time = "2024-09-04T09:04:16.514Z" },
+ { url = "https://files.pythonhosted.org/packages/70/d1/c9f96df26b459e15cf8a965304e6e6f4eb291e0f7a9460b4ad97b047561e/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51", size = 1541080, upload-time = "2024-09-04T09:04:18.322Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/73/2686990eb8b02d05f3de759d6a23a4ee7d491e659007dd4c075fede4b5d0/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052", size = 1470049, upload-time = "2024-09-04T09:04:20.266Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/4b/2db7af3ed3af7c35f388d5f53c28e155cd402a55432d800c543dc6deb731/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18", size = 1426376, upload-time = "2024-09-04T09:04:22.419Z" },
+ { url = "https://files.pythonhosted.org/packages/05/83/2857317d04ea46dc5d115f0df7e676997bbd968ced8e2bd6f7f19cfc8d7f/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545", size = 2222231, upload-time = "2024-09-04T09:04:24.526Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/b5/866f86f5897cd4ab6d25d22e403404766a123f138bd6a02ecb2cdde52c18/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b", size = 2368634, upload-time = "2024-09-04T09:04:25.899Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/ee/73de8385403faba55f782a41260210528fe3273d0cddcf6d51648202d6d0/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36", size = 2329024, upload-time = "2024-09-04T09:04:28.523Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/e7/cd101d8cd2cdfaa42dc06c433df17c8303d31129c9fdd16c0ea37672af91/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3", size = 2468484, upload-time = "2024-09-04T09:04:30.547Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/72/84f09d45a10bc57a40bb58b81b99d8f22b58b2040c912b7eb97ebf625bf2/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523", size = 2284078, upload-time = "2024-09-04T09:04:33.218Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/d4/71828f32b956612dc36efd7be1788980cb1e66bfb3706e6dec9acad9b4f9/kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d", size = 46645, upload-time = "2024-09-04T09:04:34.371Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/65/d43e9a20aabcf2e798ad1aff6c143ae3a42cf506754bcb6a7ed8259c8425/kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b", size = 56022, upload-time = "2024-09-04T09:04:35.786Z" },
+ { url = "https://files.pythonhosted.org/packages/35/b3/9f75a2e06f1b4ca00b2b192bc2b739334127d27f1d0625627ff8479302ba/kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376", size = 48536, upload-time = "2024-09-04T09:04:37.525Z" },
+ { url = "https://files.pythonhosted.org/packages/97/9c/0a11c714cf8b6ef91001c8212c4ef207f772dd84540104952c45c1f0a249/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2", size = 121808, upload-time = "2024-09-04T09:04:38.637Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/d8/0fe8c5f5d35878ddd135f44f2af0e4e1d379e1c7b0716f97cdcb88d4fd27/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a", size = 65531, upload-time = "2024-09-04T09:04:39.694Z" },
+ { url = "https://files.pythonhosted.org/packages/80/c5/57fa58276dfdfa612241d640a64ca2f76adc6ffcebdbd135b4ef60095098/kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee", size = 63894, upload-time = "2024-09-04T09:04:41.6Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/e9/26d3edd4c4ad1c5b891d8747a4f81b1b0aba9fb9721de6600a4adc09773b/kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640", size = 1369296, upload-time = "2024-09-04T09:04:42.886Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/67/3f4850b5e6cffb75ec40577ddf54f7b82b15269cc5097ff2e968ee32ea7d/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f", size = 1461450, upload-time = "2024-09-04T09:04:46.284Z" },
+ { url = "https://files.pythonhosted.org/packages/52/be/86cbb9c9a315e98a8dc6b1d23c43cffd91d97d49318854f9c37b0e41cd68/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483", size = 1579168, upload-time = "2024-09-04T09:04:47.91Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/00/65061acf64bd5fd34c1f4ae53f20b43b0a017a541f242a60b135b9d1e301/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258", size = 1507308, upload-time = "2024-09-04T09:04:49.465Z" },
+ { url = "https://files.pythonhosted.org/packages/21/e4/c0b6746fd2eb62fe702118b3ca0cb384ce95e1261cfada58ff693aeec08a/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e", size = 1464186, upload-time = "2024-09-04T09:04:50.949Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/0f/529d0a9fffb4d514f2782c829b0b4b371f7f441d61aa55f1de1c614c4ef3/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107", size = 2247877, upload-time = "2024-09-04T09:04:52.388Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/e1/66603ad779258843036d45adcbe1af0d1a889a07af4635f8b4ec7dccda35/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948", size = 2404204, upload-time = "2024-09-04T09:04:54.385Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/61/de5fb1ca7ad1f9ab7970e340a5b833d735df24689047de6ae71ab9d8d0e7/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038", size = 2352461, upload-time = "2024-09-04T09:04:56.307Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/d2/0edc00a852e369827f7e05fd008275f550353f1f9bcd55db9363d779fc63/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383", size = 2501358, upload-time = "2024-09-04T09:04:57.922Z" },
+ { url = "https://files.pythonhosted.org/packages/84/15/adc15a483506aec6986c01fb7f237c3aec4d9ed4ac10b756e98a76835933/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520", size = 2314119, upload-time = "2024-09-04T09:04:59.332Z" },
+ { url = "https://files.pythonhosted.org/packages/36/08/3a5bb2c53c89660863a5aa1ee236912269f2af8762af04a2e11df851d7b2/kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b", size = 46367, upload-time = "2024-09-04T09:05:00.804Z" },
+ { url = "https://files.pythonhosted.org/packages/19/93/c05f0a6d825c643779fc3c70876bff1ac221f0e31e6f701f0e9578690d70/kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb", size = 55884, upload-time = "2024-09-04T09:05:01.924Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/f9/3828d8f21b6de4279f0667fb50a9f5215e6fe57d5ec0d61905914f5b6099/kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a", size = 48528, upload-time = "2024-09-04T09:05:02.983Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/06/7da99b04259b0f18b557a4effd1b9c901a747f7fdd84cf834ccf520cb0b2/kiwisolver-1.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e", size = 121913, upload-time = "2024-09-04T09:05:04.072Z" },
+ { url = "https://files.pythonhosted.org/packages/97/f5/b8a370d1aa593c17882af0a6f6755aaecd643640c0ed72dcfd2eafc388b9/kiwisolver-1.4.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6", size = 65627, upload-time = "2024-09-04T09:05:05.119Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/fc/6c0374f7503522539e2d4d1b497f5ebad3f8ed07ab51aed2af988dd0fb65/kiwisolver-1.4.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750", size = 63888, upload-time = "2024-09-04T09:05:06.191Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/3e/0b7172793d0f41cae5c923492da89a2ffcd1adf764c16159ca047463ebd3/kiwisolver-1.4.7-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d", size = 1369145, upload-time = "2024-09-04T09:05:07.919Z" },
+ { url = "https://files.pythonhosted.org/packages/77/92/47d050d6f6aced2d634258123f2688fbfef8ded3c5baf2c79d94d91f1f58/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379", size = 1461448, upload-time = "2024-09-04T09:05:10.01Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/1b/8f80b18e20b3b294546a1adb41701e79ae21915f4175f311a90d042301cf/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c", size = 1578750, upload-time = "2024-09-04T09:05:11.598Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/fe/fe8e72f3be0a844f257cadd72689c0848c6d5c51bc1d60429e2d14ad776e/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34", size = 1507175, upload-time = "2024-09-04T09:05:13.22Z" },
+ { url = "https://files.pythonhosted.org/packages/39/fa/cdc0b6105d90eadc3bee525fecc9179e2b41e1ce0293caaf49cb631a6aaf/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1", size = 1463963, upload-time = "2024-09-04T09:05:15.925Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/5c/0c03c4e542720c6177d4f408e56d1c8315899db72d46261a4e15b8b33a41/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f", size = 2248220, upload-time = "2024-09-04T09:05:17.434Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/ee/55ef86d5a574f4e767df7da3a3a7ff4954c996e12d4fbe9c408170cd7dcc/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b", size = 2404463, upload-time = "2024-09-04T09:05:18.997Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/6d/73ad36170b4bff4825dc588acf4f3e6319cb97cd1fb3eb04d9faa6b6f212/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27", size = 2352842, upload-time = "2024-09-04T09:05:21.299Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/16/fa531ff9199d3b6473bb4d0f47416cdb08d556c03b8bc1cccf04e756b56d/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a", size = 2501635, upload-time = "2024-09-04T09:05:23.588Z" },
+ { url = "https://files.pythonhosted.org/packages/78/7e/aa9422e78419db0cbe75fb86d8e72b433818f2e62e2e394992d23d23a583/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee", size = 2314556, upload-time = "2024-09-04T09:05:25.907Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/b2/15f7f556df0a6e5b3772a1e076a9d9f6c538ce5f05bd590eca8106508e06/kiwisolver-1.4.7-cp313-cp313-win32.whl", hash = "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07", size = 46364, upload-time = "2024-09-04T09:05:27.184Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/db/32e897e43a330eee8e4770bfd2737a9584b23e33587a0812b8e20aac38f7/kiwisolver-1.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76", size = 55887, upload-time = "2024-09-04T09:05:28.372Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/a4/df2bdca5270ca85fd25253049eb6708d4127be2ed0e5c2650217450b59e9/kiwisolver-1.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650", size = 48530, upload-time = "2024-09-04T09:05:30.225Z" },
+ { url = "https://files.pythonhosted.org/packages/11/88/37ea0ea64512997b13d69772db8dcdc3bfca5442cda3a5e4bb943652ee3e/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd", size = 122449, upload-time = "2024-09-04T09:05:55.311Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/45/5a5c46078362cb3882dcacad687c503089263c017ca1241e0483857791eb/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583", size = 65757, upload-time = "2024-09-04T09:05:56.906Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/be/a6ae58978772f685d48dd2e84460937761c53c4bbd84e42b0336473d9775/kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417", size = 64312, upload-time = "2024-09-04T09:05:58.384Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/04/18ef6f452d311e1e1eb180c9bf5589187fa1f042db877e6fe443ef10099c/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904", size = 1626966, upload-time = "2024-09-04T09:05:59.855Z" },
+ { url = "https://files.pythonhosted.org/packages/21/b1/40655f6c3fa11ce740e8a964fa8e4c0479c87d6a7944b95af799c7a55dfe/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a", size = 1607044, upload-time = "2024-09-04T09:06:02.16Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/93/af67dbcfb9b3323bbd2c2db1385a7139d8f77630e4a37bb945b57188eb2d/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8", size = 1391879, upload-time = "2024-09-04T09:06:03.908Z" },
+ { url = "https://files.pythonhosted.org/packages/40/6f/d60770ef98e77b365d96061d090c0cd9e23418121c55fff188fa4bdf0b54/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2", size = 1504751, upload-time = "2024-09-04T09:06:05.58Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/3a/5f38667d313e983c432f3fcd86932177519ed8790c724e07d77d1de0188a/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88", size = 1436990, upload-time = "2024-09-04T09:06:08.126Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/3b/1520301a47326e6a6043b502647e42892be33b3f051e9791cc8bb43f1a32/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde", size = 2191122, upload-time = "2024-09-04T09:06:10.345Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/c4/eb52da300c166239a2233f1f9c4a1b767dfab98fae27681bfb7ea4873cb6/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c", size = 2338126, upload-time = "2024-09-04T09:06:12.321Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/cb/42b92fd5eadd708dd9107c089e817945500685f3437ce1fd387efebc6d6e/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2", size = 2298313, upload-time = "2024-09-04T09:06:14.562Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/eb/be25aa791fe5fc75a8b1e0c965e00f942496bc04635c9aae8035f6b76dcd/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb", size = 2437784, upload-time = "2024-09-04T09:06:16.767Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/22/30a66be7f3368d76ff95689e1c2e28d382383952964ab15330a15d8bfd03/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327", size = 2253988, upload-time = "2024-09-04T09:06:18.705Z" },
+ { url = "https://files.pythonhosted.org/packages/35/d3/5f2ecb94b5211c8a04f218a76133cc8d6d153b0f9cd0b45fad79907f0689/kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644", size = 46980, upload-time = "2024-09-04T09:06:20.106Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/17/cd10d020578764ea91740204edc6b3236ed8106228a46f568d716b11feb2/kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4", size = 55847, upload-time = "2024-09-04T09:06:21.407Z" },
+ { url = "https://files.pythonhosted.org/packages/91/84/32232502020bd78d1d12be7afde15811c64a95ed1f606c10456db4e4c3ac/kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f", size = 48494, upload-time = "2024-09-04T09:06:22.648Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/59/741b79775d67ab67ced9bb38552da688c0305c16e7ee24bba7a2be253fb7/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643", size = 59491, upload-time = "2024-09-04T09:06:24.188Z" },
+ { url = "https://files.pythonhosted.org/packages/58/cc/fb239294c29a5656e99e3527f7369b174dd9cc7c3ef2dea7cb3c54a8737b/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706", size = 57648, upload-time = "2024-09-04T09:06:25.559Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/ef/2f009ac1f7aab9f81efb2d837301d255279d618d27b6015780115ac64bdd/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6", size = 84257, upload-time = "2024-09-04T09:06:27.038Z" },
+ { url = "https://files.pythonhosted.org/packages/81/e1/c64f50987f85b68b1c52b464bb5bf73e71570c0f7782d626d1eb283ad620/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2", size = 80906, upload-time = "2024-09-04T09:06:28.48Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/71/1687c5c0a0be2cee39a5c9c389e546f9c6e215e46b691d00d9f646892083/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4", size = 79951, upload-time = "2024-09-04T09:06:29.966Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/8b/d7497df4a1cae9367adf21665dd1f896c2a7aeb8769ad77b662c5e2bcce7/kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a", size = 55715, upload-time = "2024-09-04T09:06:31.489Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/df/ce37d9b26f07ab90880923c94d12a6ff4d27447096b4c849bfc4339ccfdf/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39", size = 58666, upload-time = "2024-09-04T09:06:43.756Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/d3/e4b04f43bc629ac8e186b77b2b1a251cdfa5b7610fa189dc0db622672ce6/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e", size = 57088, upload-time = "2024-09-04T09:06:45.406Z" },
+ { url = "https://files.pythonhosted.org/packages/30/1c/752df58e2d339e670a535514d2db4fe8c842ce459776b8080fbe08ebb98e/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608", size = 84321, upload-time = "2024-09-04T09:06:47.557Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/f8/fe6484e847bc6e238ec9f9828089fb2c0bb53f2f5f3a79351fde5b565e4f/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674", size = 80776, upload-time = "2024-09-04T09:06:49.235Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/57/d7163c0379f250ef763aba85330a19feefb5ce6cb541ade853aaba881524/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225", size = 79984, upload-time = "2024-09-04T09:06:51.336Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/95/4a103776c265d13b3d2cd24fb0494d4e04ea435a8ef97e1b2c026d43250b/kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0", size = 55811, upload-time = "2024-09-04T09:06:53.078Z" },
+]
+
+[[package]]
+name = "kiwisolver"
+version = "1.4.9"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c6/5d/8ce64e36d4e3aac5ca96996457dcf33e34e6051492399a3f1fec5657f30b/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b", size = 124159, upload-time = "2025-08-10T21:25:35.472Z" },
+ { url = "https://files.pythonhosted.org/packages/96/1e/22f63ec454874378175a5f435d6ea1363dd33fb2af832c6643e4ccea0dc8/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f", size = 66578, upload-time = "2025-08-10T21:25:36.73Z" },
+ { url = "https://files.pythonhosted.org/packages/41/4c/1925dcfff47a02d465121967b95151c82d11027d5ec5242771e580e731bd/kiwisolver-1.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf", size = 65312, upload-time = "2025-08-10T21:25:37.658Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/42/0f333164e6307a0687d1eb9ad256215aae2f4bd5d28f4653d6cd319a3ba3/kiwisolver-1.4.9-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9", size = 1628458, upload-time = "2025-08-10T21:25:39.067Z" },
+ { url = "https://files.pythonhosted.org/packages/86/b6/2dccb977d651943995a90bfe3495c2ab2ba5cd77093d9f2318a20c9a6f59/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415", size = 1225640, upload-time = "2025-08-10T21:25:40.489Z" },
+ { url = "https://files.pythonhosted.org/packages/50/2b/362ebd3eec46c850ccf2bfe3e30f2fc4c008750011f38a850f088c56a1c6/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b", size = 1244074, upload-time = "2025-08-10T21:25:42.221Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/bb/f09a1e66dab8984773d13184a10a29fe67125337649d26bdef547024ed6b/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154", size = 1293036, upload-time = "2025-08-10T21:25:43.801Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/01/11ecf892f201cafda0f68fa59212edaea93e96c37884b747c181303fccd1/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48", size = 2175310, upload-time = "2025-08-10T21:25:45.045Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/5f/bfe11d5b934f500cc004314819ea92427e6e5462706a498c1d4fc052e08f/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220", size = 2270943, upload-time = "2025-08-10T21:25:46.393Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/de/259f786bf71f1e03e73d87e2db1a9a3bcab64d7b4fd780167123161630ad/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586", size = 2440488, upload-time = "2025-08-10T21:25:48.074Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/76/c989c278faf037c4d3421ec07a5c452cd3e09545d6dae7f87c15f54e4edf/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634", size = 2246787, upload-time = "2025-08-10T21:25:49.442Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/55/c2898d84ca440852e560ca9f2a0d28e6e931ac0849b896d77231929900e7/kiwisolver-1.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611", size = 73730, upload-time = "2025-08-10T21:25:51.102Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/09/486d6ac523dd33b80b368247f238125d027964cfacb45c654841e88fb2ae/kiwisolver-1.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536", size = 65036, upload-time = "2025-08-10T21:25:52.063Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" },
+ { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" },
+ { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" },
+ { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" },
+ { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" },
+ { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" },
+ { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" },
+ { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" },
+ { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" },
+ { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" },
+ { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" },
+ { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" },
+ { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" },
+ { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" },
+ { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" },
+ { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" },
+ { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" },
+ { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" },
+ { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" },
+ { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" },
+ { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" },
+ { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" },
+ { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" },
+ { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" },
+ { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" },
+ { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" },
+ { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" },
+ { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" },
+ { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" },
+ { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" },
+ { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" },
+ { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" },
+ { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" },
+ { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" },
+ { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/63/fde392691690f55b38d5dd7b3710f5353bf7a8e52de93a22968801ab8978/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527", size = 60183, upload-time = "2025-08-10T21:27:37.669Z" },
+ { url = "https://files.pythonhosted.org/packages/27/b1/6aad34edfdb7cced27f371866f211332bba215bfd918ad3322a58f480d8b/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771", size = 58675, upload-time = "2025-08-10T21:27:39.031Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/1a/23d855a702bb35a76faed5ae2ba3de57d323f48b1f6b17ee2176c4849463/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e", size = 80277, upload-time = "2025-08-10T21:27:40.129Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/5b/5239e3c2b8fb5afa1e8508f721bb77325f740ab6994d963e61b2b7abcc1e/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9", size = 77994, upload-time = "2025-08-10T21:27:41.181Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/1c/5d4d468fb16f8410e596ed0eac02d2c68752aa7dc92997fe9d60a7147665/kiwisolver-1.4.9-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb", size = 73744, upload-time = "2025-08-10T21:27:42.254Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" },
+ { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" },
+ { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" },
+]
+
+[[package]]
+name = "mariadb"
+version = "1.1.14"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "packaging" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c4/ba/cedef19833be88e07bfff11964441cda8a998f1628dd3b2fa3e7751d36e0/mariadb-1.1.14.tar.gz", hash = "sha256:e6d702a53eccf20922e47f2f45cfb5c7a0c2c6c0a46e4ee2d8a80d0ff4a52f34", size = 111715, upload-time = "2025-10-07T06:45:48.017Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/da/bf/5e1a3a5be297c3a679e08f5359165491508fbfb64faf854dc1d626cea9c0/mariadb-1.1.14-cp310-cp310-win32.whl", hash = "sha256:4c7f33578da163a1b79929aae241f5f981d7b9d5a94d89e589aad7ec58e313ea", size = 185064, upload-time = "2025-10-07T06:45:24.858Z" },
+ { url = "https://files.pythonhosted.org/packages/31/30/3a61991c13cb8257f5db64aca12bafaa3d811d407e1fae019139fd17c99b/mariadb-1.1.14-cp310-cp310-win_amd64.whl", hash = "sha256:3f6fdc4ded5e0500a6a29bf0c8bf1be94189dcef5a8d5e0e154a4b3456f86bcc", size = 202020, upload-time = "2025-10-07T06:45:27.785Z" },
+ { url = "https://files.pythonhosted.org/packages/56/aa/a7b3c66b2792e8319ec9157d63851ff2e0b26496a05044e22b50a012a05e/mariadb-1.1.14-cp311-cp311-win32.whl", hash = "sha256:932a95016b7e9b8d78893aa5ee608e74199e3c6dd607dbe5e4da2010a4f67b88", size = 185061, upload-time = "2025-10-07T06:45:29.964Z" },
+ { url = "https://files.pythonhosted.org/packages/54/04/ea2374867756b4082764484bc8b82e1798d94f171bcc914e08c60d640f8f/mariadb-1.1.14-cp311-cp311-win_amd64.whl", hash = "sha256:55ddbe5272c292cbcb2968d87681b5d2b327e65646a015e324b8eeb804d14531", size = 202016, upload-time = "2025-10-07T06:45:32.151Z" },
+ { url = "https://files.pythonhosted.org/packages/00/04/659a8d30513700b5921ec96bddc07f550016c045fcbeb199d8cd18476ecc/mariadb-1.1.14-cp312-cp312-win32.whl", hash = "sha256:98d552a8bb599eceaa88f65002ad00bd88aeed160592c273a7e5c1d79ab733dd", size = 185266, upload-time = "2025-10-07T06:45:34.164Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/a9/8f210291bc5fc044e20497454f40d35b3bab326e2cab6fccdc38121cb2c1/mariadb-1.1.14-cp312-cp312-win_amd64.whl", hash = "sha256:685a1ad2a24fd0aae1c4416fe0ac794adc84ab9209c8d0c57078f770d39731db", size = 202112, upload-time = "2025-10-07T06:45:35.824Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/42/51130048bcce038bb859978250515f1aad90e9c4d273630a704e0a8b1ae1/mariadb-1.1.14-cp313-cp313-win32.whl", hash = "sha256:3d2c795cde606f4e12c0d73282b062433f414cae035675b0d81f2d65c9b79ac5", size = 185221, upload-time = "2025-10-07T06:45:37.848Z" },
+ { url = "https://files.pythonhosted.org/packages/af/23/e952a7e442913abd8079cd27b80b69474895c93b3727fad41c7642a80c62/mariadb-1.1.14-cp313-cp313-win_amd64.whl", hash = "sha256:7fd603c5cf23c47ef0d28fdc2b4b79919ee7f75d00ed070d3cd1054dcf816aeb", size = 202121, upload-time = "2025-10-07T06:45:39.453Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/f2/7059f83543a4264b98777d7cc8aa203e1ca6a13a461f730d1c97f29628d4/mariadb-1.1.14-cp314-cp314-win32.whl", hash = "sha256:1a50b4612c0dd5b69690cebb34cef552a7f64dcadeb5aa91d70cd99bf01bc5b3", size = 190620, upload-time = "2025-10-07T06:45:41.349Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/7c/7e094b0b396d742494f6346f2ffa9709e429970b0461aca50526f5f02f12/mariadb-1.1.14-cp314-cp314-win_amd64.whl", hash = "sha256:6659725837e48fa6af05e20128fb525029f706f1921d5dbf639a25b2f80b9f93", size = 206263, upload-time = "2025-10-07T06:45:43.227Z" },
+ { url = "https://files.pythonhosted.org/packages/91/bd/264ecaac1ca13a3f3c01f51b23473e76037a94dd94cc6fcbfa80b43543cd/mariadb-1.1.14-cp39-cp39-win32.whl", hash = "sha256:0f5fc74023f2e479be159542633f8b5865fee18a36e5a6d4e262387b39a692ee", size = 187615, upload-time = "2025-10-07T06:45:44.905Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/64/9ab6eb812a3130700b790857f46a7a2a103c33b0c2c9bfd0952c3abad1f2/mariadb-1.1.14-cp39-cp39-win_amd64.whl", hash = "sha256:5b514362ba3ad3ef7ada91bc8a8b3b4c0e5144efce96b5bffa3dbc46b8af7d7a", size = 204631, upload-time = "2025-10-07T06:45:46.769Z" },
+]
+
[[package]]
name = "markupsafe"
version = "3.0.2"
@@ -340,6 +1100,149 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
]
+[[package]]
+name = "matplotlib"
+version = "3.9.4"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "contourpy", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "cycler", marker = "python_full_version < '3.10'" },
+ { name = "fonttools", marker = "python_full_version < '3.10'" },
+ { name = "importlib-resources", marker = "python_full_version < '3.10'" },
+ { name = "kiwisolver", version = "1.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "packaging", marker = "python_full_version < '3.10'" },
+ { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "pyparsing", marker = "python_full_version < '3.10'" },
+ { name = "python-dateutil", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/17/1747b4154034befd0ed33b52538f5eb7752d05bb51c5e2a31470c3bc7d52/matplotlib-3.9.4.tar.gz", hash = "sha256:1e00e8be7393cbdc6fedfa8a6fba02cf3e83814b285db1c60b906a023ba41bc3", size = 36106529, upload-time = "2024-12-13T05:56:34.184Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/94/27d2e2c30d54b56c7b764acc1874a909e34d1965a427fc7092bb6a588b63/matplotlib-3.9.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fdd7abfb706dfa8d307af64a87f1a862879ec3cd8d0ec8637458f0885b9c50", size = 7885089, upload-time = "2024-12-13T05:54:24.224Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/25/828273307e40a68eb8e9df832b6b2aaad075864fdc1de4b1b81e40b09e48/matplotlib-3.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d89bc4e85e40a71d1477780366c27fb7c6494d293e1617788986f74e2a03d7ff", size = 7770600, upload-time = "2024-12-13T05:54:27.214Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/65/f841a422ec994da5123368d76b126acf4fc02ea7459b6e37c4891b555b83/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddf9f3c26aae695c5daafbf6b94e4c1a30d6cd617ba594bbbded3b33a1fcfa26", size = 8200138, upload-time = "2024-12-13T05:54:29.497Z" },
+ { url = "https://files.pythonhosted.org/packages/07/06/272aca07a38804d93b6050813de41ca7ab0e29ba7a9dd098e12037c919a9/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18ebcf248030173b59a868fda1fe42397253f6698995b55e81e1f57431d85e50", size = 8312711, upload-time = "2024-12-13T05:54:34.396Z" },
+ { url = "https://files.pythonhosted.org/packages/98/37/f13e23b233c526b7e27ad61be0a771894a079e0f7494a10d8d81557e0e9a/matplotlib-3.9.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974896ec43c672ec23f3f8c648981e8bc880ee163146e0312a9b8def2fac66f5", size = 9090622, upload-time = "2024-12-13T05:54:36.808Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/8c/b1f5bd2bd70e60f93b1b54c4d5ba7a992312021d0ddddf572f9a1a6d9348/matplotlib-3.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:4598c394ae9711cec135639374e70871fa36b56afae17bdf032a345be552a88d", size = 7828211, upload-time = "2024-12-13T05:54:40.596Z" },
+ { url = "https://files.pythonhosted.org/packages/74/4b/65be7959a8fa118a3929b49a842de5b78bb55475236fcf64f3e308ff74a0/matplotlib-3.9.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d4dd29641d9fb8bc4492420c5480398dd40a09afd73aebe4eb9d0071a05fbe0c", size = 7894430, upload-time = "2024-12-13T05:54:44.049Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/18/80f70d91896e0a517b4a051c3fd540daa131630fd75e02e250365353b253/matplotlib-3.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30e5b22e8bcfb95442bf7d48b0d7f3bdf4a450cbf68986ea45fca3d11ae9d099", size = 7780045, upload-time = "2024-12-13T05:54:46.414Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/73/ccb381026e3238c5c25c3609ba4157b2d1a617ec98d65a8b4ee4e1e74d02/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bb0030d1d447fd56dcc23b4c64a26e44e898f0416276cac1ebc25522e0ac249", size = 8209906, upload-time = "2024-12-13T05:54:49.459Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/33/1648da77b74741c89f5ea95cbf42a291b4b364f2660b316318811404ed97/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aca90ed222ac3565d2752b83dbb27627480d27662671e4d39da72e97f657a423", size = 8322873, upload-time = "2024-12-13T05:54:53.066Z" },
+ { url = "https://files.pythonhosted.org/packages/57/d3/8447ba78bc6593c9044c372d1609f8ea10fb1e071e7a9e0747bea74fc16c/matplotlib-3.9.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a181b2aa2906c608fcae72f977a4a2d76e385578939891b91c2550c39ecf361e", size = 9099566, upload-time = "2024-12-13T05:54:55.522Z" },
+ { url = "https://files.pythonhosted.org/packages/23/e1/4f0e237bf349c02ff9d1b6e7109f1a17f745263809b9714a8576dc17752b/matplotlib-3.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:1f6882828231eca17f501c4dcd98a05abb3f03d157fbc0769c6911fe08b6cfd3", size = 7838065, upload-time = "2024-12-13T05:54:58.337Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/2b/c918bf6c19d6445d1cefe3d2e42cb740fb997e14ab19d4daeb6a7ab8a157/matplotlib-3.9.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dfc48d67e6661378a21c2983200a654b72b5c5cdbd5d2cf6e5e1ece860f0cc70", size = 7891131, upload-time = "2024-12-13T05:55:02.837Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/e5/b4e8fc601ca302afeeabf45f30e706a445c7979a180e3a978b78b2b681a4/matplotlib-3.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47aef0fab8332d02d68e786eba8113ffd6f862182ea2999379dec9e237b7e483", size = 7776365, upload-time = "2024-12-13T05:55:05.158Z" },
+ { url = "https://files.pythonhosted.org/packages/99/06/b991886c506506476e5d83625c5970c656a491b9f80161458fed94597808/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fba1f52c6b7dc764097f52fd9ab627b90db452c9feb653a59945de16752e965f", size = 8200707, upload-time = "2024-12-13T05:55:09.48Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/e2/556b627498cb27e61026f2d1ba86a78ad1b836fef0996bef5440e8bc9559/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:173ac3748acaac21afcc3fa1633924609ba1b87749006bc25051c52c422a5d00", size = 8313761, upload-time = "2024-12-13T05:55:12.95Z" },
+ { url = "https://files.pythonhosted.org/packages/58/ff/165af33ec766ff818306ea88e91f9f60d2a6ed543be1eb122a98acbf3b0d/matplotlib-3.9.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320edea0cadc07007765e33f878b13b3738ffa9745c5f707705692df70ffe0e0", size = 9095284, upload-time = "2024-12-13T05:55:16.199Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/8b/3d0c7a002db3b1ed702731c2a9a06d78d035f1f2fb0fb936a8e43cc1e9f4/matplotlib-3.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a4a4cfc82330b27042a7169533da7991e8789d180dd5b3daeaee57d75cd5a03b", size = 7841160, upload-time = "2024-12-13T05:55:19.991Z" },
+ { url = "https://files.pythonhosted.org/packages/49/b1/999f89a7556d101b23a2f0b54f1b6e140d73f56804da1398f2f0bc0924bc/matplotlib-3.9.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37eeffeeca3c940985b80f5b9a7b95ea35671e0e7405001f249848d2b62351b6", size = 7891499, upload-time = "2024-12-13T05:55:22.142Z" },
+ { url = "https://files.pythonhosted.org/packages/87/7b/06a32b13a684977653396a1bfcd34d4e7539c5d55c8cbfaa8ae04d47e4a9/matplotlib-3.9.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3e7465ac859ee4abcb0d836137cd8414e7bb7ad330d905abced457217d4f0f45", size = 7776802, upload-time = "2024-12-13T05:55:25.947Z" },
+ { url = "https://files.pythonhosted.org/packages/65/87/ac498451aff739e515891bbb92e566f3c7ef31891aaa878402a71f9b0910/matplotlib-3.9.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4c12302c34afa0cf061bea23b331e747e5e554b0fa595c96e01c7b75bc3b858", size = 8200802, upload-time = "2024-12-13T05:55:28.461Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/6b/9eb761c00e1cb838f6c92e5f25dcda3f56a87a52f6cb8fdfa561e6cf6a13/matplotlib-3.9.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b8c97917f21b75e72108b97707ba3d48f171541a74aa2a56df7a40626bafc64", size = 8313880, upload-time = "2024-12-13T05:55:30.965Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/a2/c8eaa600e2085eec7e38cbbcc58a30fc78f8224939d31d3152bdafc01fd1/matplotlib-3.9.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0229803bd7e19271b03cb09f27db76c918c467aa4ce2ae168171bc67c3f508df", size = 9094637, upload-time = "2024-12-13T05:55:33.701Z" },
+ { url = "https://files.pythonhosted.org/packages/71/1f/c6e1daea55b7bfeb3d84c6cb1abc449f6a02b181e7e2a5e4db34c3afb793/matplotlib-3.9.4-cp313-cp313-win_amd64.whl", hash = "sha256:7c0d8ef442ebf56ff5e206f8083d08252ee738e04f3dc88ea882853a05488799", size = 7841311, upload-time = "2024-12-13T05:55:36.737Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/3a/2757d3f7d388b14dd48f5a83bea65b6d69f000e86b8f28f74d86e0d375bd/matplotlib-3.9.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a04c3b00066a688834356d196136349cb32f5e1003c55ac419e91585168b88fb", size = 7919989, upload-time = "2024-12-13T05:55:39.024Z" },
+ { url = "https://files.pythonhosted.org/packages/24/28/f5077c79a4f521589a37fe1062d6a6ea3534e068213f7357e7cfffc2e17a/matplotlib-3.9.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04c519587f6c210626741a1e9a68eefc05966ede24205db8982841826af5871a", size = 7809417, upload-time = "2024-12-13T05:55:42.412Z" },
+ { url = "https://files.pythonhosted.org/packages/36/c8/c523fd2963156692916a8eb7d4069084cf729359f7955cf09075deddfeaf/matplotlib-3.9.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308afbf1a228b8b525fcd5cec17f246bbbb63b175a3ef6eb7b4d33287ca0cf0c", size = 8226258, upload-time = "2024-12-13T05:55:47.259Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/88/499bf4b8fa9349b6f5c0cf4cead0ebe5da9d67769129f1b5651e5ac51fbc/matplotlib-3.9.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb3b02246ddcffd3ce98e88fed5b238bc5faff10dbbaa42090ea13241d15764", size = 8335849, upload-time = "2024-12-13T05:55:49.763Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/9f/20a4156b9726188646a030774ee337d5ff695a965be45ce4dbcb9312c170/matplotlib-3.9.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8a75287e9cb9eee48cb79ec1d806f75b29c0fde978cb7223a1f4c5848d696041", size = 9102152, upload-time = "2024-12-13T05:55:51.997Z" },
+ { url = "https://files.pythonhosted.org/packages/10/11/237f9c3a4e8d810b1759b67ff2da7c32c04f9c80aa475e7beb36ed43a8fb/matplotlib-3.9.4-cp313-cp313t-win_amd64.whl", hash = "sha256:488deb7af140f0ba86da003e66e10d55ff915e152c78b4b66d231638400b1965", size = 7896987, upload-time = "2024-12-13T05:55:55.941Z" },
+ { url = "https://files.pythonhosted.org/packages/56/eb/501b465c9fef28f158e414ea3a417913dc2ac748564c7ed41535f23445b4/matplotlib-3.9.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3c3724d89a387ddf78ff88d2a30ca78ac2b4c89cf37f2db4bd453c34799e933c", size = 7885919, upload-time = "2024-12-13T05:55:59.66Z" },
+ { url = "https://files.pythonhosted.org/packages/da/36/236fbd868b6c91309a5206bd90c3f881f4f44b2d997cd1d6239ef652f878/matplotlib-3.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d5f0a8430ffe23d7e32cfd86445864ccad141797f7d25b7c41759a5b5d17cfd7", size = 7771486, upload-time = "2024-12-13T05:56:04.264Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/4b/105caf2d54d5ed11d9f4335398f5103001a03515f2126c936a752ccf1461/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bb0141a21aef3b64b633dc4d16cbd5fc538b727e4958be82a0e1c92a234160e", size = 8201838, upload-time = "2024-12-13T05:56:06.792Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/a7/bb01188fb4013d34d274caf44a2f8091255b0497438e8b6c0a7c1710c692/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57aa235109e9eed52e2c2949db17da185383fa71083c00c6c143a60e07e0888c", size = 8314492, upload-time = "2024-12-13T05:56:09.964Z" },
+ { url = "https://files.pythonhosted.org/packages/33/19/02e1a37f7141fc605b193e927d0a9cdf9dc124a20b9e68793f4ffea19695/matplotlib-3.9.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b18c600061477ccfdd1e6fd050c33d8be82431700f3452b297a56d9ed7037abb", size = 9092500, upload-time = "2024-12-13T05:56:13.55Z" },
+ { url = "https://files.pythonhosted.org/packages/57/68/c2feb4667adbf882ffa4b3e0ac9967f848980d9f8b5bebd86644aa67ce6a/matplotlib-3.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:ef5f2d1b67d2d2145ff75e10f8c008bfbf71d45137c4b648c87193e7dd053eac", size = 7822962, upload-time = "2024-12-13T05:56:16.358Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/22/2ef6a364cd3f565442b0b055e0599744f1e4314ec7326cdaaa48a4d864d7/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:44e0ed786d769d85bc787b0606a53f2d8d2d1d3c8a2608237365e9121c1a338c", size = 7877995, upload-time = "2024-12-13T05:56:18.805Z" },
+ { url = "https://files.pythonhosted.org/packages/87/b8/2737456e566e9f4d94ae76b8aa0d953d9acb847714f9a7ad80184474f5be/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:09debb9ce941eb23ecdbe7eab972b1c3e0276dcf01688073faff7b0f61d6c6ca", size = 7769300, upload-time = "2024-12-13T05:56:21.315Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/1f/e709c6ec7b5321e6568769baa288c7178e60a93a9da9e682b39450da0e29/matplotlib-3.9.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc53cf157a657bfd03afab14774d54ba73aa84d42cfe2480c91bd94873952db", size = 8313423, upload-time = "2024-12-13T05:56:26.719Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/b6/5a1f868782cd13f053a679984e222007ecff654a9bfbac6b27a65f4eeb05/matplotlib-3.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ad45da51be7ad02387801fd154ef74d942f49fe3fcd26a64c94842ba7ec0d865", size = 7854624, upload-time = "2024-12-13T05:56:29.359Z" },
+]
+
+[[package]]
+name = "matplotlib"
+version = "3.10.7"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+ { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "cycler", marker = "python_full_version >= '3.10'" },
+ { name = "fonttools", marker = "python_full_version >= '3.10'" },
+ { name = "kiwisolver", version = "1.4.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+ { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "packaging", marker = "python_full_version >= '3.10'" },
+ { name = "pillow", version = "12.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "pyparsing", marker = "python_full_version >= '3.10'" },
+ { name = "python-dateutil", marker = "python_full_version >= '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ae/e2/d2d5295be2f44c678ebaf3544ba32d20c1f9ef08c49fe47f496180e1db15/matplotlib-3.10.7.tar.gz", hash = "sha256:a06ba7e2a2ef9131c79c49e63dad355d2d878413a0376c1727c8b9335ff731c7", size = 34804865, upload-time = "2025-10-09T00:28:00.669Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6c/87/3932d5778ab4c025db22710b61f49ccaed3956c5cf46ffb2ffa7492b06d9/matplotlib-3.10.7-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7ac81eee3b7c266dd92cee1cd658407b16c57eed08c7421fa354ed68234de380", size = 8247141, upload-time = "2025-10-09T00:26:06.023Z" },
+ { url = "https://files.pythonhosted.org/packages/45/a8/bfed45339160102bce21a44e38a358a1134a5f84c26166de03fb4a53208f/matplotlib-3.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:667ecd5d8d37813a845053d8f5bf110b534c3c9f30e69ebd25d4701385935a6d", size = 8107995, upload-time = "2025-10-09T00:26:08.669Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/3c/5692a2d9a5ba848fda3f48d2b607037df96460b941a59ef236404b39776b/matplotlib-3.10.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc1c51b846aca49a5a8b44fbba6a92d583a35c64590ad9e1e950dc88940a4297", size = 8680503, upload-time = "2025-10-09T00:26:10.607Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/a0/86ace53c48b05d0e6e9c127b2ace097434901f3e7b93f050791c8243201a/matplotlib-3.10.7-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a11c2e9e72e7de09b7b72e62f3df23317c888299c875e2b778abf1eda8c0a42", size = 9514982, upload-time = "2025-10-09T00:26:12.594Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/81/ead71e2824da8f72640a64166d10e62300df4ae4db01a0bac56c5b39fa51/matplotlib-3.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f19410b486fdd139885ace124e57f938c1e6a3210ea13dd29cab58f5d4bc12c7", size = 9566429, upload-time = "2025-10-09T00:26:14.758Z" },
+ { url = "https://files.pythonhosted.org/packages/65/7d/954b3067120456f472cce8fdcacaf4a5fcd522478db0c37bb243c7cb59dd/matplotlib-3.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:b498e9e4022f93de2d5a37615200ca01297ceebbb56fe4c833f46862a490f9e3", size = 8108174, upload-time = "2025-10-09T00:26:17.015Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/bc/0fb489005669127ec13f51be0c6adc074d7cf191075dab1da9fe3b7a3cfc/matplotlib-3.10.7-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:53b492410a6cd66c7a471de6c924f6ede976e963c0f3097a3b7abfadddc67d0a", size = 8257507, upload-time = "2025-10-09T00:26:19.073Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/6a/d42588ad895279ff6708924645b5d2ed54a7fb2dc045c8a804e955aeace1/matplotlib-3.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d9749313deb729f08207718d29c86246beb2ea3fdba753595b55901dee5d2fd6", size = 8119565, upload-time = "2025-10-09T00:26:21.023Z" },
+ { url = "https://files.pythonhosted.org/packages/10/b7/4aa196155b4d846bd749cf82aa5a4c300cf55a8b5e0dfa5b722a63c0f8a0/matplotlib-3.10.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2222c7ba2cbde7fe63032769f6eb7e83ab3227f47d997a8453377709b7fe3a5a", size = 8692668, upload-time = "2025-10-09T00:26:22.967Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/e7/664d2b97016f46683a02d854d730cfcf54ff92c1dafa424beebef50f831d/matplotlib-3.10.7-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e91f61a064c92c307c5a9dc8c05dc9f8a68f0a3be199d9a002a0622e13f874a1", size = 9521051, upload-time = "2025-10-09T00:26:25.041Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/a3/37aef1404efa615f49b5758a5e0261c16dd88f389bc1861e722620e4a754/matplotlib-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f1851eab59ca082c95df5a500106bad73672645625e04538b3ad0f69471ffcc", size = 9576878, upload-time = "2025-10-09T00:26:27.478Z" },
+ { url = "https://files.pythonhosted.org/packages/33/cd/b145f9797126f3f809d177ca378de57c45413c5099c5990de2658760594a/matplotlib-3.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:6516ce375109c60ceec579e699524e9d504cd7578506f01150f7a6bc174a775e", size = 8115142, upload-time = "2025-10-09T00:26:29.774Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/39/63bca9d2b78455ed497fcf51a9c71df200a11048f48249038f06447fa947/matplotlib-3.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:b172db79759f5f9bc13ef1c3ef8b9ee7b37b0247f987fbbbdaa15e4f87fd46a9", size = 7992439, upload-time = "2025-10-09T00:26:40.32Z" },
+ { url = "https://files.pythonhosted.org/packages/be/b3/09eb0f7796932826ec20c25b517d568627754f6c6462fca19e12c02f2e12/matplotlib-3.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a0edb7209e21840e8361e91ea84ea676658aa93edd5f8762793dec77a4a6748", size = 8272389, upload-time = "2025-10-09T00:26:42.474Z" },
+ { url = "https://files.pythonhosted.org/packages/11/0b/1ae80ddafb8652fd8046cb5c8460ecc8d4afccb89e2c6d6bec61e04e1eaf/matplotlib-3.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c380371d3c23e0eadf8ebff114445b9f970aff2010198d498d4ab4c3b41eea4f", size = 8128247, upload-time = "2025-10-09T00:26:44.77Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/18/95ae2e242d4a5c98bd6e90e36e128d71cf1c7e39b0874feaed3ef782e789/matplotlib-3.10.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d5f256d49fea31f40f166a5e3131235a5d2f4b7f44520b1cf0baf1ce568ccff0", size = 8696996, upload-time = "2025-10-09T00:26:46.792Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/3d/5b559efc800bd05cb2033aa85f7e13af51958136a48327f7c261801ff90a/matplotlib-3.10.7-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11ae579ac83cdf3fb72573bb89f70e0534de05266728740d478f0f818983c695", size = 9530153, upload-time = "2025-10-09T00:26:49.07Z" },
+ { url = "https://files.pythonhosted.org/packages/88/57/eab4a719fd110312d3c220595d63a3c85ec2a39723f0f4e7fa7e6e3f74ba/matplotlib-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4c14b6acd16cddc3569a2d515cfdd81c7a68ac5639b76548cfc1a9e48b20eb65", size = 9593093, upload-time = "2025-10-09T00:26:51.067Z" },
+ { url = "https://files.pythonhosted.org/packages/31/3c/80816f027b3a4a28cd2a0a6ef7f89a2db22310e945cd886ec25bfb399221/matplotlib-3.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:0d8c32b7ea6fb80b1aeff5a2ceb3fb9778e2759e899d9beff75584714afcc5ee", size = 8122771, upload-time = "2025-10-09T00:26:53.296Z" },
+ { url = "https://files.pythonhosted.org/packages/de/77/ef1fc78bfe99999b2675435cc52120887191c566b25017d78beaabef7f2d/matplotlib-3.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:5f3f6d315dcc176ba7ca6e74c7768fb7e4cf566c49cb143f6bc257b62e634ed8", size = 7992812, upload-time = "2025-10-09T00:26:54.882Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9c/207547916a02c78f6bdd83448d9b21afbc42f6379ed887ecf610984f3b4e/matplotlib-3.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1d9d3713a237970569156cfb4de7533b7c4eacdd61789726f444f96a0d28f57f", size = 8273212, upload-time = "2025-10-09T00:26:56.752Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/d0/b3d3338d467d3fc937f0bb7f256711395cae6f78e22cef0656159950adf0/matplotlib-3.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37a1fea41153dd6ee061d21ab69c9cf2cf543160b1b85d89cd3d2e2a7902ca4c", size = 8128713, upload-time = "2025-10-09T00:26:59.001Z" },
+ { url = "https://files.pythonhosted.org/packages/22/ff/6425bf5c20d79aa5b959d1ce9e65f599632345391381c9a104133fe0b171/matplotlib-3.10.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b3c4ea4948d93c9c29dc01c0c23eef66f2101bf75158c291b88de6525c55c3d1", size = 8698527, upload-time = "2025-10-09T00:27:00.69Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/7f/ccdca06f4c2e6c7989270ed7829b8679466682f4cfc0f8c9986241c023b6/matplotlib-3.10.7-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22df30ffaa89f6643206cf13877191c63a50e8f800b038bc39bee9d2d4957632", size = 9529690, upload-time = "2025-10-09T00:27:02.664Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/95/b80fc2c1f269f21ff3d193ca697358e24408c33ce2b106a7438a45407b63/matplotlib-3.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b69676845a0a66f9da30e87f48be36734d6748024b525ec4710be40194282c84", size = 9593732, upload-time = "2025-10-09T00:27:04.653Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/b6/23064a96308b9aeceeffa65e96bcde459a2ea4934d311dee20afde7407a0/matplotlib-3.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:744991e0cc863dd669c8dc9136ca4e6e0082be2070b9d793cbd64bec872a6815", size = 8122727, upload-time = "2025-10-09T00:27:06.814Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/a6/2faaf48133b82cf3607759027f82b5c702aa99cdfcefb7f93d6ccf26a424/matplotlib-3.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:fba2974df0bf8ce3c995fa84b79cde38326e0f7b5409e7a3a481c1141340bcf7", size = 7992958, upload-time = "2025-10-09T00:27:08.567Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/f0/b018fed0b599bd48d84c08794cb242227fe3341952da102ee9d9682db574/matplotlib-3.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:932c55d1fa7af4423422cb6a492a31cbcbdbe68fd1a9a3f545aa5e7a143b5355", size = 8316849, upload-time = "2025-10-09T00:27:10.254Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/b7/bb4f23856197659f275e11a2a164e36e65e9b48ea3e93c4ec25b4f163198/matplotlib-3.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e38c2d581d62ee729a6e144c47a71b3f42fb4187508dbbf4fe71d5612c3433b", size = 8178225, upload-time = "2025-10-09T00:27:12.241Z" },
+ { url = "https://files.pythonhosted.org/packages/62/56/0600609893ff277e6f3ab3c0cef4eafa6e61006c058e84286c467223d4d5/matplotlib-3.10.7-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:786656bb13c237bbcebcd402f65f44dd61ead60ee3deb045af429d889c8dbc67", size = 8711708, upload-time = "2025-10-09T00:27:13.879Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/1a/6bfecb0cafe94d6658f2f1af22c43b76cf7a1c2f0dc34ef84cbb6809617e/matplotlib-3.10.7-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09d7945a70ea43bf9248f4b6582734c2fe726723204a76eca233f24cffc7ef67", size = 9541409, upload-time = "2025-10-09T00:27:15.684Z" },
+ { url = "https://files.pythonhosted.org/packages/08/50/95122a407d7f2e446fd865e2388a232a23f2b81934960ea802f3171518e4/matplotlib-3.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d0b181e9fa8daf1d9f2d4c547527b167cb8838fc587deabca7b5c01f97199e84", size = 9594054, upload-time = "2025-10-09T00:27:17.547Z" },
+ { url = "https://files.pythonhosted.org/packages/13/76/75b194a43b81583478a81e78a07da8d9ca6ddf50dd0a2ccabf258059481d/matplotlib-3.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:31963603041634ce1a96053047b40961f7a29eb8f9a62e80cc2c0427aa1d22a2", size = 8200100, upload-time = "2025-10-09T00:27:20.039Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/9e/6aefebdc9f8235c12bdeeda44cc0383d89c1e41da2c400caf3ee2073a3ce/matplotlib-3.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:aebed7b50aa6ac698c90f60f854b47e48cd2252b30510e7a1feddaf5a3f72cbf", size = 8042131, upload-time = "2025-10-09T00:27:21.608Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/4b/e5bc2c321b6a7e3a75638d937d19ea267c34bd5a90e12bee76c4d7c7a0d9/matplotlib-3.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d883460c43e8c6b173fef244a2341f7f7c0e9725c7fe68306e8e44ed9c8fb100", size = 8273787, upload-time = "2025-10-09T00:27:23.27Z" },
+ { url = "https://files.pythonhosted.org/packages/86/ad/6efae459c56c2fbc404da154e13e3a6039129f3c942b0152624f1c621f05/matplotlib-3.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07124afcf7a6504eafcb8ce94091c5898bbdd351519a1beb5c45f7a38c67e77f", size = 8131348, upload-time = "2025-10-09T00:27:24.926Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/5a/a4284d2958dee4116359cc05d7e19c057e64ece1b4ac986ab0f2f4d52d5a/matplotlib-3.10.7-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c17398b709a6cce3d9fdb1595c33e356d91c098cd9486cb2cc21ea2ea418e715", size = 9533949, upload-time = "2025-10-09T00:27:26.704Z" },
+ { url = "https://files.pythonhosted.org/packages/de/ff/f3781b5057fa3786623ad8976fc9f7b0d02b2f28534751fd5a44240de4cf/matplotlib-3.10.7-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7146d64f561498764561e9cd0ed64fcf582e570fc519e6f521e2d0cfd43365e1", size = 9804247, upload-time = "2025-10-09T00:27:28.514Z" },
+ { url = "https://files.pythonhosted.org/packages/47/5a/993a59facb8444efb0e197bf55f545ee449902dcee86a4dfc580c3b61314/matplotlib-3.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90ad854c0a435da3104c01e2c6f0028d7e719b690998a2333d7218db80950722", size = 9595497, upload-time = "2025-10-09T00:27:30.418Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/a5/77c95aaa9bb32c345cbb49626ad8eb15550cba2e6d4c88081a6c2ac7b08d/matplotlib-3.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:4645fc5d9d20ffa3a39361fcdbcec731382763b623b72627806bf251b6388866", size = 8252732, upload-time = "2025-10-09T00:27:32.332Z" },
+ { url = "https://files.pythonhosted.org/packages/74/04/45d269b4268d222390d7817dae77b159651909669a34ee9fdee336db5883/matplotlib-3.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:9257be2f2a03415f9105c486d304a321168e61ad450f6153d77c69504ad764bb", size = 8124240, upload-time = "2025-10-09T00:27:33.94Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/c7/ca01c607bb827158b439208c153d6f14ddb9fb640768f06f7ca3488ae67b/matplotlib-3.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1e4bbad66c177a8fdfa53972e5ef8be72a5f27e6a607cec0d8579abd0f3102b1", size = 8316938, upload-time = "2025-10-09T00:27:35.534Z" },
+ { url = "https://files.pythonhosted.org/packages/84/d2/5539e66e9f56d2fdec94bb8436f5e449683b4e199bcc897c44fbe3c99e28/matplotlib-3.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d8eb7194b084b12feb19142262165832fc6ee879b945491d1c3d4660748020c4", size = 8178245, upload-time = "2025-10-09T00:27:37.334Z" },
+ { url = "https://files.pythonhosted.org/packages/77/b5/e6ca22901fd3e4fe433a82e583436dd872f6c966fca7e63cf806b40356f8/matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d41379b05528091f00e1728004f9a8d7191260f3862178b88e8fd770206318", size = 9541411, upload-time = "2025-10-09T00:27:39.387Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/99/a4524db57cad8fee54b7237239a8f8360bfcfa3170d37c9e71c090c0f409/matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a74f79fafb2e177f240579bc83f0b60f82cc47d2f1d260f422a0627207008ca", size = 9803664, upload-time = "2025-10-09T00:27:41.492Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/a5/85e2edf76ea0ad4288d174926d9454ea85f3ce5390cc4e6fab196cbf250b/matplotlib-3.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:702590829c30aada1e8cef0568ddbffa77ca747b4d6e36c6d173f66e301f89cc", size = 9594066, upload-time = "2025-10-09T00:27:43.694Z" },
+ { url = "https://files.pythonhosted.org/packages/39/69/9684368a314f6d83fe5c5ad2a4121a3a8e03723d2e5c8ea17b66c1bad0e7/matplotlib-3.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:f79d5de970fc90cd5591f60053aecfce1fcd736e0303d9f0bf86be649fa68fb8", size = 8342832, upload-time = "2025-10-09T00:27:45.543Z" },
+ { url = "https://files.pythonhosted.org/packages/04/5f/e22e08da14bc1a0894184640d47819d2338b792732e20d292bf86e5ab785/matplotlib-3.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:cb783436e47fcf82064baca52ce748af71725d0352e1d31564cbe9c95df92b9c", size = 8172585, upload-time = "2025-10-09T00:27:47.185Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/6c/a9bcf03e9afb2a873e0a5855f79bce476d1023f26f8212969f2b7504756c/matplotlib-3.10.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5c09cf8f2793f81368f49f118b6f9f937456362bee282eac575cca7f84cda537", size = 8241204, upload-time = "2025-10-09T00:27:48.806Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/fd/0e6f5aa762ed689d9fa8750b08f1932628ffa7ed30e76423c399d19407d2/matplotlib-3.10.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:de66744b2bb88d5cd27e80dfc2ec9f0517d0a46d204ff98fe9e5f2864eb67657", size = 8104607, upload-time = "2025-10-09T00:27:50.876Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/a9/21c9439d698fac5f0de8fc68b2405b738ed1f00e1279c76f2d9aa5521ead/matplotlib-3.10.7-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:53cc80662dd197ece414dd5b66e07370201515a3eaf52e7c518c68c16814773b", size = 8682257, upload-time = "2025-10-09T00:27:52.597Z" },
+ { url = "https://files.pythonhosted.org/packages/58/8f/76d5dc21ac64a49e5498d7f0472c0781dae442dd266a67458baec38288ec/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:15112bcbaef211bd663fa935ec33313b948e214454d949b723998a43357b17b0", size = 8252283, upload-time = "2025-10-09T00:27:54.739Z" },
+ { url = "https://files.pythonhosted.org/packages/27/0d/9c5d4c2317feb31d819e38c9f947c942f42ebd4eb935fc6fd3518a11eaa7/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d2a959c640cdeecdd2ec3136e8ea0441da59bcaf58d67e9c590740addba2cb68", size = 8116733, upload-time = "2025-10-09T00:27:56.406Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/cc/3fe688ff1355010937713164caacf9ed443675ac48a997bab6ed23b3f7c0/matplotlib-3.10.7-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3886e47f64611046bc1db523a09dd0a0a6bed6081e6f90e13806dd1d1d1b5e91", size = 8693919, upload-time = "2025-10-09T00:27:58.41Z" },
+]
+
[[package]]
name = "mypy"
version = "1.16.0"
@@ -388,16 +1291,91 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
+[[package]]
+name = "mysql-connector-python"
+version = "9.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/02/77/2b45e6460d05b1f1b7a4c8eb79a50440b4417971973bb78c9ef6cad630a6/mysql_connector_python-9.4.0.tar.gz", hash = "sha256:d111360332ae78933daf3d48ff497b70739aa292ab0017791a33e826234e743b", size = 12185532, upload-time = "2025-07-22T08:02:05.788Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a2/ef/1a35d9ebfaf80cf5aa238be471480e16a69a494d276fb07b889dc9a5cfc3/mysql_connector_python-9.4.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:3c2603e00516cf4208c6266e85c5c87d5f4d0ac79768106d50de42ccc8414c05", size = 17501678, upload-time = "2025-07-22T07:57:23.237Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/39/09ae7082c77a978f2d72d94856e2e57906165c645693bc3a940bcad3a32d/mysql_connector_python-9.4.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:47884fcb050112b8bef3458e17eac47cc81a6cbbf3524e3456146c949772d9b4", size = 18369526, upload-time = "2025-07-22T07:57:27.569Z" },
+ { url = "https://files.pythonhosted.org/packages/40/56/1bea00f5129550bcd0175781b9cd467e865d4aea4a6f38f700f34d95dcb8/mysql_connector_python-9.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:f14b6936cd326e212fc9ab5f666dea3efea654f0cb644460334e60e22986e735", size = 33508525, upload-time = "2025-07-22T07:57:32.935Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/ec/86dfefd3e6c0fca13085bc28b7f9baae3fce9f6af243d8693729f6b5063c/mysql_connector_python-9.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0f5ad70355720e64b72d7c068e858c9fd1f69b671d9575f857f235a10f878939", size = 33911834, upload-time = "2025-07-22T07:57:38.203Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/11/6907d53349b11478f72c8f22e38368d18262fbffc27e0f30e365d76dad93/mysql_connector_python-9.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:7106670abce510e440d393e27fc3602b8cf21e7a8a80216cc9ad9a68cd2e4595", size = 16393044, upload-time = "2025-07-22T07:57:42.053Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/0c/4365a802129be9fa63885533c38be019f1c6b6f5bcf8844ac53902314028/mysql_connector_python-9.4.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:7df1a8ddd182dd8adc914f6dc902a986787bf9599705c29aca7b2ce84e79d361", size = 17501627, upload-time = "2025-07-22T07:57:45.416Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/bf/ca596c00d7a6eaaf8ef2f66c9b23cd312527f483073c43ffac7843049cb4/mysql_connector_python-9.4.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:3892f20472e13e63b1fb4983f454771dd29f211b09724e69a9750e299542f2f8", size = 18369494, upload-time = "2025-07-22T07:57:49.714Z" },
+ { url = "https://files.pythonhosted.org/packages/25/14/6510a11ed9f80d77f743dc207773092c4ab78d5efa454b39b48480315d85/mysql_connector_python-9.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:d3e87142103d71c4df647ece30f98e85e826652272ed1c74822b56f6acdc38e7", size = 33516187, upload-time = "2025-07-22T07:57:55.294Z" },
+ { url = "https://files.pythonhosted.org/packages/16/a8/4f99d80f1cf77733ce9a44b6adb7f0dd7079e7afa51ca4826515ef0c3e16/mysql_connector_python-9.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:b27fcd403436fe83bafb2fe7fcb785891e821e639275c4ad3b3bd1e25f533206", size = 33917818, upload-time = "2025-07-22T07:58:00.523Z" },
+ { url = "https://files.pythonhosted.org/packages/15/9c/127f974ca9d5ee25373cb5433da06bb1f36e05f2a6b7436da1fe9c6346b0/mysql_connector_python-9.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd6ff5afb9c324b0bbeae958c93156cce4168c743bf130faf224d52818d1f0ee", size = 16392378, upload-time = "2025-07-22T07:58:04.669Z" },
+ { url = "https://files.pythonhosted.org/packages/03/7c/a543fb17c2dfa6be8548dfdc5879a0c7924cd5d1c79056c48472bb8fe858/mysql_connector_python-9.4.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:4efa3898a24aba6a4bfdbf7c1f5023c78acca3150d72cc91199cca2ccd22f76f", size = 17503693, upload-time = "2025-07-22T07:58:08.96Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/6e/c22fbee05f5cfd6ba76155b6d45f6261d8d4c1e36e23de04e7f25fbd01a4/mysql_connector_python-9.4.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:665c13e7402235162e5b7a2bfdee5895192121b64ea455c90a81edac6a48ede5", size = 18371987, upload-time = "2025-07-22T07:58:13.273Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/fd/f426f5f35a3d3180c7f84d1f96b4631be2574df94ca1156adab8618b236c/mysql_connector_python-9.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:815aa6cad0f351c1223ef345781a538f2e5e44ef405fdb3851eb322bd9c4ca2b", size = 33516214, upload-time = "2025-07-22T07:58:18.967Z" },
+ { url = "https://files.pythonhosted.org/packages/45/5a/1b053ae80b43cd3ccebc4bb99a98826969b3b0f8adebdcc2530750ad76ed/mysql_connector_python-9.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b3436a2c8c0ec7052932213e8d01882e6eb069dbab33402e685409084b133a1c", size = 33918565, upload-time = "2025-07-22T07:58:25.28Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/69/36b989de675d98ba8ff7d45c96c30c699865c657046f2e32db14e78f13d9/mysql_connector_python-9.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:57b0c224676946b70548c56798d5023f65afa1ba5b8ac9f04a143d27976c7029", size = 16392563, upload-time = "2025-07-22T07:58:29.623Z" },
+ { url = "https://files.pythonhosted.org/packages/79/e2/13036479cd1070d1080cee747de6c96bd6fbb021b736dd3ccef2b19016c8/mysql_connector_python-9.4.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:fde3bbffb5270a4b02077029914e6a9d2ec08f67d8375b4111432a2778e7540b", size = 17503749, upload-time = "2025-07-22T07:58:33.649Z" },
+ { url = "https://files.pythonhosted.org/packages/31/df/b89e6551b91332716d384dcc3223e1f8065902209dcd9e477a3df80154f7/mysql_connector_python-9.4.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:25f77ad7d845df3b5a5a3a6a8d1fed68248dc418a6938a371d1ddaaab6b9a8e3", size = 18372145, upload-time = "2025-07-22T07:58:37.384Z" },
+ { url = "https://files.pythonhosted.org/packages/07/bd/af0de40a01d5cb4df19318cc018e64666f2b7fa89bffa1ab5b35337aae2c/mysql_connector_python-9.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:227dd420c71e6d4788d52d98f298e563f16b6853577e5ade4bd82d644257c812", size = 33516503, upload-time = "2025-07-22T07:58:41.987Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/9b/712053216fcbe695e519ecb1035ffd767c2de9f51ccba15078537c99d6fa/mysql_connector_python-9.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5163381a312d38122eded2197eb5cd7ccf1a5c5881d4e7a6de10d6ea314d088e", size = 33918904, upload-time = "2025-07-22T07:58:46.796Z" },
+ { url = "https://files.pythonhosted.org/packages/64/15/cbd996d425c59811849f3c1d1b1dae089a1ae18c4acd4d8de2b847b772df/mysql_connector_python-9.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:c727cb1f82b40c9aaa7a15ab5cf0a7f87c5d8dce32eab5ff2530a4aa6054e7df", size = 16392566, upload-time = "2025-07-22T07:58:50.223Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/36/b32635b69729f144d45c0cbcd135cfd6c480a62160ac015ca71ebf68fca7/mysql_connector_python-9.4.0-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:20f8154ab5c0ed444f8ef8e5fa91e65215037db102c137b5f995ebfffd309b78", size = 17501675, upload-time = "2025-07-22T07:58:53.049Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/23/65e801f74b3fcc2a6944242d64f0d623af48497e4d9cf55419c2c6d6439b/mysql_connector_python-9.4.0-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:7b8976d89d67c8b0dc452471cb557d9998ed30601fb69a876bf1f0ecaa7954a4", size = 18369579, upload-time = "2025-07-22T07:58:55.995Z" },
+ { url = "https://files.pythonhosted.org/packages/86/e9/dc31eeffe33786016e1370be72f339544ee00034cb702c0b4a3c6f5c1585/mysql_connector_python-9.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:4ee4fe1b067e243aae21981e4b9f9d300a3104814b8274033ca8fc7a89b1729e", size = 33506513, upload-time = "2025-07-22T07:58:59.341Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/c7/aa6f4cc2e5e3fb68b5a6bba680429b761e387b8a040cf16a5f17e0b09df6/mysql_connector_python-9.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1c6b95404e80d003cd452e38674e91528e2b3a089fe505c882f813b564e64f9d", size = 33909982, upload-time = "2025-07-22T07:59:02.832Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/a4/b1e2adc65121e7eabed06d09bed87638e7f9a51e9b5dbb1cfb17b58b1181/mysql_connector_python-9.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:a8f820c111335f225d63367307456eb7e10494f87e7a94acded3bb762e55a6d4", size = 16393051, upload-time = "2025-07-22T07:59:05.983Z" },
+ { url = "https://files.pythonhosted.org/packages/36/34/b6165e15fd45a8deb00932d8e7d823de7650270873b4044c4db6688e1d8f/mysql_connector_python-9.4.0-py2.py3-none-any.whl", hash = "sha256:56e679169c704dab279b176fab2a9ee32d2c632a866c0f7cd48a8a1e2cf802c4", size = 406574, upload-time = "2025-07-22T07:59:08.394Z" },
+]
+
+[[package]]
+name = "mysqlclient"
+version = "2.2.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/61/68/810093cb579daae426794bbd9d88aa830fae296e85172d18cb0f0e5dd4bc/mysqlclient-2.2.7.tar.gz", hash = "sha256:24ae22b59416d5fcce7e99c9d37548350b4565baac82f95e149cac6ce4163845", size = 91383, upload-time = "2025-01-10T12:06:00.763Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/24/cdaaef42aac7d53c0a01bb638da64961c293b1b6d204efd47400a68029d4/mysqlclient-2.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:2e3c11f7625029d7276ca506f8960a7fd3c5a0a0122c9e7404e6a8fe961b3d22", size = 207748, upload-time = "2025-01-10T11:56:24.357Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/e3/3e2de3f93cd60dd63bd229ec3e3b679f682982614bf513d046c2722aa4ce/mysqlclient-2.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:a22d99d26baf4af68ebef430e3131bb5a9b722b79a9fcfac6d9bbf8a88800687", size = 207745, upload-time = "2025-01-10T11:56:28.67Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/b5/2a8a4bcba3440550f358b839638fe8ec9146fa3c9194890b4998a530c926/mysqlclient-2.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:4b4c0200890837fc64014cc938ef2273252ab544c1b12a6c1d674c23943f3f2e", size = 208032, upload-time = "2025-01-10T11:56:29.879Z" },
+ { url = "https://files.pythonhosted.org/packages/29/01/e80141f1cd0459e4c9a5dd309dee135bbae41d6c6c121252fdd853001a8a/mysqlclient-2.2.7-cp313-cp313-win_amd64.whl", hash = "sha256:201a6faa301011dd07bca6b651fe5aaa546d7c9a5426835a06c3172e1056a3c5", size = 208000, upload-time = "2025-01-10T11:56:32.293Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/e0/524b0777524e0d410f71987f556dd6a0e3273fdb06cd6e91e96afade7220/mysqlclient-2.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:199dab53a224357dd0cb4d78ca0e54018f9cee9bf9ec68d72db50e0a23569076", size = 207857, upload-time = "2025-01-10T11:56:33.666Z" },
+ { url = "https://files.pythonhosted.org/packages/16/cc/5b1570be9f8597ee41e2a0bd7b62ba861ec2c81898d9449f3d6bfbe15d29/mysqlclient-2.2.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:92af368ed9c9144737af569c86d3b6c74a012a6f6b792eb868384787b52bb585", size = 207800, upload-time = "2025-01-10T11:56:36.023Z" },
+ { url = "https://files.pythonhosted.org/packages/20/40/b5d03494c1caa8f4da171db41d8d9d5b0d8959f893761597d97420083362/mysqlclient-2.2.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:977e35244fe6ef44124e9a1c2d1554728a7b76695598e4b92b37dc2130503069", size = 207965, upload-time = "2025-01-10T11:56:37.252Z" },
+]
+
+[[package]]
+name = "nest-asyncio"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" },
+]
+
[[package]]
name = "nexios"
version = "3.11.4"
source = { editable = "." }
dependencies = [
+ { name = "aiomysql" },
+ { name = "aiopg" },
+ { name = "aiosqlite" },
{ name = "anyio" },
+ { name = "apsw", version = "3.51.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "apsw", version = "3.51.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "asyncmy" },
+ { name = "asyncpg" },
{ name = "itsdangerous" },
+ { name = "mariadb" },
+ { name = "mysql-connector-python" },
+ { name = "mysqlclient" },
+ { name = "nest-asyncio" },
+ { name = "pg8000" },
+ { name = "psutil" },
+ { name = "psycopg" },
+ { name = "psycopg-pool" },
{ name = "pydantic" },
+ { name = "pymysql" },
{ name = "python-multipart" },
{ name = "uvicorn" },
+ { name = "uvloop" },
]
[package.optional-dependencies]
@@ -420,7 +1398,10 @@ dev = [
{ name = "httpx" },
{ name = "itsdangerous" },
{ name = "jinja2" },
+ { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "matplotlib", version = "3.10.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "mypy" },
+ { name = "pandas" },
{ name = "pyjwt" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
@@ -464,6 +1445,7 @@ requires-dist = [
{ name = "click", marker = "extra == 'all'", specifier = ">=8.1.3" },
{ name = "click", marker = "extra == 'cli'", specifier = ">=8.1.3" },
{ name = "click", marker = "extra == 'dev'", specifier = ">=8.1.3" },
+ { name = "commitizen", marker = "extra == 'dev'", specifier = ">=2.11.10" },
{ name = "coverage", marker = "extra == 'dev'", specifier = ">=6.3,<8.0" },
{ name = "granian", marker = "extra == 'granian'", specifier = ">=1.2.0" },
{ name = "httpx", marker = "extra == 'dev'", specifier = ">=0.23.3,<0.29.0" },
@@ -474,7 +1456,17 @@ requires-dist = [
{ name = "jinja2", marker = "extra == 'all'", specifier = ">=3.1.6" },
{ name = "jinja2", marker = "extra == 'dev'", specifier = ">=3.1.6" },
{ name = "jinja2", marker = "extra == 'templating'", specifier = ">=3.1.6" },
+ { name = "mariadb", specifier = ">=1.1.14" },
+ { name = "matplotlib", marker = "extra == 'dev'", specifier = ">=3.7.1" },
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.15.0" },
+ { name = "mysql-connector-python", specifier = ">=9.4.0" },
+ { name = "mysqlclient", specifier = ">=2.2.7" },
+ { name = "nest-asyncio", specifier = ">=1.6.0" },
+ { name = "pandas", marker = "extra == 'dev'", specifier = ">=2.0.0" },
+ { name = "pg8000", specifier = ">=1.31.5" },
+ { name = "psutil", specifier = ">=7.1.3" },
+ { name = "psycopg", specifier = ">=3.2.9" },
+ { name = "psycopg-pool", specifier = ">=3.2.7" },
{ name = "pydantic", specifier = ">=2.0,<3.0" },
{ name = "pyjwt", marker = "extra == 'all'", specifier = ">=2.7.0" },
{ name = "pyjwt", marker = "extra == 'dev'", specifier = ">=2.7.0" },
@@ -493,6 +1485,7 @@ requires-dist = [
{ name = "uvicorn", marker = "extra == 'all'", specifier = ">=0.27.0" },
{ name = "uvicorn", marker = "extra == 'dev'", specifier = ">=0.27.0" },
{ name = "uvicorn", marker = "extra == 'http'", specifier = ">=0.27.0" },
+ { name = "uvloop", specifier = ">=0.22.1" },
]
provides-extras = ["templating", "jwt", "granian", "cli", "http", "all", "dev"]
@@ -514,6 +1507,76 @@ 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 = "pandas"
+version = "2.3.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+ { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "python-dateutil" },
+ { name = "pytz" },
+ { name = "tzdata" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" },
+ { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" },
+ { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" },
+ { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" },
+ { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" },
+ { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" },
+ { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" },
+ { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" },
+ { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" },
+ { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" },
+ { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" },
+ { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" },
+ { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" },
+ { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" },
+ { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" },
+ { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" },
+ { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" },
+ { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" },
+ { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" },
+ { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" },
+ { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" },
+ { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" },
+ { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" },
+ { url = "https://files.pythonhosted.org/packages/56/b4/52eeb530a99e2a4c55ffcd352772b599ed4473a0f892d127f4147cf0f88e/pandas-2.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c503ba5216814e295f40711470446bc3fd00f0faea8a086cbc688808e26f92a2", size = 11567720, upload-time = "2025-09-29T23:33:06.209Z" },
+ { url = "https://files.pythonhosted.org/packages/48/4a/2d8b67632a021bced649ba940455ed441ca854e57d6e7658a6024587b083/pandas-2.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a637c5cdfa04b6d6e2ecedcb81fc52ffb0fd78ce2ebccc9ea964df9f658de8c8", size = 10810302, upload-time = "2025-09-29T23:33:35.846Z" },
+ { url = "https://files.pythonhosted.org/packages/13/e6/d2465010ee0569a245c975dc6967b801887068bc893e908239b1f4b6c1ac/pandas-2.3.3-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:854d00d556406bffe66a4c0802f334c9ad5a96b4f1f868adf036a21b11ef13ff", size = 12154874, upload-time = "2025-09-29T23:33:49.939Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/18/aae8c0aa69a386a3255940e9317f793808ea79d0a525a97a903366bb2569/pandas-2.3.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf1f8a81d04ca90e32a0aceb819d34dbd378a98bf923b6398b9a3ec0bf44de29", size = 12790141, upload-time = "2025-09-29T23:34:05.655Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/26/617f98de789de00c2a444fbe6301bb19e66556ac78cff933d2c98f62f2b4/pandas-2.3.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23ebd657a4d38268c7dfbdf089fbc31ea709d82e4923c5ffd4fbd5747133ce73", size = 13208697, upload-time = "2025-09-29T23:34:21.835Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/fb/25709afa4552042bd0e15717c75e9b4a2294c3dc4f7e6ea50f03c5136600/pandas-2.3.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5554c929ccc317d41a5e3d1234f3be588248e61f08a74dd17c9eabb535777dc9", size = 13879233, upload-time = "2025-09-29T23:34:35.079Z" },
+ { url = "https://files.pythonhosted.org/packages/98/af/7be05277859a7bc399da8ba68b88c96b27b48740b6cf49688899c6eb4176/pandas-2.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:d3e28b3e83862ccf4d85ff19cf8c20b2ae7e503881711ff2d534dc8f761131aa", size = 11359119, upload-time = "2025-09-29T23:34:46.339Z" },
+]
+
[[package]]
name = "pathspec"
version = "0.12.1"
@@ -532,6 +1595,143 @@ 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 = "prompt-toolkit"
+version = "3.0.51"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "wcwidth" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" },
+]
+
+[[package]]
+name = "psutil"
+version = "7.1.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e1/88/bdd0a41e5857d5d703287598cbf08dad90aed56774ea52ae071bae9071b6/psutil-7.1.3.tar.gz", hash = "sha256:6c86281738d77335af7aec228328e944b30930899ea760ecf33a4dba66be5e74", size = 489059, upload-time = "2025-11-02T12:25:54.619Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bd/93/0c49e776b8734fef56ec9c5c57f923922f2cf0497d62e0f419465f28f3d0/psutil-7.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0005da714eee687b4b8decd3d6cc7c6db36215c9e74e5ad2264b90c3df7d92dc", size = 239751, upload-time = "2025-11-02T12:25:58.161Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/8d/b31e39c769e70780f007969815195a55c81a63efebdd4dbe9e7a113adb2f/psutil-7.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19644c85dcb987e35eeeaefdc3915d059dac7bd1167cdcdbf27e0ce2df0c08c0", size = 240368, upload-time = "2025-11-02T12:26:00.491Z" },
+ { url = "https://files.pythonhosted.org/packages/62/61/23fd4acc3c9eebbf6b6c78bcd89e5d020cfde4acf0a9233e9d4e3fa698b4/psutil-7.1.3-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95ef04cf2e5ba0ab9eaafc4a11eaae91b44f4ef5541acd2ee91d9108d00d59a7", size = 287134, upload-time = "2025-11-02T12:26:02.613Z" },
+ { url = "https://files.pythonhosted.org/packages/30/1c/f921a009ea9ceb51aa355cb0cc118f68d354db36eae18174bab63affb3e6/psutil-7.1.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1068c303be3a72f8e18e412c5b2a8f6d31750fb152f9cb106b54090296c9d251", size = 289904, upload-time = "2025-11-02T12:26:05.207Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/82/62d68066e13e46a5116df187d319d1724b3f437ddd0f958756fc052677f4/psutil-7.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:18349c5c24b06ac5612c0428ec2a0331c26443d259e2a0144a9b24b4395b58fa", size = 249642, upload-time = "2025-11-02T12:26:07.447Z" },
+ { url = "https://files.pythonhosted.org/packages/df/ad/c1cd5fe965c14a0392112f68362cfceb5230819dbb5b1888950d18a11d9f/psutil-7.1.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c525ffa774fe4496282fb0b1187725793de3e7c6b29e41562733cae9ada151ee", size = 245518, upload-time = "2025-11-02T12:26:09.719Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/bb/6670bded3e3236eb4287c7bcdc167e9fae6e1e9286e437f7111caed2f909/psutil-7.1.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b403da1df4d6d43973dc004d19cee3b848e998ae3154cc8097d139b77156c353", size = 239843, upload-time = "2025-11-02T12:26:11.968Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/66/853d50e75a38c9a7370ddbeefabdd3d3116b9c31ef94dc92c6729bc36bec/psutil-7.1.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad81425efc5e75da3f39b3e636293360ad8d0b49bed7df824c79764fb4ba9b8b", size = 240369, upload-time = "2025-11-02T12:26:14.358Z" },
+ { url = "https://files.pythonhosted.org/packages/41/bd/313aba97cb5bfb26916dc29cf0646cbe4dd6a89ca69e8c6edce654876d39/psutil-7.1.3-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f33a3702e167783a9213db10ad29650ebf383946e91bc77f28a5eb083496bc9", size = 288210, upload-time = "2025-11-02T12:26:16.699Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/fa/76e3c06e760927a0cfb5705eb38164254de34e9bd86db656d4dbaa228b04/psutil-7.1.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fac9cd332c67f4422504297889da5ab7e05fd11e3c4392140f7370f4208ded1f", size = 291182, upload-time = "2025-11-02T12:26:18.848Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/1d/5774a91607035ee5078b8fd747686ebec28a962f178712de100d00b78a32/psutil-7.1.3-cp314-cp314t-win_amd64.whl", hash = "sha256:3792983e23b69843aea49c8f5b8f115572c5ab64c153bada5270086a2123c7e7", size = 250466, upload-time = "2025-11-02T12:26:21.183Z" },
+ { url = "https://files.pythonhosted.org/packages/00/ca/e426584bacb43a5cb1ac91fae1937f478cd8fbe5e4ff96574e698a2c77cd/psutil-7.1.3-cp314-cp314t-win_arm64.whl", hash = "sha256:31d77fcedb7529f27bb3a0472bea9334349f9a04160e8e6e5020f22c59893264", size = 245756, upload-time = "2025-11-02T12:26:23.148Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/94/46b9154a800253e7ecff5aaacdf8ebf43db99de4a2dfa18575b02548654e/psutil-7.1.3-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2bdbcd0e58ca14996a42adf3621a6244f1bb2e2e528886959c72cf1e326677ab", size = 238359, upload-time = "2025-11-02T12:26:25.284Z" },
+ { url = "https://files.pythonhosted.org/packages/68/3a/9f93cff5c025029a36d9a92fef47220ab4692ee7f2be0fba9f92813d0cb8/psutil-7.1.3-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc31fa00f1fbc3c3802141eede66f3a2d51d89716a194bf2cd6fc68310a19880", size = 239171, upload-time = "2025-11-02T12:26:27.23Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/b1/5f49af514f76431ba4eea935b8ad3725cdeb397e9245ab919dbc1d1dc20f/psutil-7.1.3-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb428f9f05c1225a558f53e30ccbad9930b11c3fc206836242de1091d3e7dd3", size = 263261, upload-time = "2025-11-02T12:26:29.48Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/95/992c8816a74016eb095e73585d747e0a8ea21a061ed3689474fabb29a395/psutil-7.1.3-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d974e02ca2c8eb4812c3f76c30e28836fffc311d55d979f1465c1feeb2b68b", size = 264635, upload-time = "2025-11-02T12:26:31.74Z" },
+ { url = "https://files.pythonhosted.org/packages/55/4c/c3ed1a622b6ae2fd3c945a366e64eb35247a31e4db16cf5095e269e8eb3c/psutil-7.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:f39c2c19fe824b47484b96f9692932248a54c43799a84282cfe58d05a6449efd", size = 247633, upload-time = "2025-11-02T12:26:33.887Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/ad/33b2ccec09bf96c2b2ef3f9a6f66baac8253d7565d8839e024a6b905d45d/psutil-7.1.3-cp37-abi3-win_arm64.whl", hash = "sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1", size = 244608, upload-time = "2025-11-02T12:26:36.136Z" },
+]
+
+[[package]]
+name = "psycopg"
+version = "3.2.11"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+ { name = "tzdata", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/27/02/9fdfc018c026df2bcf9c11480c1014f9b90c6d801e5f929408cbfbf94cc0/psycopg-3.2.11.tar.gz", hash = "sha256:398bb484ed44361e041c8f804ed7af3d2fcefbffdace1d905b7446c319321706", size = 160644, upload-time = "2025-10-18T22:48:28.136Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/aa/1b/96ee90ed0007d64936d9bd1bb3108d0af3cf762b4f11dbd73359f0687c3d/psycopg-3.2.11-py3-none-any.whl", hash = "sha256:217231b2b6b72fba88281b94241b2f16043ee67f81def47c52a01b72ff0c086a", size = 206766, upload-time = "2025-10-18T22:43:32.114Z" },
+]
+
+[[package]]
+name = "psycopg-pool"
+version = "3.2.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9d/8f/3ec52b17087c2ed5fa32b64fd4814dde964c9aa4bd49d0d30fc24725ca6d/psycopg_pool-3.2.7.tar.gz", hash = "sha256:a77d531bfca238e49e5fb5832d65b98e69f2c62bfda3d2d4d833696bdc9ca54b", size = 29765, upload-time = "2025-10-26T00:46:10.379Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/59/74e752f605c6f0e351d4cf1c54fb9a1616dc800db4572b95bbfbb1a6225f/psycopg_pool-3.2.7-py3-none-any.whl", hash = "sha256:4b47bb59d887ef5da522eb63746b9f70e2faf967d34aac4f56ffc65e9606728f", size = 38232, upload-time = "2025-10-26T00:46:00.496Z" },
+]
+
+[[package]]
+name = "psycopg2-binary"
+version = "2.9.11"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6a/f2/8e377d29c2ecf99f6062d35ea606b036e8800720eccfec5fe3dd672c2b24/psycopg2_binary-2.9.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d6fe6b47d0b42ce1c9f1fa3e35bb365011ca22e39db37074458f27921dca40f2", size = 3756506, upload-time = "2025-10-10T11:10:30.144Z" },
+ { url = "https://files.pythonhosted.org/packages/24/cc/dc143ea88e4ec9d386106cac05023b69668bd0be20794c613446eaefafe5/psycopg2_binary-2.9.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a6c0e4262e089516603a09474ee13eabf09cb65c332277e39af68f6233911087", size = 3863943, upload-time = "2025-10-10T11:10:34.586Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/df/16848771155e7c419c60afeb24950b8aaa3ab09c0a091ec3ccca26a574d0/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c47676e5b485393f069b4d7a811267d3168ce46f988fa602658b8bb901e9e64d", size = 4410873, upload-time = "2025-10-10T11:10:38.951Z" },
+ { url = "https://files.pythonhosted.org/packages/43/79/5ef5f32621abd5a541b89b04231fe959a9b327c874a1d41156041c75494b/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a28d8c01a7b27a1e3265b11250ba7557e5f72b5ee9e5f3a2fa8d2949c29bf5d2", size = 4468016, upload-time = "2025-10-10T11:10:43.319Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/9b/d7542d0f7ad78f57385971f426704776d7b310f5219ed58da5d605b1892e/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f3f2732cf504a1aa9e9609d02f79bea1067d99edf844ab92c247bbca143303b", size = 4164996, upload-time = "2025-10-10T11:10:46.705Z" },
+ { url = "https://files.pythonhosted.org/packages/14/ed/e409388b537fa7414330687936917c522f6a77a13474e4238219fcfd9a84/psycopg2_binary-2.9.11-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:865f9945ed1b3950d968ec4690ce68c55019d79e4497366d36e090327ce7db14", size = 3981881, upload-time = "2025-10-30T02:54:57.182Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/30/50e330e63bb05efc6fa7c1447df3e08954894025ca3dcb396ecc6739bc26/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:91537a8df2bde69b1c1db01d6d944c831ca793952e4f57892600e96cee95f2cd", size = 3650857, upload-time = "2025-10-10T11:10:50.112Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/e0/4026e4c12bb49dd028756c5b0bc4c572319f2d8f1c9008e0dad8cc9addd7/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4dca1f356a67ecb68c81a7bc7809f1569ad9e152ce7fd02c2f2036862ca9f66b", size = 3296063, upload-time = "2025-10-10T11:10:54.089Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/34/eb172be293c886fef5299fe5c3fcf180a05478be89856067881007934a7c/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0da4de5c1ac69d94ed4364b6cbe7190c1a70d325f112ba783d83f8440285f152", size = 3043464, upload-time = "2025-10-30T02:55:02.483Z" },
+ { url = "https://files.pythonhosted.org/packages/18/1c/532c5d2cb11986372f14b798a95f2eaafe5779334f6a80589a68b5fcf769/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37d8412565a7267f7d79e29ab66876e55cb5e8e7b3bbf94f8206f6795f8f7e7e", size = 3345378, upload-time = "2025-10-10T11:11:01.039Z" },
+ { url = "https://files.pythonhosted.org/packages/70/e7/de420e1cf16f838e1fa17b1120e83afff374c7c0130d088dba6286fcf8ea/psycopg2_binary-2.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:c665f01ec8ab273a61c62beeb8cce3014c214429ced8a308ca1fc410ecac3a39", size = 2713904, upload-time = "2025-10-10T11:11:04.81Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/ae/8d8266f6dd183ab4d48b95b9674034e1b482a3f8619b33a0d86438694577/psycopg2_binary-2.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10", size = 3756452, upload-time = "2025-10-10T11:11:11.583Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/34/aa03d327739c1be70e09d01182619aca8ebab5970cd0cfa50dd8b9cec2ac/psycopg2_binary-2.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a", size = 3863957, upload-time = "2025-10-10T11:11:16.932Z" },
+ { url = "https://files.pythonhosted.org/packages/48/89/3fdb5902bdab8868bbedc1c6e6023a4e08112ceac5db97fc2012060e0c9a/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4", size = 4410955, upload-time = "2025-10-10T11:11:21.21Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/24/e18339c407a13c72b336e0d9013fbbbde77b6fd13e853979019a1269519c/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7", size = 4468007, upload-time = "2025-10-10T11:11:24.831Z" },
+ { url = "https://files.pythonhosted.org/packages/91/7e/b8441e831a0f16c159b5381698f9f7f7ed54b77d57bc9c5f99144cc78232/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee", size = 4165012, upload-time = "2025-10-10T11:11:29.51Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/61/4aa89eeb6d751f05178a13da95516c036e27468c5d4d2509bb1e15341c81/psycopg2_binary-2.9.11-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb", size = 3981881, upload-time = "2025-10-30T02:55:07.332Z" },
+ { url = "https://files.pythonhosted.org/packages/76/a1/2f5841cae4c635a9459fe7aca8ed771336e9383b6429e05c01267b0774cf/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f", size = 3650985, upload-time = "2025-10-10T11:11:34.975Z" },
+ { url = "https://files.pythonhosted.org/packages/84/74/4defcac9d002bca5709951b975173c8c2fa968e1a95dc713f61b3a8d3b6a/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94", size = 3296039, upload-time = "2025-10-10T11:11:40.432Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/c2/782a3c64403d8ce35b5c50e1b684412cf94f171dc18111be8c976abd2de1/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f", size = 3043477, upload-time = "2025-10-30T02:55:11.182Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/31/36a1d8e702aa35c38fc117c2b8be3f182613faa25d794b8aeaab948d4c03/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908", size = 3345842, upload-time = "2025-10-10T11:11:45.366Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/b4/a5375cda5b54cb95ee9b836930fea30ae5a8f14aa97da7821722323d979b/psycopg2_binary-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03", size = 2713894, upload-time = "2025-10-10T11:11:48.775Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" },
+ { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" },
+ { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" },
+ { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" },
+ { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" },
+ { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" },
+ { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" },
+ { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" },
+ { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" },
+ { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" },
+ { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" },
+ { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" },
+ { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" },
+ { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" },
+ { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" },
+ { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" },
+ { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" },
+ { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/41/cb36a61146b3afed03e980477f6dd29c0263f15e4b4844660501a774dc0b/psycopg2_binary-2.9.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:20e7fb94e20b03dcc783f76c0865f9da39559dcc0c28dd1a3fce0d01902a6b9c", size = 3756418, upload-time = "2025-10-10T11:14:00.728Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/e3/8623be505c8921727277f22753092835b559543b078e83378bb74712dbc8/psycopg2_binary-2.9.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bdab48575b6f870f465b397c38f1b415520e9879fdf10a53ee4f49dcbdf8a21", size = 3863981, upload-time = "2025-10-10T11:14:05.215Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/86/ec3682dc3550c65eff80384f603a6a55b798e1b86ccef262d454d19f96eb/psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9d3a9edcfbe77a3ed4bc72836d466dfce4174beb79eda79ea155cc77237ed9e8", size = 4410882, upload-time = "2025-10-10T11:14:09.552Z" },
+ { url = "https://files.pythonhosted.org/packages/41/af/540ee7d56fb33408c57240d55904c95e4a30952c096f5e1542769cadc787/psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:44fc5c2b8fa871ce7f0023f619f1349a0aa03a0857f2c96fbc01c657dcbbdb49", size = 4468062, upload-time = "2025-10-10T11:14:15.225Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/d5/b95d47b2e67b2adfaba517c803a99a1ac41e84c8201d0f3b29d77b56e357/psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9c55460033867b4622cda1b6872edf445809535144152e5d14941ef591980edf", size = 4165036, upload-time = "2025-10-10T11:14:21.209Z" },
+ { url = "https://files.pythonhosted.org/packages/af/f9/99e39882b70d9b0cfdcbad33bea2e5823843c3a7839c1aaf89fc1337c05c/psycopg2_binary-2.9.11-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2d11098a83cca92deaeaed3d58cfd150d49b3b06ee0d0852be466bf87596899e", size = 3981901, upload-time = "2025-10-30T02:55:39.325Z" },
+ { url = "https://files.pythonhosted.org/packages/de/c3/8d2c97f1dfddedf5a06c6ad2eda83fba48555a7bc525c3150aedc6f2bedc/psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:691c807d94aecfbc76a14e1408847d59ff5b5906a04a23e12a89007672b9e819", size = 3650995, upload-time = "2025-10-10T11:14:27.733Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/89/afdf59b44b84ebb28111652485fab608429389f4051d22bc5a7bb43d5208/psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:8b81627b691f29c4c30a8f322546ad039c40c328373b11dff7490a3e1b517855", size = 3296106, upload-time = "2025-10-10T11:14:33.312Z" },
+ { url = "https://files.pythonhosted.org/packages/94/21/851c9ecf0e9a699907d1c455dbbde7ef9b11dba28e7b7b132c7bb28391f2/psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:b637d6d941209e8d96a072d7977238eea128046effbf37d1d8b2c0764750017d", size = 3043491, upload-time = "2025-10-30T02:55:42.228Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/de/50f6eced439e7a131b268276c4b68cf8800fd55d8cef7b37109c44bf957a/psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:41360b01c140c2a03d346cec3280cf8a71aa07d94f3b1509fa0161c366af66b4", size = 3345816, upload-time = "2025-10-10T11:14:38.648Z" },
+ { url = "https://files.pythonhosted.org/packages/45/3b/e0506f199dc8a90ff3b462f261f45d15c0703bb8c59f0da1add5f0c11a30/psycopg2_binary-2.9.11-cp39-cp39-win_amd64.whl", hash = "sha256:875039274f8a2361e5207857899706da840768e2a775bf8c65e82f60b197df02", size = 2714968, upload-time = "2025-10-10T11:14:43.24Z" },
+]
+
[[package]]
name = "pydantic"
version = "2.11.7"
@@ -643,6 +1843,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" },
]
+[[package]]
+name = "pymysql"
+version = "1.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f5/ae/1fe3fcd9f959efa0ebe200b8de88b5a5ce3e767e38c7ac32fb179f16a388/pymysql-1.1.2.tar.gz", hash = "sha256:4961d3e165614ae65014e361811a724e2044ad3ea3739de9903ae7c21f539f03", size = 48258, upload-time = "2025-08-24T12:55:55.146Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl", hash = "sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9", size = 45300, upload-time = "2025-08-24T12:55:53.394Z" },
+]
+
+[[package]]
+name = "pyparsing"
+version = "3.2.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" },
+]
+
[[package]]
name = "pytest"
version = "8.3.5"
@@ -672,6 +1890,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694, upload-time = "2025-03-25T06:22:27.807Z" },
]
+[[package]]
+name = "pytest-dependency"
+version = "0.6.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytest" },
+ { name = "setuptools" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7e/3b/317cc04e77d707d338540ca67b619df8f247f3f4c9f40e67bf5ea503ad94/pytest-dependency-0.6.0.tar.gz", hash = "sha256:934b0e6a39d95995062c193f7eaeed8a8ffa06ff1bcef4b62b0dc74a708bacc1", size = 19499, upload-time = "2023-12-31T20:38:54.991Z" }
+
+[[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-multipart"
version = "0.0.20"
@@ -681,6 +1921,100 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
]
+[[package]]
+name = "pytz"
+version = "2025.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
+ { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
+ { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
+ { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
+ { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
+ { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
+ { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" },
+ { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" },
+ { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" },
+]
+
+[[package]]
+name = "questionary"
+version = "2.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "prompt-toolkit" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" },
+]
+
[[package]]
name = "ruff"
version = "0.12.0"
@@ -790,6 +2124,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
]
+[[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 = "uvicorn"
version = "0.34.3"
@@ -803,3 +2146,176 @@ sdist = { url = "https://files.pythonhosted.org/packages/de/ad/713be230bcda622ea
wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431, upload-time = "2025-06-01T07:48:15.664Z" },
]
+
+[[package]]
+name = "uvloop"
+version = "0.22.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" },
+ { url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" },
+ { url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" },
+ { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" },
+ { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" },
+ { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" },
+ { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" },
+ { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" },
+ { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" },
+ { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" },
+ { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" },
+ { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" },
+ { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" },
+ { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" },
+ { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" },
+ { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/1b/6fbd611aeba01ef802c5876c94d7be603a9710db055beacbad39e75a31aa/uvloop-0.22.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4", size = 1345858, upload-time = "2025-10-16T22:17:11.106Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/91/2c84f00bdbe3c51023cc83b027bac1fe959ba4a552e970da5ef0237f7945/uvloop-0.22.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c", size = 743913, upload-time = "2025-10-16T22:17:12.165Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/10/76aec83886d41a88aca5681db6a2c0601622d0d2cb66cd0d200587f962ad/uvloop-0.22.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54", size = 3635818, upload-time = "2025-10-16T22:17:13.812Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/9a/733fcb815d345979fc54d3cdc3eb50bc75a47da3e4003ea7ada58e6daa65/uvloop-0.22.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659", size = 3685477, upload-time = "2025-10-16T22:17:15.307Z" },
+ { url = "https://files.pythonhosted.org/packages/83/fb/bee1eb11cc92bd91f76d97869bb6a816e80d59fd73721b0a3044dc703d9c/uvloop-0.22.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743", size = 3496128, upload-time = "2025-10-16T22:17:16.558Z" },
+ { url = "https://files.pythonhosted.org/packages/76/ee/3fdfeaa9776c0fd585d358c92b1dbca669720ffa476f0bbe64ed8f245bd7/uvloop-0.22.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7", size = 3602565, upload-time = "2025-10-16T22:17:17.755Z" },
+]
+
+[[package]]
+name = "wcwidth"
+version = "0.2.14"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" },
+]
+
+[[package]]
+name = "wrapt"
+version = "2.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/49/2a/6de8a50cb435b7f42c46126cf1a54b2aab81784e74c8595c8e025e8f36d3/wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f", size = 82040, upload-time = "2025-11-07T00:45:33.312Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/61/0d/12d8c803ed2ce4e5e7d5b9f5f602721f9dfef82c95959f3ce97fa584bb5c/wrapt-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:64b103acdaa53b7caf409e8d45d39a8442fe6dcfec6ba3f3d141e0cc2b5b4dbd", size = 77481, upload-time = "2025-11-07T00:43:11.103Z" },
+ { url = "https://files.pythonhosted.org/packages/05/3e/4364ebe221ebf2a44d9fc8695a19324692f7dd2795e64bd59090856ebf12/wrapt-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:91bcc576260a274b169c3098e9a3519fb01f2989f6d3d386ef9cbf8653de1374", size = 60692, upload-time = "2025-11-07T00:43:13.697Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/ff/ae2a210022b521f86a8ddcdd6058d137c051003812b0388a5e9a03d3fe10/wrapt-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ab594f346517010050126fcd822697b25a7031d815bb4fbc238ccbe568216489", size = 61574, upload-time = "2025-11-07T00:43:14.967Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/93/5cf92edd99617095592af919cb81d4bff61c5dbbb70d3c92099425a8ec34/wrapt-2.0.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:36982b26f190f4d737f04a492a68accbfc6fa042c3f42326fdfbb6c5b7a20a31", size = 113688, upload-time = "2025-11-07T00:43:18.275Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/0a/e38fc0cee1f146c9fb266d8ef96ca39fb14a9eef165383004019aa53f88a/wrapt-2.0.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23097ed8bc4c93b7bf36fa2113c6c733c976316ce0ee2c816f64ca06102034ef", size = 115698, upload-time = "2025-11-07T00:43:19.407Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/85/bef44ea018b3925fb0bcbe9112715f665e4d5309bd945191da814c314fd1/wrapt-2.0.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bacfe6e001749a3b64db47bcf0341da757c95959f592823a93931a422395013", size = 112096, upload-time = "2025-11-07T00:43:16.5Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/0b/733a2376e413117e497aa1a5b1b78e8f3a28c0e9537d26569f67d724c7c5/wrapt-2.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8ec3303e8a81932171f455f792f8df500fc1a09f20069e5c16bd7049ab4e8e38", size = 114878, upload-time = "2025-11-07T00:43:20.81Z" },
+ { url = "https://files.pythonhosted.org/packages/da/03/d81dcb21bbf678fcda656495792b059f9d56677d119ca022169a12542bd0/wrapt-2.0.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:3f373a4ab5dbc528a94334f9fe444395b23c2f5332adab9ff4ea82f5a9e33bc1", size = 111298, upload-time = "2025-11-07T00:43:22.229Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/d5/5e623040e8056e1108b787020d56b9be93dbbf083bf2324d42cde80f3a19/wrapt-2.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f49027b0b9503bf6c8cdc297ca55006b80c2f5dd36cecc72c6835ab6e10e8a25", size = 113361, upload-time = "2025-11-07T00:43:24.301Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/f3/de535ccecede6960e28c7b722e5744846258111d6c9f071aa7578ea37ad3/wrapt-2.0.1-cp310-cp310-win32.whl", hash = "sha256:8330b42d769965e96e01fa14034b28a2a7600fbf7e8f0cc90ebb36d492c993e4", size = 58035, upload-time = "2025-11-07T00:43:28.96Z" },
+ { url = "https://files.pythonhosted.org/packages/21/15/39d3ca5428a70032c2ec8b1f1c9d24c32e497e7ed81aed887a4998905fcc/wrapt-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:1218573502a8235bb8a7ecaed12736213b22dcde9feab115fa2989d42b5ded45", size = 60383, upload-time = "2025-11-07T00:43:25.804Z" },
+ { url = "https://files.pythonhosted.org/packages/43/c2/dfd23754b7f7a4dce07e08f4309c4e10a40046a83e9ae1800f2e6b18d7c1/wrapt-2.0.1-cp310-cp310-win_arm64.whl", hash = "sha256:eda8e4ecd662d48c28bb86be9e837c13e45c58b8300e43ba3c9b4fa9900302f7", size = 58894, upload-time = "2025-11-07T00:43:27.074Z" },
+ { url = "https://files.pythonhosted.org/packages/98/60/553997acf3939079dab022e37b67b1904b5b0cc235503226898ba573b10c/wrapt-2.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e17283f533a0d24d6e5429a7d11f250a58d28b4ae5186f8f47853e3e70d2590", size = 77480, upload-time = "2025-11-07T00:43:30.573Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/50/e5b3d30895d77c52105c6d5cbf94d5b38e2a3dd4a53d22d246670da98f7c/wrapt-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85df8d92158cb8f3965aecc27cf821461bb5f40b450b03facc5d9f0d4d6ddec6", size = 60690, upload-time = "2025-11-07T00:43:31.594Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/40/660b2898703e5cbbb43db10cdefcc294274458c3ca4c68637c2b99371507/wrapt-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1be685ac7700c966b8610ccc63c3187a72e33cab53526a27b2a285a662cd4f7", size = 61578, upload-time = "2025-11-07T00:43:32.918Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/36/825b44c8a10556957bc0c1d84c7b29a40e05fcf1873b6c40aa9dbe0bd972/wrapt-2.0.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:df0b6d3b95932809c5b3fecc18fda0f1e07452d05e2662a0b35548985f256e28", size = 114115, upload-time = "2025-11-07T00:43:35.605Z" },
+ { url = "https://files.pythonhosted.org/packages/83/73/0a5d14bb1599677304d3c613a55457d34c344e9b60eda8a737c2ead7619e/wrapt-2.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da7384b0e5d4cae05c97cd6f94faaf78cc8b0f791fc63af43436d98c4ab37bb", size = 116157, upload-time = "2025-11-07T00:43:37.058Z" },
+ { url = "https://files.pythonhosted.org/packages/01/22/1c158fe763dbf0a119f985d945711d288994fe5514c0646ebe0eb18b016d/wrapt-2.0.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ec65a78fbd9d6f083a15d7613b2800d5663dbb6bb96003899c834beaa68b242c", size = 112535, upload-time = "2025-11-07T00:43:34.138Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/28/4f16861af67d6de4eae9927799b559c20ebdd4fe432e89ea7fe6fcd9d709/wrapt-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7de3cc939be0e1174969f943f3b44e0d79b6f9a82198133a5b7fc6cc92882f16", size = 115404, upload-time = "2025-11-07T00:43:39.214Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/8b/7960122e625fad908f189b59c4aae2d50916eb4098b0fb2819c5a177414f/wrapt-2.0.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fb1a5b72cbd751813adc02ef01ada0b0d05d3dcbc32976ce189a1279d80ad4a2", size = 111802, upload-time = "2025-11-07T00:43:40.476Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/73/7881eee5ac31132a713ab19a22c9e5f1f7365c8b1df50abba5d45b781312/wrapt-2.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3fa272ca34332581e00bf7773e993d4f632594eb2d1b0b162a9038df0fd971dd", size = 113837, upload-time = "2025-11-07T00:43:42.921Z" },
+ { url = "https://files.pythonhosted.org/packages/45/00/9499a3d14e636d1f7089339f96c4409bbc7544d0889f12264efa25502ae8/wrapt-2.0.1-cp311-cp311-win32.whl", hash = "sha256:fc007fdf480c77301ab1afdbb6ab22a5deee8885f3b1ed7afcb7e5e84a0e27be", size = 58028, upload-time = "2025-11-07T00:43:47.369Z" },
+ { url = "https://files.pythonhosted.org/packages/70/5d/8f3d7eea52f22638748f74b102e38fdf88cb57d08ddeb7827c476a20b01b/wrapt-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:47434236c396d04875180171ee1f3815ca1eada05e24a1ee99546320d54d1d1b", size = 60385, upload-time = "2025-11-07T00:43:44.34Z" },
+ { url = "https://files.pythonhosted.org/packages/14/e2/32195e57a8209003587bbbad44d5922f13e0ced2a493bb46ca882c5b123d/wrapt-2.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:837e31620e06b16030b1d126ed78e9383815cbac914693f54926d816d35d8edf", size = 58893, upload-time = "2025-11-07T00:43:46.161Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/73/8cb252858dc8254baa0ce58ce382858e3a1cf616acebc497cb13374c95c6/wrapt-2.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1fdbb34da15450f2b1d735a0e969c24bdb8d8924892380126e2a293d9902078c", size = 78129, upload-time = "2025-11-07T00:43:48.852Z" },
+ { url = "https://files.pythonhosted.org/packages/19/42/44a0db2108526ee6e17a5ab72478061158f34b08b793df251d9fbb9a7eb4/wrapt-2.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d32794fe940b7000f0519904e247f902f0149edbe6316c710a8562fb6738841", size = 61205, upload-time = "2025-11-07T00:43:50.402Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/8a/5b4b1e44b791c22046e90d9b175f9a7581a8cc7a0debbb930f81e6ae8e25/wrapt-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:386fb54d9cd903ee0012c09291336469eb7b244f7183d40dc3e86a16a4bace62", size = 61692, upload-time = "2025-11-07T00:43:51.678Z" },
+ { url = "https://files.pythonhosted.org/packages/11/53/3e794346c39f462bcf1f58ac0487ff9bdad02f9b6d5ee2dc84c72e0243b2/wrapt-2.0.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7b219cb2182f230676308cdcacd428fa837987b89e4b7c5c9025088b8a6c9faf", size = 121492, upload-time = "2025-11-07T00:43:55.017Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/7e/10b7b0e8841e684c8ca76b462a9091c45d62e8f2de9c4b1390b690eadf16/wrapt-2.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:641e94e789b5f6b4822bb8d8ebbdfc10f4e4eae7756d648b717d980f657a9eb9", size = 123064, upload-time = "2025-11-07T00:43:56.323Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/d1/3c1e4321fc2f5ee7fd866b2d822aa89b84495f28676fd976c47327c5b6aa/wrapt-2.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe21b118b9f58859b5ebaa4b130dee18669df4bd111daad082b7beb8799ad16b", size = 117403, upload-time = "2025-11-07T00:43:53.258Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/b0/d2f0a413cf201c8c2466de08414a15420a25aa83f53e647b7255cc2fab5d/wrapt-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17fb85fa4abc26a5184d93b3efd2dcc14deb4b09edcdb3535a536ad34f0b4dba", size = 121500, upload-time = "2025-11-07T00:43:57.468Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/45/bddb11d28ca39970a41ed48a26d210505120f925918592283369219f83cc/wrapt-2.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b89ef9223d665ab255ae42cc282d27d69704d94be0deffc8b9d919179a609684", size = 116299, upload-time = "2025-11-07T00:43:58.877Z" },
+ { url = "https://files.pythonhosted.org/packages/81/af/34ba6dd570ef7a534e7eec0c25e2615c355602c52aba59413411c025a0cb/wrapt-2.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a453257f19c31b31ba593c30d997d6e5be39e3b5ad9148c2af5a7314061c63eb", size = 120622, upload-time = "2025-11-07T00:43:59.962Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/3e/693a13b4146646fb03254636f8bafd20c621955d27d65b15de07ab886187/wrapt-2.0.1-cp312-cp312-win32.whl", hash = "sha256:3e271346f01e9c8b1130a6a3b0e11908049fe5be2d365a5f402778049147e7e9", size = 58246, upload-time = "2025-11-07T00:44:03.169Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/36/715ec5076f925a6be95f37917b66ebbeaa1372d1862c2ccd7a751574b068/wrapt-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:2da620b31a90cdefa9cd0c2b661882329e2e19d1d7b9b920189956b76c564d75", size = 60492, upload-time = "2025-11-07T00:44:01.027Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/3e/62451cd7d80f65cc125f2b426b25fbb6c514bf6f7011a0c3904fc8c8df90/wrapt-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:aea9c7224c302bc8bfc892b908537f56c430802560e827b75ecbde81b604598b", size = 58987, upload-time = "2025-11-07T00:44:02.095Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/fe/41af4c46b5e498c90fc87981ab2972fbd9f0bccda597adb99d3d3441b94b/wrapt-2.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:47b0f8bafe90f7736151f61482c583c86b0693d80f075a58701dd1549b0010a9", size = 78132, upload-time = "2025-11-07T00:44:04.628Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/92/d68895a984a5ebbbfb175512b0c0aad872354a4a2484fbd5552e9f275316/wrapt-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cbeb0971e13b4bd81d34169ed57a6dda017328d1a22b62fda45e1d21dd06148f", size = 61211, upload-time = "2025-11-07T00:44:05.626Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/26/ba83dc5ae7cf5aa2b02364a3d9cf74374b86169906a1f3ade9a2d03cf21c/wrapt-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb7cffe572ad0a141a7886a1d2efa5bef0bf7fe021deeea76b3ab334d2c38218", size = 61689, upload-time = "2025-11-07T00:44:06.719Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/67/d7a7c276d874e5d26738c22444d466a3a64ed541f6ef35f740dbd865bab4/wrapt-2.0.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8d60527d1ecfc131426b10d93ab5d53e08a09c5fa0175f6b21b3252080c70a9", size = 121502, upload-time = "2025-11-07T00:44:09.557Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/6b/806dbf6dd9579556aab22fc92908a876636e250f063f71548a8660382184/wrapt-2.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c654eafb01afac55246053d67a4b9a984a3567c3808bb7df2f8de1c1caba2e1c", size = 123110, upload-time = "2025-11-07T00:44:10.64Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/08/cdbb965fbe4c02c5233d185d070cabed2ecc1f1e47662854f95d77613f57/wrapt-2.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:98d873ed6c8b4ee2418f7afce666751854d6d03e3c0ec2a399bb039cd2ae89db", size = 117434, upload-time = "2025-11-07T00:44:08.138Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/d1/6aae2ce39db4cb5216302fa2e9577ad74424dfbe315bd6669725569e048c/wrapt-2.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9e850f5b7fc67af856ff054c71690d54fa940c3ef74209ad9f935b4f66a0233", size = 121533, upload-time = "2025-11-07T00:44:12.142Z" },
+ { url = "https://files.pythonhosted.org/packages/79/35/565abf57559fbe0a9155c29879ff43ce8bd28d2ca61033a3a3dd67b70794/wrapt-2.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e505629359cb5f751e16e30cf3f91a1d3ddb4552480c205947da415d597f7ac2", size = 116324, upload-time = "2025-11-07T00:44:13.28Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/e0/53ff5e76587822ee33e560ad55876d858e384158272cd9947abdd4ad42ca/wrapt-2.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2879af909312d0baf35f08edeea918ee3af7ab57c37fe47cb6a373c9f2749c7b", size = 120627, upload-time = "2025-11-07T00:44:14.431Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/7b/38df30fd629fbd7612c407643c63e80e1c60bcc982e30ceeae163a9800e7/wrapt-2.0.1-cp313-cp313-win32.whl", hash = "sha256:d67956c676be5a24102c7407a71f4126d30de2a569a1c7871c9f3cabc94225d7", size = 58252, upload-time = "2025-11-07T00:44:17.814Z" },
+ { url = "https://files.pythonhosted.org/packages/85/64/d3954e836ea67c4d3ad5285e5c8fd9d362fd0a189a2db622df457b0f4f6a/wrapt-2.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9ca66b38dd642bf90c59b6738af8070747b610115a39af2498535f62b5cdc1c3", size = 60500, upload-time = "2025-11-07T00:44:15.561Z" },
+ { url = "https://files.pythonhosted.org/packages/89/4e/3c8b99ac93527cfab7f116089db120fef16aac96e5f6cdb724ddf286086d/wrapt-2.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:5a4939eae35db6b6cec8e7aa0e833dcca0acad8231672c26c2a9ab7a0f8ac9c8", size = 58993, upload-time = "2025-11-07T00:44:16.65Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/f4/eff2b7d711cae20d220780b9300faa05558660afb93f2ff5db61fe725b9a/wrapt-2.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a52f93d95c8d38fed0669da2ebdb0b0376e895d84596a976c15a9eb45e3eccb3", size = 82028, upload-time = "2025-11-07T00:44:18.944Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/67/cb945563f66fd0f61a999339460d950f4735c69f18f0a87ca586319b1778/wrapt-2.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e54bbf554ee29fcceee24fa41c4d091398b911da6e7f5d7bffda963c9aed2e1", size = 62949, upload-time = "2025-11-07T00:44:20.074Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/ca/f63e177f0bbe1e5cf5e8d9b74a286537cd709724384ff20860f8f6065904/wrapt-2.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:908f8c6c71557f4deaa280f55d0728c3bca0960e8c3dd5ceeeafb3c19942719d", size = 63681, upload-time = "2025-11-07T00:44:21.345Z" },
+ { url = "https://files.pythonhosted.org/packages/39/a1/1b88fcd21fd835dca48b556daef750952e917a2794fa20c025489e2e1f0f/wrapt-2.0.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e2f84e9af2060e3904a32cea9bb6db23ce3f91cfd90c6b426757cf7cc01c45c7", size = 152696, upload-time = "2025-11-07T00:44:24.318Z" },
+ { url = "https://files.pythonhosted.org/packages/62/1c/d9185500c1960d9f5f77b9c0b890b7fc62282b53af7ad1b6bd779157f714/wrapt-2.0.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3612dc06b436968dfb9142c62e5dfa9eb5924f91120b3c8ff501ad878f90eb3", size = 158859, upload-time = "2025-11-07T00:44:25.494Z" },
+ { url = "https://files.pythonhosted.org/packages/91/60/5d796ed0f481ec003220c7878a1d6894652efe089853a208ea0838c13086/wrapt-2.0.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d2d947d266d99a1477cd005b23cbd09465276e302515e122df56bb9511aca1b", size = 146068, upload-time = "2025-11-07T00:44:22.81Z" },
+ { url = "https://files.pythonhosted.org/packages/04/f8/75282dd72f102ddbfba137e1e15ecba47b40acff32c08ae97edbf53f469e/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7d539241e87b650cbc4c3ac9f32c8d1ac8a54e510f6dca3f6ab60dcfd48c9b10", size = 155724, upload-time = "2025-11-07T00:44:26.634Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/27/fe39c51d1b344caebb4a6a9372157bdb8d25b194b3561b52c8ffc40ac7d1/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4811e15d88ee62dbf5c77f2c3ff3932b1e3ac92323ba3912f51fc4016ce81ecf", size = 144413, upload-time = "2025-11-07T00:44:27.939Z" },
+ { url = "https://files.pythonhosted.org/packages/83/2b/9f6b643fe39d4505c7bf926d7c2595b7cb4b607c8c6b500e56c6b36ac238/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c1c91405fcf1d501fa5d55df21e58ea49e6b879ae829f1039faaf7e5e509b41e", size = 150325, upload-time = "2025-11-07T00:44:29.29Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/b6/20ffcf2558596a7f58a2e69c89597128781f0b88e124bf5a4cadc05b8139/wrapt-2.0.1-cp313-cp313t-win32.whl", hash = "sha256:e76e3f91f864e89db8b8d2a8311d57df93f01ad6bb1e9b9976d1f2e83e18315c", size = 59943, upload-time = "2025-11-07T00:44:33.211Z" },
+ { url = "https://files.pythonhosted.org/packages/87/6a/0e56111cbb3320151eed5d3821ee1373be13e05b376ea0870711f18810c3/wrapt-2.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:83ce30937f0ba0d28818807b303a412440c4b63e39d3d8fc036a94764b728c92", size = 63240, upload-time = "2025-11-07T00:44:30.935Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/54/5ab4c53ea1f7f7e5c3e7c1095db92932cc32fd62359d285486d00c2884c3/wrapt-2.0.1-cp313-cp313t-win_arm64.whl", hash = "sha256:4b55cacc57e1dc2d0991dbe74c6419ffd415fb66474a02335cb10efd1aa3f84f", size = 60416, upload-time = "2025-11-07T00:44:32.002Z" },
+ { url = "https://files.pythonhosted.org/packages/73/81/d08d83c102709258e7730d3cd25befd114c60e43ef3891d7e6877971c514/wrapt-2.0.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5e53b428f65ece6d9dad23cb87e64506392b720a0b45076c05354d27a13351a1", size = 78290, upload-time = "2025-11-07T00:44:34.691Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/14/393afba2abb65677f313aa680ff0981e829626fed39b6a7e3ec807487790/wrapt-2.0.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ad3ee9d0f254851c71780966eb417ef8e72117155cff04821ab9b60549694a55", size = 61255, upload-time = "2025-11-07T00:44:35.762Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/10/a4a1f2fba205a9462e36e708ba37e5ac95f4987a0f1f8fd23f0bf1fc3b0f/wrapt-2.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7b822c61ed04ee6ad64bc90d13368ad6eb094db54883b5dde2182f67a7f22c0", size = 61797, upload-time = "2025-11-07T00:44:37.22Z" },
+ { url = "https://files.pythonhosted.org/packages/12/db/99ba5c37cf1c4fad35349174f1e38bd8d992340afc1ff27f526729b98986/wrapt-2.0.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7164a55f5e83a9a0b031d3ffab4d4e36bbec42e7025db560f225489fa929e509", size = 120470, upload-time = "2025-11-07T00:44:39.425Z" },
+ { url = "https://files.pythonhosted.org/packages/30/3f/a1c8d2411eb826d695fc3395a431757331582907a0ec59afce8fe8712473/wrapt-2.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e60690ba71a57424c8d9ff28f8d006b7ad7772c22a4af432188572cd7fa004a1", size = 122851, upload-time = "2025-11-07T00:44:40.582Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/8d/72c74a63f201768d6a04a8845c7976f86be6f5ff4d74996c272cefc8dafc/wrapt-2.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3cd1a4bd9a7a619922a8557e1318232e7269b5fb69d4ba97b04d20450a6bf970", size = 117433, upload-time = "2025-11-07T00:44:38.313Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/5a/df37cf4042cb13b08256f8e27023e2f9b3d471d553376616591bb99bcb31/wrapt-2.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4c2e3d777e38e913b8ce3a6257af72fb608f86a1df471cb1d4339755d0a807c", size = 121280, upload-time = "2025-11-07T00:44:41.69Z" },
+ { url = "https://files.pythonhosted.org/packages/54/34/40d6bc89349f9931e1186ceb3e5fbd61d307fef814f09fbbac98ada6a0c8/wrapt-2.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3d366aa598d69416b5afedf1faa539fac40c1d80a42f6b236c88c73a3c8f2d41", size = 116343, upload-time = "2025-11-07T00:44:43.013Z" },
+ { url = "https://files.pythonhosted.org/packages/70/66/81c3461adece09d20781dee17c2366fdf0cb8754738b521d221ca056d596/wrapt-2.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c235095d6d090aa903f1db61f892fffb779c1eaeb2a50e566b52001f7a0f66ed", size = 119650, upload-time = "2025-11-07T00:44:44.523Z" },
+ { url = "https://files.pythonhosted.org/packages/46/3a/d0146db8be8761a9e388cc9cc1c312b36d583950ec91696f19bbbb44af5a/wrapt-2.0.1-cp314-cp314-win32.whl", hash = "sha256:bfb5539005259f8127ea9c885bdc231978c06b7a980e63a8a61c8c4c979719d0", size = 58701, upload-time = "2025-11-07T00:44:48.277Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/38/5359da9af7d64554be63e9046164bd4d8ff289a2dd365677d25ba3342c08/wrapt-2.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:4ae879acc449caa9ed43fc36ba08392b9412ee67941748d31d94e3cedb36628c", size = 60947, upload-time = "2025-11-07T00:44:46.086Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/3f/96db0619276a833842bf36343685fa04f987dd6e3037f314531a1e00492b/wrapt-2.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:8639b843c9efd84675f1e100ed9e99538ebea7297b62c4b45a7042edb84db03e", size = 59359, upload-time = "2025-11-07T00:44:47.164Z" },
+ { url = "https://files.pythonhosted.org/packages/71/49/5f5d1e867bf2064bf3933bc6cf36ade23505f3902390e175e392173d36a2/wrapt-2.0.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:9219a1d946a9b32bb23ccae66bdb61e35c62773ce7ca6509ceea70f344656b7b", size = 82031, upload-time = "2025-11-07T00:44:49.4Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/89/0009a218d88db66ceb83921e5685e820e2c61b59bbbb1324ba65342668bc/wrapt-2.0.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fa4184e74197af3adad3c889a1af95b53bb0466bced92ea99a0c014e48323eec", size = 62952, upload-time = "2025-11-07T00:44:50.74Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/18/9b968e920dd05d6e44bcc918a046d02afea0fb31b2f1c80ee4020f377cbe/wrapt-2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c5ef2f2b8a53b7caee2f797ef166a390fef73979b15778a4a153e4b5fedce8fa", size = 63688, upload-time = "2025-11-07T00:44:52.248Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/7d/78bdcb75826725885d9ea26c49a03071b10c4c92da93edda612910f150e4/wrapt-2.0.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e042d653a4745be832d5aa190ff80ee4f02c34b21f4b785745eceacd0907b815", size = 152706, upload-time = "2025-11-07T00:44:54.613Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/77/cac1d46f47d32084a703df0d2d29d47e7eb2a7d19fa5cbca0e529ef57659/wrapt-2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2afa23318136709c4b23d87d543b425c399887b4057936cd20386d5b1422b6fa", size = 158866, upload-time = "2025-11-07T00:44:55.79Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/11/b521406daa2421508903bf8d5e8b929216ec2af04839db31c0a2c525eee0/wrapt-2.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c72328f668cf4c503ffcf9434c2b71fdd624345ced7941bc6693e61bbe36bef", size = 146148, upload-time = "2025-11-07T00:44:53.388Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/c0/340b272bed297baa7c9ce0c98ef7017d9c035a17a6a71dce3184b8382da2/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3793ac154afb0e5b45d1233cb94d354ef7a983708cc3bb12563853b1d8d53747", size = 155737, upload-time = "2025-11-07T00:44:56.971Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/93/bfcb1fb2bdf186e9c2883a4d1ab45ab099c79cbf8f4e70ea453811fa3ea7/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fec0d993ecba3991645b4857837277469c8cc4c554a7e24d064d1ca291cfb81f", size = 144451, upload-time = "2025-11-07T00:44:58.515Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/6b/dca504fb18d971139d232652656180e3bd57120e1193d9a5899c3c0b7cdd/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:949520bccc1fa227274da7d03bf238be15389cd94e32e4297b92337df9b7a349", size = 150353, upload-time = "2025-11-07T00:44:59.753Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/f6/a1de4bd3653afdf91d250ca5c721ee51195df2b61a4603d4b373aa804d1d/wrapt-2.0.1-cp314-cp314t-win32.whl", hash = "sha256:be9e84e91d6497ba62594158d3d31ec0486c60055c49179edc51ee43d095f79c", size = 60609, upload-time = "2025-11-07T00:45:03.315Z" },
+ { url = "https://files.pythonhosted.org/packages/01/3a/07cd60a9d26fe73efead61c7830af975dfdba8537632d410462672e4432b/wrapt-2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:61c4956171c7434634401db448371277d07032a81cc21c599c22953374781395", size = 64038, upload-time = "2025-11-07T00:45:00.948Z" },
+ { url = "https://files.pythonhosted.org/packages/41/99/8a06b8e17dddbf321325ae4eb12465804120f699cd1b8a355718300c62da/wrapt-2.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:35cdbd478607036fee40273be8ed54a451f5f23121bd9d4be515158f9498f7ad", size = 60634, upload-time = "2025-11-07T00:45:02.087Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/1f/5af0ae22368ec69067a577f9e07a0dd2619a1f63aabc2851263679942667/wrapt-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:68424221a2dc00d634b54f92441914929c5ffb1c30b3b837343978343a3512a3", size = 77478, upload-time = "2025-11-07T00:45:16.65Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/b7/fd6b563aada859baabc55db6aa71b8afb4a3ceb8bc33d1053e4c7b5e0109/wrapt-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6bd1a18f5a797fe740cb3d7a0e853a8ce6461cc62023b630caec80171a6b8097", size = 60687, upload-time = "2025-11-07T00:45:17.896Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/8c/9ededfff478af396bcd081076986904bdca336d9664d247094150c877dcb/wrapt-2.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fb3a86e703868561c5cad155a15c36c716e1ab513b7065bd2ac8ed353c503333", size = 61563, upload-time = "2025-11-07T00:45:19.109Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/a7/d795a1aa2b6ab20ca21157fe03cbfc6aa7e870a88ac3b4ea189e2f6c79f0/wrapt-2.0.1-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5dc1b852337c6792aa111ca8becff5bacf576bf4a0255b0f05eb749da6a1643e", size = 113395, upload-time = "2025-11-07T00:45:21.551Z" },
+ { url = "https://files.pythonhosted.org/packages/61/32/56cde2bbf95f2d5698a1850a765520aa86bc7ae0f95b8ec80b6f2e2049bb/wrapt-2.0.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c046781d422f0830de6329fa4b16796096f28a92c8aef3850674442cdcb87b7f", size = 115362, upload-time = "2025-11-07T00:45:22.809Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/53/8d3cc433847c219212c133a3e8305bd087b386ef44442ff39189e8fa62ac/wrapt-2.0.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f73f9f7a0ebd0db139253d27e5fc8d2866ceaeef19c30ab5d69dcbe35e1a6981", size = 111766, upload-time = "2025-11-07T00:45:20.294Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/d3/14b50c2d0463c0dcef8f388cb1527ed7bbdf0972b9fd9976905f36c77ebf/wrapt-2.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b667189cf8efe008f55bbda321890bef628a67ab4147ebf90d182f2dadc78790", size = 114560, upload-time = "2025-11-07T00:45:24.054Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/b8/4f731ff178f77ae55385586de9ff4b4261e872cf2ced4875e6c976fbcb8b/wrapt-2.0.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:a9a83618c4f0757557c077ef71d708ddd9847ed66b7cc63416632af70d3e2308", size = 110999, upload-time = "2025-11-07T00:45:25.596Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/bb/5f1bb0f9ae9d12e19f1d71993d052082062603e83fe3e978377f918f054d/wrapt-2.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e9b121e9aeb15df416c2c960b8255a49d44b4038016ee17af03975992d03931", size = 113164, upload-time = "2025-11-07T00:45:26.8Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/f6/f3a3c623d3065c7bf292ee0b73566236b562d5ed894891bd8e435762b618/wrapt-2.0.1-cp39-cp39-win32.whl", hash = "sha256:1f186e26ea0a55f809f232e92cc8556a0977e00183c3ebda039a807a42be1494", size = 58028, upload-time = "2025-11-07T00:45:30.943Z" },
+ { url = "https://files.pythonhosted.org/packages/24/78/647c609dfa18063a7fcd5c23f762dd006be401cc9206314d29c9b0b12078/wrapt-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:bf4cb76f36be5de950ce13e22e7fdf462b35b04665a12b64f3ac5c1bbbcf3728", size = 60380, upload-time = "2025-11-07T00:45:28.341Z" },
+ { url = "https://files.pythonhosted.org/packages/07/90/0c14b241d18d80ddf4c847a5f52071e126e8a6a9e5a8a7952add8ef0d766/wrapt-2.0.1-cp39-cp39-win_arm64.whl", hash = "sha256:d6cc985b9c8b235bd933990cdbf0f891f8e010b65a3911f7a55179cd7b0fc57b", size = 58895, upload-time = "2025-11-07T00:45:29.527Z" },
+ { url = "https://files.pythonhosted.org/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" },
+]
+
+[[package]]
+name = "zipp"
+version = "3.23.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
+]