lint fixes
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user