lint fixes

This commit is contained in:
bryan
2026-02-15 17:45:56 -08:00
parent 3e158b07af
commit e0bf09dd78
12 changed files with 512 additions and 513 deletions
@@ -200,7 +200,7 @@ class DeepResearchAgent:
},
)
def _setup(self) -> GraphExecutor:
def _setup(self, mock_mode: bool = False) -> None:
"""Set up the executor with all components."""
from pathlib import Path
@@ -152,7 +152,9 @@ def info(output_json):
click.echo(f"\nNodes: {', '.join(info_data['nodes'])}")
click.echo(f"Client-facing: {', '.join(info_data['client_facing_nodes'])}")
click.echo(f"Entry: {info_data['entry_node']}")
click.echo(f"Terminal: {', '.join(info_data['terminal_nodes']) or '(forever-alive)'}")
click.echo(
f"Terminal: {', '.join(info_data['terminal_nodes']) or '(forever-alive)'}"
)
@cli.command()
@@ -214,7 +216,9 @@ async def _interactive_shell(verbose=False):
if result.success:
output = result.output
if "report_status" in output:
click.echo(f"\nAssessment complete: {output['report_status']}\n")
click.echo(
f"\nAssessment complete: {output['report_status']}\n"
)
else:
click.echo(f"\nAssessment failed: {result.error}\n")
@@ -47,10 +47,7 @@ def register_tools(mcp: FastMCP) -> None:
"""
if not _DNS_AVAILABLE:
return {
"error": (
"dnspython is not installed. "
"Install it with: pip install dnspython"
),
"error": ("dnspython is not installed. Install it with: pip install dnspython"),
}
# Clean domain
@@ -118,9 +115,7 @@ def _check_spf(resolver: dns.resolver.Resolver, domain: str) -> dict:
)
elif "?all" in txt:
policy = "neutral"
issues.append(
"Uses ?all (neutral). SPF results are not used for filtering."
)
issues.append("Uses ?all (neutral). SPF results are not used for filtering.")
else:
policy = "unknown"
issues.append("No 'all' mechanism found in SPF record.")
@@ -179,9 +174,7 @@ def _check_dmarc(resolver: dns.resolver.Resolver, domain: str) -> dict:
"present": False,
"record": None,
"policy": None,
"issues": [
"No DMARC record found. Email spoofing is not actively monitored or blocked."
],
"issues": ["No DMARC record found. Email spoofing is not actively monitored or blocked."],
}
@@ -35,9 +35,7 @@ SECURITY_HEADERS = {
},
"X-Frame-Options": {
"severity": "medium",
"description": (
"No X-Frame-Options header. The site may be vulnerable to clickjacking."
),
"description": ("No X-Frame-Options header. The site may be vulnerable to clickjacking."),
"remediation": "Add the header: X-Frame-Options: DENY (or SAMEORIGIN)",
},
"X-Content-Type-Options": {
@@ -54,9 +52,7 @@ SECURITY_HEADERS = {
"No Referrer-Policy header. Full URLs (including query params) "
"may leak to third-party sites via the Referer header."
),
"remediation": (
"Add the header: Referrer-Policy: strict-origin-when-cross-origin"
),
"remediation": ("Add the header: Referrer-Policy: strict-origin-when-cross-origin"),
},
"Permissions-Policy": {
"severity": "low",
@@ -140,24 +136,28 @@ def register_tools(mcp: FastMCP) -> None:
if header_name.lower() in {k.lower() for k in headers}:
headers_present.append(header_name)
else:
headers_missing.append({
"header": header_name,
"severity": info["severity"],
"description": info["description"],
"remediation": info["remediation"],
})
headers_missing.append(
{
"header": header_name,
"severity": info["severity"],
"description": info["description"],
"remediation": info["remediation"],
}
)
# Check for leaky headers
leaky_found = []
for header_name, info in LEAKY_HEADERS.items():
value = headers.get(header_name)
if value:
leaky_found.append({
"header": header_name,
"value": value,
"severity": info["severity"],
"remediation": info["remediation"],
})
leaky_found.append(
{
"header": header_name,
"value": value,
"severity": info["severity"],
"remediation": info["remediation"],
}
)
# Check for deprecated X-XSS-Protection
xss_protection = headers.get("X-XSS-Protection")
@@ -43,15 +43,83 @@ TOP100_PORTS = sorted(
set(TOP20_PORTS)
| {
# Additional common ports
8, 20, 69, 111, 119, 123, 135, 137, 138, 139,
161, 162, 179, 389, 443, 465, 514, 515, 520, 587,
631, 636, 873, 902, 989, 990, 1080, 1194, 1443,
1521, 1723, 2049, 2082, 2083, 2086, 2087, 2096,
2181, 2222, 3000, 3128, 4443, 5000, 5001, 5060,
5222, 5601, 5984, 6443, 6660, 6661, 6662, 6663,
6664, 6665, 6666, 6667, 7001, 7002, 7443, 8000,
8008, 8081, 8082, 8083, 8088, 8443, 8888, 9000,
9090, 9200, 9300, 9443, 10000, 11211, 27017, 27018,
8,
20,
69,
111,
119,
123,
135,
137,
138,
139,
161,
162,
179,
389,
443,
465,
514,
515,
520,
587,
631,
636,
873,
902,
989,
990,
1080,
1194,
1443,
1521,
1723,
2049,
2082,
2083,
2086,
2087,
2096,
2181,
2222,
3000,
3128,
4443,
5000,
5001,
5060,
5222,
5601,
5984,
6443,
6660,
6661,
6662,
6663,
6664,
6665,
6666,
6667,
7001,
7002,
7443,
8000,
8008,
8081,
8082,
8083,
8088,
8443,
8888,
9000,
9090,
9200,
9300,
9443,
10000,
11211,
27017,
27018,
}
)
@@ -157,9 +225,7 @@ def register_tools(mcp: FastMCP) -> None:
# Check if this port is risky
if port in DATABASE_PORTS:
entry["severity"] = PORT_FINDINGS["database"]["severity"]
entry["finding"] = (
f"{entry['service']} port ({port}) exposed to internet"
)
entry["finding"] = f"{entry['service']} port ({port}) exposed to internet"
entry["remediation"] = PORT_FINDINGS["database"]["remediation"]
elif port in ADMIN_PORTS:
entry["severity"] = PORT_FINDINGS["admin"]["severity"]
@@ -267,7 +267,7 @@ def register_tools(mcp: FastMCP) -> None:
# Build top risks — sorted by category score (worst first), then by finding
all_findings.sort(key=lambda x: (x[2], x[0]))
top_risks = []
for category, finding, cat_score in all_findings[:10]:
for category, finding, _cat_score in all_findings[:10]:
cat_grade = categories[category]["grade"]
cat_label = category.replace("_", " ").title()
top_risks.append(f"{finding} ({cat_label}: {cat_grade})")
@@ -77,14 +77,16 @@ def register_tools(mcp: FastMCP) -> None:
conn = ctx_noverify.wrap_socket(socket.socket(), server_hostname=hostname)
conn.settimeout(10)
conn.connect((hostname, port))
issues.append({
"severity": "critical",
"finding": f"SSL certificate verification failed: {e}",
"remediation": (
"Obtain a valid certificate from a trusted CA. "
"Let's Encrypt provides free certificates."
),
})
issues.append(
{
"severity": "critical",
"finding": f"SSL certificate verification failed: {e}",
"remediation": (
"Obtain a valid certificate from a trusted CA. "
"Let's Encrypt provides free certificates."
),
}
)
# Gather TLS info
tls_version = conn.version() or "unknown"
@@ -134,34 +136,40 @@ def register_tools(mcp: FastMCP) -> None:
# TLS version
tls_version_ok = tls_version not in INSECURE_TLS_VERSIONS
if not tls_version_ok:
issues.append({
"severity": "high",
"finding": f"Insecure TLS version: {tls_version}",
"remediation": (
"Disable TLS 1.0 and 1.1 in your server configuration. "
"Use TLS 1.2 or 1.3 only."
),
})
issues.append(
{
"severity": "high",
"finding": f"Insecure TLS version: {tls_version}",
"remediation": (
"Disable TLS 1.0 and 1.1 in your server configuration. "
"Use TLS 1.2 or 1.3 only."
),
}
)
# Cipher strength
strong_cipher = True
if any(weak in cipher_name.upper() for weak in WEAK_CIPHERS):
strong_cipher = False
issues.append({
"severity": "high",
"finding": f"Weak cipher suite: {cipher_name}",
"remediation": (
"Configure your server to use strong cipher suites only. "
"Prefer AES-GCM and ChaCha20-Poly1305."
),
})
issues.append(
{
"severity": "high",
"finding": f"Weak cipher suite: {cipher_name}",
"remediation": (
"Configure your server to use strong cipher suites only. "
"Prefer AES-GCM and ChaCha20-Poly1305."
),
}
)
if cipher_bits and cipher_bits < 128:
strong_cipher = False
issues.append({
"severity": "high",
"finding": f"Cipher key length too short: {cipher_bits} bits",
"remediation": "Use cipher suites with at least 128-bit keys.",
})
issues.append(
{
"severity": "high",
"finding": f"Cipher key length too short: {cipher_bits} bits",
"remediation": "Use cipher suites with at least 128-bit keys.",
}
)
# Certificate validity
cert_valid = True
@@ -169,29 +177,35 @@ def register_tools(mcp: FastMCP) -> None:
if not_after and now > not_after:
cert_valid = False
issues.append({
"severity": "critical",
"finding": "SSL certificate has expired",
"remediation": "Renew the SSL certificate immediately.",
})
issues.append(
{
"severity": "critical",
"finding": "SSL certificate has expired",
"remediation": "Renew the SSL certificate immediately.",
}
)
elif days_until_expiry is not None and days_until_expiry <= 30:
cert_expiring_soon = True
issues.append({
"severity": "medium",
"finding": f"SSL certificate expires in {days_until_expiry} days",
"remediation": "Renew the SSL certificate before it expires.",
})
issues.append(
{
"severity": "medium",
"finding": f"SSL certificate expires in {days_until_expiry} days",
"remediation": "Renew the SSL certificate before it expires.",
}
)
if self_signed:
cert_valid = False
issues.append({
"severity": "high",
"finding": "Self-signed certificate detected",
"remediation": (
"Replace with a certificate from a trusted CA. "
"Let's Encrypt provides free certificates."
),
})
issues.append(
{
"severity": "high",
"finding": "Self-signed certificate detected",
"remediation": (
"Replace with a certificate from a trusted CA. "
"Let's Encrypt provides free certificates."
),
}
)
return {
"hostname": hostname,
@@ -145,12 +145,14 @@ def register_tools(mcp: FastMCP) -> None:
prefix = sub.replace(f".{domain}", "").lower()
for keyword, info in INTERESTING_KEYWORDS.items():
if re.search(rf"\b{keyword}\b", prefix) or prefix == keyword:
interesting.append({
"subdomain": sub,
"reason": info["reason"],
"severity": info["severity"],
"remediation": info["remediation"],
})
interesting.append(
{
"subdomain": sub,
"reason": info["reason"],
"severity": info["severity"],
"remediation": info["remediation"],
}
)
break
# Grade input
@@ -160,8 +162,7 @@ def register_tools(mcp: FastMCP) -> None:
for i in interesting
)
has_admin = any(
any(kw in i["subdomain"] for kw in ("admin", "backup"))
for i in interesting
any(kw in i["subdomain"] for kw in ("admin", "backup")) for i in interesting
)
# "reasonable" = fewer than 50 subdomains
reasonable_surface = len(subdomains) < 50
@@ -328,7 +328,7 @@ def _detect_js_libraries(html: str) -> list[str]:
if match:
# Try to extract version
version_match = re.search(
rf'{lib_name.lower().replace(".", r".")}[/-](\d+\.\d+(?:\.\d+)?)',
rf"{lib_name.lower().replace('.', r'.')}[/-](\d+\.\d+(?:\.\d+)?)",
html,
re.I,
)
@@ -376,9 +376,17 @@ def _detect_cms_from_html(html: str) -> str | None:
return "Ghost"
# Check meta generator tag
gen_match = re.search(r'<meta[^>]+name=["\']generator["\'][^>]+content=["\'](.*?)["\']', html, re.I)
gen_match = re.search(
r'<meta[^>]+name=["\']generator["\'][^>]+content=["\'](.*?)["\']',
html,
re.I,
)
if not gen_match:
gen_match = re.search(r'<meta[^>]+content=["\'](.*?)["\'][^>]+name=["\']generator["\']', html, re.I)
gen_match = re.search(
r'<meta[^>]+content=["\'](.*?)["\'][^>]+name=["\']generator["\']',
html,
re.I,
)
if gen_match:
return gen_match.group(1)
@@ -391,12 +399,14 @@ def _analyze_cookies(headers: httpx.Headers) -> list[dict]:
for raw in headers.get_list("set-cookie"):
name = raw.split("=", 1)[0].strip()
parts = [p.strip().lower() for p in raw.split(";")]
result.append({
"name": name,
"secure": "secure" in parts,
"httponly": "httponly" in parts,
"samesite": _extract_samesite(raw.lower()),
})
result.append(
{
"name": name,
"secure": "secure" in parts,
"httponly": "httponly" in parts,
"samesite": _extract_samesite(raw.lower()),
}
)
return result
+57 -36
View File
@@ -12,7 +12,6 @@ from aden_tools.tools.tech_stack_detector.tech_stack_detector import (
_extract_samesite,
)
# ---------------------------------------------------------------------------
# Cookie Analysis (_analyze_cookies)
# ---------------------------------------------------------------------------
@@ -34,9 +33,11 @@ class TestAnalyzeCookies:
"""Tests for _analyze_cookies parsing raw Set-Cookie headers."""
def test_secure_and_httponly_detected(self):
headers = FakeHeaders([
"session_id=abc123; Path=/; Secure; HttpOnly",
])
headers = FakeHeaders(
[
"session_id=abc123; Path=/; Secure; HttpOnly",
]
)
result = _analyze_cookies(headers)
assert len(result) == 1
@@ -45,9 +46,11 @@ class TestAnalyzeCookies:
assert result[0]["httponly"] is True
def test_missing_flags_detected(self):
headers = FakeHeaders([
"tracking=xyz; Path=/",
])
headers = FakeHeaders(
[
"tracking=xyz; Path=/",
]
)
result = _analyze_cookies(headers)
assert len(result) == 1
@@ -56,54 +59,66 @@ class TestAnalyzeCookies:
assert result[0]["httponly"] is False
def test_case_insensitive(self):
headers = FakeHeaders([
"tok=val; SECURE; HTTPONLY",
])
headers = FakeHeaders(
[
"tok=val; SECURE; HTTPONLY",
]
)
result = _analyze_cookies(headers)
assert result[0]["secure"] is True
assert result[0]["httponly"] is True
def test_samesite_lax(self):
headers = FakeHeaders([
"pref=dark; SameSite=Lax; Secure",
])
headers = FakeHeaders(
[
"pref=dark; SameSite=Lax; Secure",
]
)
result = _analyze_cookies(headers)
assert result[0]["samesite"] == "Lax"
assert result[0]["secure"] is True
def test_samesite_strict(self):
headers = FakeHeaders([
"csrf=token; SameSite=Strict; Secure; HttpOnly",
])
headers = FakeHeaders(
[
"csrf=token; SameSite=Strict; Secure; HttpOnly",
]
)
result = _analyze_cookies(headers)
assert result[0]["samesite"] == "Strict"
def test_samesite_none(self):
headers = FakeHeaders([
"cross=val; SameSite=None; Secure",
])
headers = FakeHeaders(
[
"cross=val; SameSite=None; Secure",
]
)
result = _analyze_cookies(headers)
assert result[0]["samesite"] == "None"
assert result[0]["secure"] is True
def test_no_samesite(self):
headers = FakeHeaders([
"id=123; Path=/; Secure",
])
headers = FakeHeaders(
[
"id=123; Path=/; Secure",
]
)
result = _analyze_cookies(headers)
assert result[0]["samesite"] is None
def test_multiple_cookies(self):
headers = FakeHeaders([
"a=1; Secure; HttpOnly",
"b=2; Path=/",
"c=3; Secure; SameSite=Strict",
])
headers = FakeHeaders(
[
"a=1; Secure; HttpOnly",
"b=2; Path=/",
"c=3; Secure; SameSite=Strict",
]
)
result = _analyze_cookies(headers)
assert len(result) == 3
@@ -119,9 +134,11 @@ class TestAnalyzeCookies:
def test_cookie_value_with_equals(self):
"""Cookie values containing '=' should not break name parsing."""
headers = FakeHeaders([
"token=abc=def==; Secure; HttpOnly",
])
headers = FakeHeaders(
[
"token=abc=def==; Secure; HttpOnly",
]
)
result = _analyze_cookies(headers)
assert result[0]["name"] == "token"
@@ -145,17 +162,21 @@ class TestAnalyzeCookies:
def test_secure_at_end_of_header(self):
"""Secure flag at the very end without trailing semicolon."""
headers = FakeHeaders([
"id=val; Path=/; Secure",
])
headers = FakeHeaders(
[
"id=val; Path=/; Secure",
]
)
result = _analyze_cookies(headers)
assert result[0]["secure"] is True
def test_no_space_after_semicolons(self):
"""Servers may omit space after semicolons (RFC 6265 Section 5.2)."""
headers = FakeHeaders([
"id=val;Secure;HttpOnly;Path=/",
])
headers = FakeHeaders(
[
"id=val;Secure;HttpOnly;Path=/",
]
)
result = _analyze_cookies(headers)
assert result[0]["name"] == "id"
assert result[0]["secure"] is True
@@ -1,365 +0,0 @@
#!/usr/bin/env python3
"""
Manual test script for security scanning tools.
Calls each tool against example.com with real network requests,
validates response structure, and feeds all results into the risk_scorer.
Usage:
python tests/tools/test_security_tools_manual.py
python tests/tools/test_security_tools_manual.py --no-verify # skip SSL verification
"""
from __future__ import annotations
import asyncio
import contextlib
import inspect
import json
import sys
import time
from unittest.mock import patch
from fastmcp import FastMCP
from aden_tools.tools.dns_security_scanner import register_tools as register_dns
from aden_tools.tools.http_headers_scanner import register_tools as register_headers
from aden_tools.tools.port_scanner import register_tools as register_ports
from aden_tools.tools.risk_scorer import register_tools as register_scorer
# Import each tool's register function
from aden_tools.tools.ssl_tls_scanner import register_tools as register_ssl
from aden_tools.tools.subdomain_enumerator import register_tools as register_subdomains
from aden_tools.tools.tech_stack_detector import register_tools as register_tech
TARGET_DOMAIN = "example.com"
TARGET_URL = "https://example.com"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def get_tool_fn(mcp: FastMCP, name: str):
"""Extract raw function from MCP tool manager."""
return mcp._tool_manager._tools[name].fn
def call_tool(fn, *args, **kwargs):
"""Call a tool function, handling async transparently."""
if inspect.iscoroutinefunction(fn):
return asyncio.run(fn(*args, **kwargs))
return fn(*args, **kwargs)
def validate_keys(result: dict, required_keys: list[str], tool_name: str) -> list[str]:
"""Check that required keys exist in the result dict. Returns list of errors."""
errors = []
for key in required_keys:
if key not in result:
errors.append(f" Missing key: '{key}'")
return errors
def validate_grade_input(result: dict, expected_keys: list[str], tool_name: str) -> list[str]:
"""Check that grade_input exists and has expected boolean keys."""
errors = []
gi = result.get("grade_input")
if gi is None:
errors.append(" Missing 'grade_input'")
return errors
if not isinstance(gi, dict):
errors.append(f" 'grade_input' is {type(gi).__name__}, expected dict")
return errors
for key in expected_keys:
if key not in gi:
errors.append(f" grade_input missing key: '{key}'")
elif not isinstance(gi[key], bool):
errors.append(f" grade_input['{key}'] is {type(gi[key]).__name__}, expected bool")
return errors
def print_section(title: str):
print(f"\n{'=' * 60}")
print(f" {title}")
print(f"{'=' * 60}")
def print_result_summary(result: dict, max_lines: int = 15):
"""Pretty-print a result dict, truncated."""
formatted = json.dumps(result, indent=2, default=str)
lines = formatted.split("\n")
for line in lines[:max_lines]:
print(f" {line}")
if len(lines) > max_lines:
print(f" ... ({len(lines) - max_lines} more lines)")
# ---------------------------------------------------------------------------
# Individual tool tests
# ---------------------------------------------------------------------------
def test_ssl_tls_scan(mcp: FastMCP) -> tuple[dict | None, list[str]]:
fn = get_tool_fn(mcp, "ssl_tls_scan")
result = call_tool(fn, hostname=TARGET_DOMAIN)
if not isinstance(result, dict):
return None, [f" Result is {type(result).__name__}, expected dict"]
if "error" in result:
return result, [f" Tool returned error: {result['error']}"]
errors = validate_keys(result, ["hostname", "tls_version", "cipher", "certificate", "issues", "grade_input"], "ssl_tls_scan")
errors += validate_grade_input(result, ["tls_version_ok", "cert_valid", "cert_expiring_soon", "strong_cipher", "self_signed"], "ssl_tls_scan")
return result, errors
def test_http_headers_scan(mcp: FastMCP) -> tuple[dict | None, list[str]]:
fn = get_tool_fn(mcp, "http_headers_scan")
result = call_tool(fn, url=TARGET_URL)
if not isinstance(result, dict):
return None, [f" Result is {type(result).__name__}, expected dict"]
if "error" in result:
return result, [f" Tool returned error: {result['error']}"]
errors = validate_keys(result, ["url", "status_code", "headers_present", "headers_missing", "leaky_headers", "grade_input"], "http_headers_scan")
errors += validate_grade_input(result, ["hsts", "csp", "x_frame_options", "x_content_type_options", "referrer_policy", "permissions_policy", "no_leaky_headers"], "http_headers_scan")
return result, errors
def test_dns_security_scan(mcp: FastMCP) -> tuple[dict | None, list[str]]:
fn = get_tool_fn(mcp, "dns_security_scan")
result = call_tool(fn, domain=TARGET_DOMAIN)
if not isinstance(result, dict):
return None, [f" Result is {type(result).__name__}, expected dict"]
if "error" in result:
return result, [f" Tool returned error: {result['error']}"]
errors = validate_keys(result, ["domain", "spf", "dmarc", "dkim", "dnssec", "mx_records", "caa_records", "zone_transfer", "grade_input"], "dns_security_scan")
errors += validate_grade_input(result, ["spf_present", "spf_strict", "dmarc_present", "dmarc_enforcing", "dkim_found", "dnssec_enabled", "zone_transfer_blocked"], "dns_security_scan")
return result, errors
def test_port_scan(mcp: FastMCP) -> tuple[dict | None, list[str]]:
fn = get_tool_fn(mcp, "port_scan")
# Only scan 80 and 443 to keep it fast
result = call_tool(fn, hostname=TARGET_DOMAIN, ports="80,443")
if not isinstance(result, dict):
return None, [f" Result is {type(result).__name__}, expected dict"]
if "error" in result:
return result, [f" Tool returned error: {result['error']}"]
errors = validate_keys(result, ["hostname", "ip", "ports_scanned", "open_ports", "closed_ports", "grade_input"], "port_scan")
errors += validate_grade_input(result, ["no_database_ports_exposed", "no_admin_ports_exposed", "no_legacy_ports_exposed", "only_web_ports"], "port_scan")
return result, errors
def test_tech_stack_detect(mcp: FastMCP) -> tuple[dict | None, list[str]]:
fn = get_tool_fn(mcp, "tech_stack_detect")
result = call_tool(fn, url=TARGET_URL)
if not isinstance(result, dict):
return None, [f" Result is {type(result).__name__}, expected dict"]
if "error" in result:
return result, [f" Tool returned error: {result['error']}"]
errors = validate_keys(result, ["url", "server", "framework", "language", "cms", "javascript_libraries", "cdn", "analytics", "security_txt", "robots_txt", "interesting_paths", "cookies", "grade_input"], "tech_stack_detect")
errors += validate_grade_input(result, ["server_version_hidden", "framework_version_hidden", "security_txt_present", "cookies_secure", "cookies_httponly"], "tech_stack_detect")
return result, errors
def test_subdomain_enumerate(mcp: FastMCP) -> tuple[dict | None, list[str]]:
fn = get_tool_fn(mcp, "subdomain_enumerate")
result = call_tool(fn, domain=TARGET_DOMAIN, max_results=10)
if not isinstance(result, dict):
return None, [f" Result is {type(result).__name__}, expected dict"]
if "error" in result:
return result, [f" Tool returned error: {result['error']}"]
errors = validate_keys(result, ["domain", "source", "total_found", "subdomains", "interesting", "grade_input"], "subdomain_enumerate")
errors += validate_grade_input(result, ["no_dev_staging_exposed", "no_admin_exposed", "reasonable_surface_area"], "subdomain_enumerate")
return result, errors
def test_risk_score(mcp: FastMCP, scan_results: dict[str, dict | None]) -> tuple[dict | None, list[str]]:
fn = get_tool_fn(mcp, "risk_score")
# Build JSON string arguments from collected scan results
kwargs = {}
param_map = {
"ssl_tls_scan": "ssl_results",
"http_headers_scan": "headers_results",
"dns_security_scan": "dns_results",
"port_scan": "ports_results",
"tech_stack_detect": "tech_results",
"subdomain_enumerate": "subdomain_results",
}
for tool_name, param_name in param_map.items():
data = scan_results.get(tool_name)
kwargs[param_name] = json.dumps(data) if data else ""
result = call_tool(fn, **kwargs)
if not isinstance(result, dict):
return None, [f" Result is {type(result).__name__}, expected dict"]
if "error" in result:
return result, [f" Tool returned error: {result['error']}"]
errors = validate_keys(result, ["overall_score", "overall_grade", "categories", "top_risks", "grade_scale"], "risk_score")
# Validate score is in range
score = result.get("overall_score")
if score is not None and not (0 <= score <= 100):
errors.append(f" overall_score={score} is out of range [0, 100]")
# Validate grade is valid
grade = result.get("overall_grade")
if grade not in ("A", "B", "C", "D", "F"):
errors.append(f" overall_grade='{grade}' is not a valid grade")
# Validate categories dict has expected keys
cats = result.get("categories", {})
for cat in ["ssl_tls", "http_headers", "dns_security", "network_exposure", "technology", "attack_surface"]:
if cat not in cats:
errors.append(f" categories missing '{cat}'")
return result, errors
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def _patch_httpx_verify():
"""Monkeypatch httpx.AsyncClient to disable SSL verification.
Useful when the local Python SSL store is missing intermediate CAs.
Only affects this test run not the tool source code.
"""
_orig_init = __import__("httpx").AsyncClient.__init__
def _patched_init(self, *args, **kwargs):
kwargs["verify"] = False
return _orig_init(self, *args, **kwargs)
return patch.object(__import__("httpx").AsyncClient, "__init__", _patched_init)
def main():
no_verify = "--no-verify" in sys.argv
print("Security Tools Manual Test")
print(f"Target: {TARGET_DOMAIN}")
print(f"Time: {time.strftime('%Y-%m-%d %H:%M:%S')}")
if no_verify:
print("Mode: --no-verify (SSL verification disabled for httpx)")
# Register all security tools on a single MCP instance
mcp = FastMCP("security-test")
# Apply SSL verification patch before registering tools that use httpx.
# The patch must be active when the tool functions are *called*, not just
# when they are registered, so we keep the context manager open.
ctx = _patch_httpx_verify() if no_verify else contextlib.nullcontext()
ctx.__enter__()
register_ssl(mcp)
register_headers(mcp)
register_dns(mcp)
register_ports(mcp)
register_tech(mcp)
register_subdomains(mcp)
register_scorer(mcp)
# Run each scanner and collect results
tests = [
("ssl_tls_scan", test_ssl_tls_scan),
("http_headers_scan", test_http_headers_scan),
("dns_security_scan", test_dns_security_scan),
("port_scan", test_port_scan),
("tech_stack_detect", test_tech_stack_detect),
("subdomain_enumerate", test_subdomain_enumerate),
]
scan_results: dict[str, dict | None] = {}
summary: list[tuple[str, bool, float, list[str]]] = [] # (name, passed, duration, errors)
for tool_name, test_fn in tests:
print_section(tool_name)
start = time.time()
try:
result, errors = test_fn(mcp)
duration = time.time() - start
passed = len(errors) == 0 and result is not None and "error" not in (result or {})
scan_results[tool_name] = result if passed else None
if result:
print_result_summary(result)
if errors:
print("\n Validation errors:")
for e in errors:
print(e)
print(f"\n Duration: {duration:.2f}s | {'PASS' if passed else 'FAIL'}")
summary.append((tool_name, passed, duration, errors))
except Exception as exc:
duration = time.time() - start
print(f" EXCEPTION: {type(exc).__name__}: {exc}")
scan_results[tool_name] = None
summary.append((tool_name, False, duration, [f" Exception: {exc}"]))
# Risk scorer (pipeline test)
print_section("risk_score (pipeline)")
start = time.time()
try:
result, errors = test_risk_score(mcp, scan_results)
duration = time.time() - start
passed = len(errors) == 0 and result is not None
if result:
print_result_summary(result, max_lines=25)
if errors:
print("\n Validation errors:")
for e in errors:
print(e)
print(f"\n Duration: {duration:.2f}s | {'PASS' if passed else 'FAIL'}")
summary.append(("risk_score", passed, duration, errors))
except Exception as exc:
duration = time.time() - start
print(f" EXCEPTION: {type(exc).__name__}: {exc}")
summary.append(("risk_score", False, duration, [f" Exception: {exc}"]))
# Final summary table
print(f"\n{'=' * 60}")
print(" SUMMARY")
print(f"{'=' * 60}")
print(f" {'Tool':<25} {'Status':<8} {'Time':>6}")
print(f" {'-' * 25} {'-' * 8} {'-' * 6}")
total_pass = 0
total_time = 0.0
for name, passed, duration, _ in summary:
status = "PASS" if passed else "FAIL"
print(f" {name:<25} {status:<8} {duration:>5.2f}s")
total_pass += int(passed)
total_time += duration
print(f" {'-' * 25} {'-' * 8} {'-' * 6}")
print(f" {'Total':<25} {total_pass}/{len(summary):<5} {total_time:>5.2f}s")
# Clean up SSL patch
ctx.__exit__(None, None, None)
# Exit code
if total_pass == len(summary):
print("\n All tools passed!")
sys.exit(0)
else:
failed = [name for name, passed, _, _ in summary if not passed]
print(f"\n Failed tools: {', '.join(failed)}")
sys.exit(1)
if __name__ == "__main__":
main()
Generated
+259 -4
View File
@@ -5,9 +5,12 @@ resolution-markers = [
"python_full_version >= '3.14' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
"python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version < '3.14' and sys_platform == 'win32'",
"python_full_version < '3.14' and sys_platform == 'emscripten'",
"python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version == '3.13.*' and sys_platform == 'win32'",
"python_full_version < '3.13' and sys_platform == 'win32'",
"python_full_version == '3.13.*' and sys_platform == 'emscripten'",
"python_full_version < '3.13' and sys_platform == 'emscripten'",
"python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
"python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'",
]
[manifest]
@@ -931,6 +934,127 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/01/c9/97cc5aae1648dcb851958a3ddf73ccd7dbe5650d95203ecb4d7720b4cdbf/fsspec-2026.1.0-py3-none-any.whl", hash = "sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc", size = 201838, upload-time = "2026-01-09T15:21:34.041Z" },
]
[[package]]
name = "google-api-core"
version = "2.29.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-auth" },
{ name = "googleapis-common-protos" },
{ name = "proto-plus" },
{ name = "protobuf" },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0d/10/05572d33273292bac49c2d1785925f7bc3ff2fe50e3044cf1062c1dde32e/google_api_core-2.29.0.tar.gz", hash = "sha256:84181be0f8e6b04006df75ddfe728f24489f0af57c96a529ff7cf45bc28797f7", size = 177828, upload-time = "2026-01-08T22:21:39.269Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/77/b6/85c4d21067220b9a78cfb81f516f9725ea6befc1544ec9bd2c1acd97c324/google_api_core-2.29.0-py3-none-any.whl", hash = "sha256:d30bc60980daa36e314b5d5a3e5958b0200cb44ca8fa1be2b614e932b75a3ea9", size = 173906, upload-time = "2026-01-08T22:21:36.093Z" },
]
[package.optional-dependencies]
grpc = [
{ name = "grpcio" },
{ name = "grpcio-status" },
]
[[package]]
name = "google-auth"
version = "2.48.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "pyasn1-modules" },
{ name = "rsa" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0c/41/242044323fbd746615884b1c16639749e73665b718209946ebad7ba8a813/google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce", size = 326522, upload-time = "2026-01-26T19:22:47.157Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f", size = 236499, upload-time = "2026-01-26T19:22:45.099Z" },
]
[[package]]
name = "google-cloud-bigquery"
version = "3.40.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-api-core", extra = ["grpc"] },
{ name = "google-auth" },
{ name = "google-cloud-core" },
{ name = "google-resumable-media" },
{ name = "packaging" },
{ name = "python-dateutil" },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/11/0c/153ee546c288949fcc6794d58811ab5420f3ecad5fa7f9e73f78d9512a6e/google_cloud_bigquery-3.40.1.tar.gz", hash = "sha256:75afcfb6e007238fe1deefb2182105249321145ff921784fe7b1de2b4ba24506", size = 511761, upload-time = "2026-02-12T18:44:18.958Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/f5/081cf5b90adfe524ae0d671781b0d497a75a0f2601d075af518828e22d8f/google_cloud_bigquery-3.40.1-py3-none-any.whl", hash = "sha256:9082a6b8193aba87bed6a2c79cf1152b524c99bb7e7ac33a785e333c09eac868", size = 262018, upload-time = "2026-02-12T18:44:16.913Z" },
]
[[package]]
name = "google-cloud-core"
version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-api-core" },
{ name = "google-auth" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a6/03/ef0bc99d0e0faf4fdbe67ac445e18cdaa74824fd93cd069e7bb6548cb52d/google_cloud_core-2.5.0.tar.gz", hash = "sha256:7c1b7ef5c92311717bd05301aa1a91ffbc565673d3b0b4163a52d8413a186963", size = 36027, upload-time = "2025-10-29T23:17:39.513Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/89/20/bfa472e327c8edee00f04beecc80baeddd2ab33ee0e86fd7654da49d45e9/google_cloud_core-2.5.0-py3-none-any.whl", hash = "sha256:67d977b41ae6c7211ee830c7912e41003ea8194bff15ae7d72fd6f51e57acabc", size = 29469, upload-time = "2025-10-29T23:17:38.548Z" },
]
[[package]]
name = "google-crc32c"
version = "1.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8", size = 31298, upload-time = "2025-12-16T00:20:32.241Z" },
{ url = "https://files.pythonhosted.org/packages/c5/b8/f8413d3f4b676136e965e764ceedec904fe38ae8de0cdc52a12d8eb1096e/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7", size = 30872, upload-time = "2025-12-16T00:33:58.785Z" },
{ url = "https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15", size = 33243, upload-time = "2025-12-16T00:40:21.46Z" },
{ url = "https://files.pythonhosted.org/packages/71/03/4820b3bd99c9653d1a5210cb32f9ba4da9681619b4d35b6a052432df4773/google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a", size = 33608, upload-time = "2025-12-16T00:40:22.204Z" },
{ url = "https://files.pythonhosted.org/packages/7c/43/acf61476a11437bf9733fb2f70599b1ced11ec7ed9ea760fdd9a77d0c619/google_crc32c-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2", size = 34439, upload-time = "2025-12-16T00:35:20.458Z" },
{ url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300, upload-time = "2025-12-16T00:21:56.723Z" },
{ url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867, upload-time = "2025-12-16T00:38:31.302Z" },
{ url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364, upload-time = "2025-12-16T00:40:22.96Z" },
{ url = "https://files.pythonhosted.org/packages/21/3f/3457ea803db0198c9aaca2dd373750972ce28a26f00544b6b85088811939/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454", size = 33740, upload-time = "2025-12-16T00:40:23.96Z" },
{ url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437, upload-time = "2025-12-16T00:35:21.395Z" },
{ url = "https://files.pythonhosted.org/packages/d1/db/000f15b41724589b0e7bc24bc7a8967898d8d3bc8caf64c513d91ef1f6c0/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b", size = 31297, upload-time = "2025-12-16T00:23:20.709Z" },
{ url = "https://files.pythonhosted.org/packages/d7/0d/8ebed0c39c53a7e838e2a486da8abb0e52de135f1b376ae2f0b160eb4c1a/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27", size = 30867, upload-time = "2025-12-16T00:43:14.628Z" },
{ url = "https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa", size = 33344, upload-time = "2025-12-16T00:40:24.742Z" },
{ url = "https://files.pythonhosted.org/packages/1c/e8/b33784d6fc77fb5062a8a7854e43e1e618b87d5ddf610a88025e4de6226e/google_crc32c-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8", size = 33694, upload-time = "2025-12-16T00:40:25.505Z" },
{ url = "https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f", size = 34435, upload-time = "2025-12-16T00:35:22.107Z" },
{ url = "https://files.pythonhosted.org/packages/21/88/8ecf3c2b864a490b9e7010c84fd203ec8cf3b280651106a3a74dd1b0ca72/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697", size = 31301, upload-time = "2025-12-16T00:24:48.527Z" },
{ url = "https://files.pythonhosted.org/packages/36/c6/f7ff6c11f5ca215d9f43d3629163727a272eabc356e5c9b2853df2bfe965/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651", size = 30868, upload-time = "2025-12-16T00:48:12.163Z" },
{ url = "https://files.pythonhosted.org/packages/56/15/c25671c7aad70f8179d858c55a6ae8404902abe0cdcf32a29d581792b491/google_crc32c-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2", size = 33381, upload-time = "2025-12-16T00:40:26.268Z" },
{ url = "https://files.pythonhosted.org/packages/42/fa/f50f51260d7b0ef5d4898af122d8a7ec5a84e2984f676f746445f783705f/google_crc32c-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21", size = 33734, upload-time = "2025-12-16T00:40:27.028Z" },
{ url = "https://files.pythonhosted.org/packages/08/a5/7b059810934a09fb3ccb657e0843813c1fee1183d3bc2c8041800374aa2c/google_crc32c-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2", size = 34878, upload-time = "2025-12-16T00:35:23.142Z" },
{ url = "https://files.pythonhosted.org/packages/52/c5/c171e4d8c44fec1422d801a6d2e5d7ddabd733eeda505c79730ee9607f07/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93", size = 28615, upload-time = "2025-12-16T00:40:29.298Z" },
{ url = "https://files.pythonhosted.org/packages/9c/97/7d75fe37a7a6ed171a2cf17117177e7aab7e6e0d115858741b41e9dd4254/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c", size = 28800, upload-time = "2025-12-16T00:40:30.322Z" },
]
[[package]]
name = "google-resumable-media"
version = "2.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-crc32c" },
]
sdist = { url = "https://files.pythonhosted.org/packages/64/d7/520b62a35b23038ff005e334dba3ffc75fcf583bee26723f1fd8fd4b6919/google_resumable_media-2.8.0.tar.gz", hash = "sha256:f1157ed8b46994d60a1bc432544db62352043113684d4e030ee02e77ebe9a1ae", size = 2163265, upload-time = "2025-11-17T15:38:06.659Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/0b/93afde9cfe012260e9fe1522f35c9b72d6ee222f316586b1f23ecf44d518/google_resumable_media-2.8.0-py3-none-any.whl", hash = "sha256:dd14a116af303845a8d932ddae161a26e86cc229645bc98b39f026f9b1717582", size = 81340, upload-time = "2025-11-17T15:38:05.594Z" },
]
[[package]]
name = "googleapis-common-protos"
version = "1.72.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" },
]
[[package]]
name = "greenlet"
version = "3.3.1"
@@ -983,6 +1107,71 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/2b/98c7f93e6db9977aaee07eb1e51ca63bd5f779b900d362791d3252e60558/greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451", size = 233181, upload-time = "2026-01-23T15:33:00.29Z" },
]
[[package]]
name = "grpcio"
version = "1.78.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/86/c7/d0b780a29b0837bf4ca9580904dfb275c1fc321ded7897d620af7047ec57/grpcio-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6", size = 5951525, upload-time = "2026-02-06T09:55:01.989Z" },
{ url = "https://files.pythonhosted.org/packages/c5/b1/96920bf2ee61df85a9503cb6f733fe711c0ff321a5a697d791b075673281/grpcio-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e", size = 11830418, upload-time = "2026-02-06T09:55:04.462Z" },
{ url = "https://files.pythonhosted.org/packages/83/0c/7c1528f098aeb75a97de2bae18c530f56959fb7ad6c882db45d9884d6edc/grpcio-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911", size = 6524477, upload-time = "2026-02-06T09:55:07.111Z" },
{ url = "https://files.pythonhosted.org/packages/8d/52/e7c1f3688f949058e19a011c4e0dec973da3d0ae5e033909677f967ae1f4/grpcio-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e", size = 7198266, upload-time = "2026-02-06T09:55:10.016Z" },
{ url = "https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303", size = 6730552, upload-time = "2026-02-06T09:55:12.207Z" },
{ url = "https://files.pythonhosted.org/packages/bd/98/b8ee0158199250220734f620b12e4a345955ac7329cfd908d0bf0fda77f0/grpcio-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04", size = 7304296, upload-time = "2026-02-06T09:55:15.044Z" },
{ url = "https://files.pythonhosted.org/packages/bd/0f/7b72762e0d8840b58032a56fdbd02b78fc645b9fa993d71abf04edbc54f4/grpcio-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec", size = 8288298, upload-time = "2026-02-06T09:55:17.276Z" },
{ url = "https://files.pythonhosted.org/packages/24/ae/ae4ce56bc5bb5caa3a486d60f5f6083ac3469228faa734362487176c15c5/grpcio-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074", size = 7730953, upload-time = "2026-02-06T09:55:19.545Z" },
{ url = "https://files.pythonhosted.org/packages/b5/6e/8052e3a28eb6a820c372b2eb4b5e32d195c661e137d3eca94d534a4cfd8a/grpcio-1.78.0-cp311-cp311-win32.whl", hash = "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856", size = 4076503, upload-time = "2026-02-06T09:55:21.521Z" },
{ url = "https://files.pythonhosted.org/packages/08/62/f22c98c5265dfad327251fa2f840b591b1df5f5e15d88b19c18c86965b27/grpcio-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558", size = 4799767, upload-time = "2026-02-06T09:55:24.107Z" },
{ url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985, upload-time = "2026-02-06T09:55:26.832Z" },
{ url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853, upload-time = "2026-02-06T09:55:29.224Z" },
{ url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766, upload-time = "2026-02-06T09:55:31.825Z" },
{ url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027, upload-time = "2026-02-06T09:55:34.7Z" },
{ url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766, upload-time = "2026-02-06T09:55:36.902Z" },
{ url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161, upload-time = "2026-02-06T09:55:39.824Z" },
{ url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303, upload-time = "2026-02-06T09:55:42.353Z" },
{ url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222, upload-time = "2026-02-06T09:55:44.629Z" },
{ url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123, upload-time = "2026-02-06T09:55:47.644Z" },
{ url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657, upload-time = "2026-02-06T09:55:49.86Z" },
{ url = "https://files.pythonhosted.org/packages/05/a9/8f75894993895f361ed8636cd9237f4ab39ef87fd30db17467235ed1c045/grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", size = 5920143, upload-time = "2026-02-06T09:55:52.035Z" },
{ url = "https://files.pythonhosted.org/packages/55/06/0b78408e938ac424100100fd081189451b472236e8a3a1f6500390dc4954/grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", size = 11803926, upload-time = "2026-02-06T09:55:55.494Z" },
{ url = "https://files.pythonhosted.org/packages/88/93/b59fe7832ff6ae3c78b813ea43dac60e295fa03606d14d89d2e0ec29f4f3/grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", size = 6478628, upload-time = "2026-02-06T09:55:58.533Z" },
{ url = "https://files.pythonhosted.org/packages/ed/df/e67e3734527f9926b7d9c0dde6cd998d1d26850c3ed8eeec81297967ac67/grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb", size = 7173574, upload-time = "2026-02-06T09:56:01.786Z" },
{ url = "https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5", size = 6692639, upload-time = "2026-02-06T09:56:04.529Z" },
{ url = "https://files.pythonhosted.org/packages/bf/9a/289c32e301b85bdb67d7ec68b752155e674ee3ba2173a1858f118e399ef3/grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9", size = 7268838, upload-time = "2026-02-06T09:56:08.397Z" },
{ url = "https://files.pythonhosted.org/packages/0e/79/1be93f32add280461fa4773880196572563e9c8510861ac2da0ea0f892b6/grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702", size = 8251878, upload-time = "2026-02-06T09:56:10.914Z" },
{ url = "https://files.pythonhosted.org/packages/65/65/793f8e95296ab92e4164593674ae6291b204bb5f67f9d4a711489cd30ffa/grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20", size = 7695412, upload-time = "2026-02-06T09:56:13.593Z" },
{ url = "https://files.pythonhosted.org/packages/1c/9f/1e233fe697ecc82845942c2822ed06bb522e70d6771c28d5528e4c50f6a4/grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670", size = 4064899, upload-time = "2026-02-06T09:56:15.601Z" },
{ url = "https://files.pythonhosted.org/packages/4d/27/d86b89e36de8a951501fb06a0f38df19853210f341d0b28f83f4aa0ffa08/grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", size = 4797393, upload-time = "2026-02-06T09:56:17.882Z" },
{ url = "https://files.pythonhosted.org/packages/29/f2/b56e43e3c968bfe822fa6ce5bca10d5c723aa40875b48791ce1029bb78c7/grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e", size = 5920591, upload-time = "2026-02-06T09:56:20.758Z" },
{ url = "https://files.pythonhosted.org/packages/5d/81/1f3b65bd30c334167bfa8b0d23300a44e2725ce39bba5b76a2460d85f745/grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f", size = 11813685, upload-time = "2026-02-06T09:56:24.315Z" },
{ url = "https://files.pythonhosted.org/packages/0e/1c/bbe2f8216a5bd3036119c544d63c2e592bdf4a8ec6e4a1867592f4586b26/grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724", size = 6487803, upload-time = "2026-02-06T09:56:27.367Z" },
{ url = "https://files.pythonhosted.org/packages/16/5c/a6b2419723ea7ddce6308259a55e8e7593d88464ce8db9f4aa857aba96fa/grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b", size = 7173206, upload-time = "2026-02-06T09:56:29.876Z" },
{ url = "https://files.pythonhosted.org/packages/df/1e/b8801345629a415ea7e26c83d75eb5dbe91b07ffe5210cc517348a8d4218/grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7", size = 6693826, upload-time = "2026-02-06T09:56:32.305Z" },
{ url = "https://files.pythonhosted.org/packages/34/84/0de28eac0377742679a510784f049738a80424b17287739fc47d63c2439e/grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452", size = 7277897, upload-time = "2026-02-06T09:56:34.915Z" },
{ url = "https://files.pythonhosted.org/packages/ca/9c/ad8685cfe20559a9edb66f735afdcb2b7d3de69b13666fdfc542e1916ebd/grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127", size = 8252404, upload-time = "2026-02-06T09:56:37.553Z" },
{ url = "https://files.pythonhosted.org/packages/3c/05/33a7a4985586f27e1de4803887c417ec7ced145ebd069bc38a9607059e2b/grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65", size = 7696837, upload-time = "2026-02-06T09:56:40.173Z" },
{ url = "https://files.pythonhosted.org/packages/73/77/7382241caf88729b106e49e7d18e3116216c778e6a7e833826eb96de22f7/grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c", size = 4142439, upload-time = "2026-02-06T09:56:43.258Z" },
{ url = "https://files.pythonhosted.org/packages/48/b2/b096ccce418882fbfda4f7496f9357aaa9a5af1896a9a7f60d9f2b275a06/grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb", size = 4929852, upload-time = "2026-02-06T09:56:45.885Z" },
]
[[package]]
name = "grpcio-status"
version = "1.78.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "googleapis-common-protos" },
{ name = "grpcio" },
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8a/cd/89ce482a931b543b92cdd9b2888805518c4620e0094409acb8c81dd4610a/grpcio_status-1.78.0.tar.gz", hash = "sha256:a34cfd28101bfea84b5aa0f936b4b423019e9213882907166af6b3bddc59e189", size = 13808, upload-time = "2026-02-06T10:01:48.034Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/83/8a/1241ec22c41028bddd4a052ae9369267b4475265ad0ce7140974548dc3fa/grpcio_status-1.78.0-py3-none-any.whl", hash = "sha256:b492b693d4bf27b47a6c32590701724f1d3b9444b36491878fb71f6208857f34", size = 14523, upload-time = "2026-02-06T10:01:32.584Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
@@ -2168,6 +2357,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" },
]
[[package]]
name = "proto-plus"
version = "1.27.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3a/02/8832cde80e7380c600fbf55090b6ab7b62bd6825dbedde6d6657c15a1f8e/proto_plus-1.27.1.tar.gz", hash = "sha256:912a7460446625b792f6448bade9e55cd4e41e6ac10e27009ef71a7f317fa147", size = 56929, upload-time = "2026-02-02T17:34:49.035Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/79/ac273cbbf744691821a9cca88957257f41afe271637794975ca090b9588b/proto_plus-1.27.1-py3-none-any.whl", hash = "sha256:e4643061f3a4d0de092d62aa4ad09fa4756b2cbb89d4627f3985018216f9fefc", size = 50480, upload-time = "2026-02-02T17:34:47.339Z" },
]
[[package]]
name = "protobuf"
version = "6.33.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" },
{ url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" },
{ url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" },
{ url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" },
{ url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" },
{ url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" },
{ url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" },
]
[[package]]
name = "py-key-value-aio"
version = "0.3.0"
@@ -2209,6 +2425,27 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/51/e4/b8b0a03ece72f47dce2307d36e1c34725b7223d209fc679315ffe6a4e2c3/py_key_value_shared-0.3.0-py3-none-any.whl", hash = "sha256:5b0efba7ebca08bb158b1e93afc2f07d30b8f40c2fc12ce24a4c0d84f42f9298", size = 19560, upload-time = "2025-11-17T16:50:05.954Z" },
]
[[package]]
name = "pyasn1"
version = "0.6.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" },
]
[[package]]
name = "pyasn1-modules"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyasn1" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" },
]
[[package]]
name = "pycparser"
version = "3.0"
@@ -2911,6 +3148,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" },
]
[[package]]
name = "rsa"
version = "4.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyasn1" },
]
sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
]
[[package]]
name = "ruff"
version = "0.15.0"
@@ -3142,11 +3391,15 @@ dependencies = [
[package.optional-dependencies]
all = [
{ name = "duckdb" },
{ name = "google-cloud-bigquery" },
{ name = "openpyxl" },
{ name = "pillow" },
{ name = "pytesseract" },
{ name = "restrictedpython" },
]
bigquery = [
{ name = "google-cloud-bigquery" },
]
dev = [
{ name = "pytest" },
{ name = "pytest-asyncio" },
@@ -3181,6 +3434,8 @@ requires-dist = [
{ name = "duckdb", marker = "extra == 'sql'", specifier = ">=1.0.0" },
{ name = "fastmcp", specifier = ">=2.0.0" },
{ name = "framework", editable = "core" },
{ name = "google-cloud-bigquery", marker = "extra == 'all'", specifier = ">=3.0.0" },
{ name = "google-cloud-bigquery", marker = "extra == 'bigquery'", specifier = ">=3.0.0" },
{ name = "httpx", specifier = ">=0.27.0" },
{ name = "jsonpath-ng", specifier = ">=1.6.0" },
{ name = "litellm", specifier = ">=1.81.0" },
@@ -3202,7 +3457,7 @@ requires-dist = [
{ name = "restrictedpython", marker = "extra == 'all'", specifier = ">=7.0" },
{ name = "restrictedpython", marker = "extra == 'sandbox'", specifier = ">=7.0" },
]
provides-extras = ["dev", "sandbox", "ocr", "excel", "sql", "all"]
provides-extras = ["dev", "sandbox", "ocr", "excel", "sql", "bigquery", "all"]
[package.metadata.requires-dev]
dev = [