initial commit

This commit is contained in:
2026-05-11 12:36:20 +05:30
commit 384cbe8019
15377 changed files with 2360544 additions and 0 deletions

Binary file not shown.

549
ai_service.py Normal file
View File

@@ -0,0 +1,549 @@
from fastapi import FastAPI
from pydantic import BaseModel
import json
import requests
import re
import traceback
import duckdb
import math
import os
from typing import Optional, Any, TypedDict, List, Dict, cast
from datetime import datetime, date
from decimal import Decimal
import time
# ---------------------------------------------------
# SCHEMAS (Pydantic for strict enforcement)
# ---------------------------------------------------
class ChartConfig(BaseModel):
type: str # e.g., "bar", "pie", "line", "area", "table", "kpi"
title: str
x_axis: Optional[str] = None
y_axis: Optional[str] = None
data: List[Dict[str, Any]]
class AnalyticsResponse(BaseModel):
analysis: str
charts: List[ChartConfig]
from fastapi.middleware.cors import CORSMiddleware
from langgraph.graph import StateGraph, END
# ---------------------------------------------------
# CONFIG
# ---------------------------------------------------
OLLAMA_URL = "http://localhost:11434/api/chat"
MODEL = "qwen2.5:3b"
# DuckDB should connect to a local persistent file
DUCKDB_PATH = "analytics.duckdb"
DEBUG_LOG = "debug.log"
# ---------------------------------------------------
# FASTAPI
# ---------------------------------------------------
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ---------------------------------------------------
# DEBUG LOGGER
# ---------------------------------------------------
def log_debug(msg: str):
with open(DEBUG_LOG, "a", encoding="utf-8") as f:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
f.write(f"[{timestamp}] {msg}\n")
def log_node_duration(label: str, start_time: float, extra: str = ""):
duration = time.perf_counter() - start_time
print(f"[{label}] {duration:.2f}s {extra}")
# ---------------------------------------------------
# DUCKDB INIT
# ---------------------------------------------------
def init_duckdb(conn):
try:
log_debug("Initializing DuckDB with live S3 data")
# Load httpfs extension for S3 access
conn.execute("INSTALL httpfs; LOAD httpfs;")
# S3 configuration matching the environment
conn.execute("SET s3_region='sgp1';")
conn.execute("SET s3_endpoint='sgp1.digitaloceanspaces.com';")
conn.execute("SET s3_url_style='path';")
# Create a view from the S3 parquet files
s3_path = 's3://nearle/parquet/deliveries/*.parquet'
conn.execute(f"""
CREATE OR REPLACE VIEW deliveries AS
SELECT * FROM read_parquet('{s3_path}', union_by_name = true)
""")
log_debug(f"DuckDB ready with view 'deliveries' from {s3_path}")
except Exception as e:
log_debug(f"DuckDB init error: {e}")
pass
# ---------------------------------------------------
# REQUEST MODEL
# ---------------------------------------------------
class ChatRequest(BaseModel):
message: str
data: Optional[Any] = None
# ---------------------------------------------------
# LANGGRAPH STATE
# ---------------------------------------------------
class WorkflowState(TypedDict):
question: str
query_plan: Dict[str, Any]
sql_query: str
data: List[Dict[str, Any]]
analysis: str
insights: List[str]
data_quality: Dict[str, Any]
final_response: Dict[str, Any]
error: Optional[str]
iteration_count: int
# ---------------------------------------------------
# OLLAMA CALL
# ---------------------------------------------------
def call_ollama(system_prompt: str, user_prompt: str) -> str:
for attempt in range(3):
try:
payload = {
"model": MODEL,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
"stream": False,
"options": {
"temperature": 0,
"num_predict": 200, # 🔥 increased to prevent cutoff
}
}
r = requests.post(OLLAMA_URL, json=payload, timeout=20)
r.raise_for_status()
content = r.json().get("message", {}).get("content", "")
if content and content.strip() != "":
return content
log_debug(f"Ollama empty response on attempt {attempt + 1}")
except Exception as e:
log_debug(f"Ollama call error: {e} on attempt {attempt + 1}")
time.sleep(1)
return ""
# ---------------------------------------------------
# UTILITIES
# ---------------------------------------------------
def safe_parse_json(text: str) -> Dict[str, Any]:
"""Robustly parse JSON from LLM response."""
try:
return json.loads(text.strip())
except json.JSONDecodeError:
match = re.search(r"```json\s*(.*?)\s*```", text, re.S)
if match:
try:
return json.loads(match.group(1).strip())
except: pass
start = text.find("{")
end = text.rfind("}")
if start != -1 and end != -1:
try:
return json.loads(text[start:end+1])
except: pass
raise ValueError("Could not find valid JSON in LLM response")
# ---------------------------------------------------
# NODES
# ---------------------------------------------------
def query_planner_node(state: WorkflowState) -> WorkflowState:
log_debug(f"NODE: Query Planner - {state['question']}")
print(f"\n🔍 NODE: PLANNER (Iteration: {state.get('iteration_count', 0)})")
start = time.perf_counter()
system = """You are an AI SQL Planner.
Return ONLY valid JSON. No explanation.
---
## DATABASE
Table: deliveries
Columns:
* orderid
* tenantname
* deliveryamt (can be TEXT or NUMERIC)
* deliverydate
---
## RULES
1. Revenue = SUM(CAST(deliveryamt AS NUMERIC))
2. Never use:
* orderamount
* unsafe filters
* TRIM unless needed
3. SQL must NEVER fail:
* Always CAST for SUM
* Keep query simple
4. If previous query failed:
* Use ERROR below to FIX SQL
* NEVER repeat same query
ERROR:
{error}
---
## OUTPUT FORMAT
You MUST return a JSON object containing EXACTLY these 6 keys:
{
"intent": "kpi | trend | comparison | table",
"sql": "valid SQL query",
"chart_type": "kpi | bar | line | pie | table",
"x_axis": "column or null",
"y_axis": "column or null",
"explanation": "short explanation"
}"""
error_msg = state.get("error", "")
system = system.replace("{error}", str(error_msg))
error_msg = state.get("error")
if error_msg and state.get("iteration_count", 0) > 0:
log_debug(f"Retrying SQL generation due to error: {error_msg}")
user_prompt = f"User Query:\n{state['question']}\n\nWarning: Your previous SQL query failed with this error in DuckDB:\n{error_msg}\n\nPlease fix the SQL query and return the correct JSON plan."
else:
user_prompt = f"User Query:\n{state['question']}"
result = call_ollama(system, user_prompt)
print("\n--- RAW LLM RESPONSE ---")
print(result)
print("------------------------\n")
print("RESPONSE LENGTH:", len(result))
if not result or result.strip() == "":
raise Exception("LLM returned empty response")
try:
plan = safe_parse_json(result)
# Strict validation
for key in ["intent", "sql", "chart_type", "y_axis"]:
if key not in plan:
raise ValueError(f"Missing required key: {key}")
if plan.get("intent") == "ranking" and "GROUP BY" not in str(plan.get("sql", "")).upper():
raise ValueError("Ranking query SQL must contain a GROUP BY clause")
print("\n--- PARSED PLAN ---")
print(json.dumps(plan, indent=2))
print("-------------------\n")
print("\n--- FINAL SQL USED ---")
print(plan["sql"])
print("----------------------\n")
except Exception as e:
log_debug(f"Planner JSON parsing error: {str(e)} for output: {result}")
print(f"\n❌ ERROR: Invalid LLM response. Original error: {str(e)}")
raise Exception("Invalid LLM response")
log_node_duration("⏱️ Planner", start)
return {**state, "query_plan": plan, "sql_query": plan.get("sql", ""), "error": None}
def execute_sql_node(state: WorkflowState) -> WorkflowState:
log_debug(f"NODE: Execute SQL - {state['sql_query']}")
print(f"\n🗄️ NODE: SQL EXECUTION")
start = time.perf_counter()
try:
conn = duckdb.connect(DUCKDB_PATH)
init_duckdb(conn)
df = conn.execute(state["sql_query"]).df()
sql_results = df.to_dict("records")
conn.close()
# Sanitize data for JSON response
clean_data = []
for r in sql_results:
row_clean = {}
for k, v in r.items():
if isinstance(v, Decimal): v = float(v)
if isinstance(v, float) and (math.isnan(v) or math.isinf(v)): v = 0.0
if isinstance(v, (datetime, date)):
# Format as clean YYYY-MM-DD to save chart axis space
v = v.strftime("%Y-%m-%d")
# Replace bad string primitives from DB with recognizable null equivalent
if v is None or (isinstance(v, str) and v.strip() == ""):
# Ignore for numeric indicators
if not ("orders" in k or "revenue" in k or "avg_order_value" in k or "charges" in k):
v = "Unknown"
row_clean[k] = v
clean_data.append(row_clean)
log_node_duration("⏱️ QUERY EXECUTION", start, f"({len(clean_data)} rows)")
return {**state, "data": clean_data, "error": None, "iteration_count": int(state.get("iteration_count", 0) or 0) + 1}
except Exception as e:
log_debug(f"SQL Error: {e}")
log_node_duration("⏱️ QUERY EXECUTION", start, "(ERROR)")
return {**state, "error": str(e), "iteration_count": int(state.get("iteration_count", 0) or 0) + 1}
def validate_data_node(state: WorkflowState) -> WorkflowState:
log_debug("NODE: Data Quality Analysis")
warnings = []
if not state.get("data"):
warnings.append("No data returned for your query.")
return {**state, "data_quality": {"warnings": warnings}}
all_zeros = True
for row in state["data"]:
for k, v in row.items():
if isinstance(v, (int, float)) and v > 0:
all_zeros = False
break
if not all_zeros: break
if all_zeros:
warnings.append("All metrics returned zero values.")
return {**state, "data_quality": {"warnings": warnings}}
def analyze_results_node(state: WorkflowState) -> WorkflowState:
log_debug("NODE: Results Analysis")
data = state.get("data", [])
if not data or state.get("error"):
return {**state, "analysis": "No data found to analyze."}
row_count = len(data)
plan = state.get("query_plan", {})
metric = plan.get("y_axis")
dimension = plan.get("x_axis")
if dimension and metric and row_count > 0:
top = data[0]
analysis = f"Analysis of {row_count} records shows that {dimension} '{(top.get(dimension, 'Unknown'))}' is the top entry with {metric} of {top.get(metric, 0)}."
elif metric and row_count > 0:
top = data[0]
analysis = f"Analysis completed. The total {metric} is {top.get(metric, 0)}."
else:
analysis = f"Retrieved {row_count} records for the requested analysis."
return {**state, "analysis": analysis}
def insights_node(state: WorkflowState) -> WorkflowState:
log_debug("NODE: Insights Generation")
print(f"\n💡 NODE: INSIGHTS")
start = time.perf_counter()
data = cast(list, state.get("data", []))
if not data or state.get("error"):
return {**state, "insights": []}
insights = []
plan = state.get("query_plan", {})
metric = plan.get("y_axis")
dimension = plan.get("x_axis")
if metric and dimension and len(data) > 0:
top = data[0]
insights.append(f"Top performer is {top.get(dimension, 'Unknown')} with {top.get(metric, 0)} {metric}.")
if len(data) > 1:
bottom = data[-1]
insights.append(f"Lowest performer is {bottom.get(dimension, 'Unknown')} with {bottom.get(metric, 0)} {metric}.")
try:
total = sum(float(row.get(metric, 0)) for row in data if row.get(metric) is not None)
avg_val = total / len(data)
insights.append(f"The average {metric} across these records is {avg_val:.2f}.")
except:
insights.append("Data volume supports high-confidence trend analysis.")
elif metric and len(data) > 0:
top = data[0]
insights.append(f"The overall {metric} is {top.get(metric, 0)}.")
else:
insights = ["Data supports standard trend analysis."]
if state.get("data_quality") and state["data_quality"].get("warnings"):
for w in state["data_quality"]["warnings"]:
insights.append(f"{w}")
log_node_duration("⏱️ Insights", start)
return {**state, "insights": insights}
def select_visualization_node(state: WorkflowState) -> WorkflowState:
log_debug("NODE: Visualization Selection")
print(f"\n📊 NODE: VISUALIZATION")
start = time.perf_counter()
data = state.get("data", [])
if not data or state.get("error"):
return {**state, "final_response": {
"analysis": state.get("error", "No data available to plot a chart."),
"charts": []
}}
plan = state.get("query_plan", {})
intent = plan.get("intent", "kpi")
chart_type = plan.get("chart_type", "kpi")
x_axis = plan.get("x_axis")
y_axis = plan.get("y_axis")
if intent and "table" in intent.lower():
chart_type = "table"
if intent and "list" in intent.lower():
chart_type = "table"
if chart_type == "kpi" and len(data) == 1:
data = [{**data[0], "summary": "Current Performance"}]
if not x_axis:
x_axis = "summary"
if chart_type == "kpi" and len(data) > 1:
chart_type = "bar"
chart = {
"type": chart_type,
"title": f"{str(intent).replace('_', ' ').title()} Analysis" if intent else "Data Visualization",
"x_axis": x_axis,
"y_axis": y_axis,
"data": data
}
ai_decision = {
"intent": intent,
"chart_type": chart_type,
"x_axis": x_axis,
"y_axis": y_axis,
"reason": "Directly mapped from LLM Engine structured plan.",
}
final_response = {
"analysis": state["analysis"],
"ai_decision": ai_decision,
"insights": state.get("insights", []),
"generated_sql": state.get("sql_query", ""),
"query_plan": plan,
"data_quality": state.get("data_quality", {"warnings": []}),
"charts": [chart],
"explanation": state["analysis"]
}
log_node_duration("⏱️ Visualization", start)
return {**state, "final_response": final_response}
def should_retry(state: WorkflowState):
if state.get("error") and state.get("iteration_count", 0) < 2:
return "retry"
return "continue"
# ---------------------------------------------------
# LANGGRAPH WORKFLOW
# ---------------------------------------------------
workflow = StateGraph(WorkflowState)
workflow.add_node("planner", query_planner_node)
workflow.add_node("exec", execute_sql_node)
workflow.add_node("validate", validate_data_node)
workflow.add_node("analysis", analyze_results_node)
workflow.add_node("insights", insights_node)
workflow.add_node("viz", select_visualization_node)
workflow.set_entry_point("planner")
workflow.add_edge("planner", "exec")
# Conditional edge for retry
workflow.add_conditional_edges(
"exec",
should_retry,
{
"retry": "planner", # Re-run planner if error
"continue": "validate"
}
)
workflow.add_edge("validate", "analysis")
workflow.add_edge("analysis", "insights")
workflow.add_edge("insights", "viz")
workflow.add_edge("viz", END)
graph = workflow.compile()
# ---------------------------------------------------
# API
# ---------------------------------------------------
@app.post("/chat")
def chat(req: ChatRequest):
print(f"\n🚀 NEW REQUEST: {req.message}")
total_start = time.perf_counter()
try:
initial_state = {
"question": req.message,
"query_plan": {},
"sql_query": "",
"data": [],
"analysis": "",
"insights": [],
"data_quality": {"warnings": []},
"final_response": {},
"error": None,
"iteration_count": 0
}
result = graph.invoke(initial_state)
total_duration = time.perf_counter() - total_start
print(f"\n[✅ TOTAL TIME] {total_duration:.2f}s")
return result["final_response"]
except Exception as e:
log_debug(f"Graph overall error: {e}")
return {
"analysis": f"Error: {str(e)}",
"charts": []
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

BIN
analytics.duckdb Normal file

Binary file not shown.

27
check_internal_db.py Normal file
View File

@@ -0,0 +1,27 @@
import duckdb
def check_db():
try:
conn = duckdb.connect('analytics.duckdb')
print("Checking tables/views in analytics.duckdb:")
res = conn.execute("SELECT table_name, table_type FROM information_schema.tables").fetchall()
for r in res:
print(f"Name: {r[0]}, Type: {r[1]}")
print("\nChecking first 5 columns of deliveries:")
try:
print(conn.execute("DESCRIBE deliveries").df().head(5))
except:
print("Could not describe deliveries (missing view?)")
print("\nSampling 5 records from deliveries:")
try:
print(conn.execute("SELECT * FROM deliveries LIMIT 5").df())
except:
print("Could not select from deliveries")
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
check_db()

14
check_live_schema.py Normal file
View File

@@ -0,0 +1,14 @@
import duckdb
def check_schema():
conn = duckdb.connect()
conn.execute("INSTALL httpfs; LOAD httpfs;")
conn.execute("SET s3_region='sgp1';")
conn.execute("SET s3_endpoint='sgp1.digitaloceanspaces.com';")
conn.execute("SET s3_url_style='path';")
df = conn.execute("SELECT * FROM read_parquet('s3://nearle/parquet/deliveries/*.parquet') LIMIT 0").df()
print("Columns:", df.columns.tolist())
if __name__ == "__main__":
check_schema()

35
check_revenue.py Normal file
View File

@@ -0,0 +1,35 @@
import duckdb
import pandas as pd
def check_revenue():
conn = duckdb.connect('analytics.duckdb')
try:
conn.execute("INSTALL httpfs; LOAD httpfs;")
conn.execute("SET s3_region='sgp1';")
conn.execute("SET s3_endpoint='sgp1.digitaloceanspaces.com';")
conn.execute("SET s3_url_style='path';")
# Check top riders by revenue
print("--- TOP 10 RIDERS BY ORDERAMOUNT ---")
sql = """
SELECT
ridername,
SUM(orderamount) as total_order_value,
COUNT(*) as total_deliveries,
SUM(deliveryamt) as total_delivery_fees
FROM read_parquet('s3://nearle/parquet/deliveries/*.parquet', union_by_name = true)
WHERE ridername IS NOT NULL AND ridername != ''
GROUP BY 1
ORDER BY 2 DESC
LIMIT 10
"""
df = conn.execute(sql).df()
print(df)
except Exception as e:
print(f"Error: {e}")
finally:
conn.close()
if __name__ == "__main__":
check_revenue()

5347
debug.log Normal file

File diff suppressed because it is too large Load Diff

43
generate_sample_data.py Normal file
View File

@@ -0,0 +1,43 @@
import duckdb
import pandas as pd
from datetime import datetime, timedelta
import random
def generate_data():
riders = ["Rider Alpha", "Rider Beta", "Rider Gamma", "Rider Delta"]
tenants = ["Tenant A", "Tenant B", "Tenant C"]
statuses = ["completed", "pending", "cancelled"]
data = []
for i in range(50):
rider = random.choice(riders)
tenant = random.choice(tenants)
order_amt = round(random.uniform(20.0, 100.0), 2)
delivery_amt = round(order_amt * 0.1, 2)
delivery_charges = round(random.uniform(2.0, 5.0), 2)
status = random.choice(statuses)
pickup = datetime.now() - timedelta(minutes=random.randint(10, 1000))
# ridertime in minutes
rider_time = random.randint(15, 45)
delivery = pickup + timedelta(minutes=rider_time)
data.append({
"ridername": rider,
"tenantname": tenant,
"orderamount": order_amt,
"deliveryamt": delivery_amt,
"deliverycharges": delivery_charges,
"orderstatus": status,
"pickuptime": pickup.isoformat(),
"deliverytime": delivery.isoformat(),
"ridertime": rider_time # explicitly adding this column for the user's example
})
df = pd.DataFrame(data)
conn = duckdb.connect()
conn.execute("COPY df TO 'c:/bitbucket/backup/analytics/ai-service/data/deliveries.parquet' (FORMAT PARQUET)")
print("Generated c:/bitbucket/backup/analytics/ai-service/data/deliveries.parquet")
if __name__ == "__main__":
generate_data()

15
get_cols_line.py Normal file
View File

@@ -0,0 +1,15 @@
import duckdb
def get_full_schema():
conn = duckdb.connect()
conn.execute("INSTALL httpfs; LOAD httpfs;")
conn.execute("SET s3_region='sgp1';")
conn.execute("SET s3_endpoint='sgp1.digitaloceanspaces.com';")
conn.execute("SET s3_url_style='path';")
rows = conn.execute("DESCRIBE SELECT * FROM read_parquet('s3://nearle/parquet/deliveries/*.parquet')").fetchall()
cols = [row[0] for row in rows]
print(", ".join(cols))
if __name__ == "__main__":
get_full_schema()

16
get_important_cols.py Normal file
View File

@@ -0,0 +1,16 @@
import duckdb
def get_special_cols():
conn = duckdb.connect()
conn.execute("INSTALL httpfs; LOAD httpfs;")
conn.execute("SET s3_region='sgp1';")
conn.execute("SET s3_endpoint='sgp1.digitaloceanspaces.com';")
conn.execute("SET s3_url_style='path';")
rows = conn.execute("DESCRIBE SELECT * FROM read_parquet('s3://nearle/parquet/deliveries/*.parquet')").fetchall()
cols = [row[0] for row in rows]
filtered = [c for c in cols if "name" in c.lower() or "amt" in c.lower() or "amount" in c.lower() or "rider" in c.lower() or "tenant" in c.lower() or "time" in c.lower() or "status" in c.lower()]
print("Filtered Columns:", filtered)
if __name__ == "__main__":
get_special_cols()

14
get_keys.py Normal file
View File

@@ -0,0 +1,14 @@
import duckdb
def get_keys():
conn = duckdb.connect()
conn.execute("INSTALL httpfs; LOAD httpfs;")
conn.execute("SET s3_region='sgp1';")
conn.execute("SET s3_endpoint='sgp1.digitaloceanspaces.com';")
conn.execute("SET s3_url_style='path';")
df = conn.execute("SELECT * FROM read_parquet('s3://nearle/parquet/deliveries/*.parquet') LIMIT 1").df()
print(df.columns.tolist())
if __name__ == "__main__":
get_keys()

15
get_live_schema.py Normal file
View File

@@ -0,0 +1,15 @@
import duckdb
def get_full_schema():
conn = duckdb.connect()
conn.execute("INSTALL httpfs; LOAD httpfs;")
conn.execute("SET s3_region='sgp1';")
conn.execute("SET s3_endpoint='sgp1.digitaloceanspaces.com';")
conn.execute("SET s3_url_style='path';")
rows = conn.execute("DESCRIBE SELECT * FROM read_parquet('s3://nearle/parquet/deliveries/*.parquet')").fetchall()
for row in rows:
print(f"{row[0]}: {row[1]}")
if __name__ == "__main__":
get_full_schema()

15
get_schema_full.py Normal file
View File

@@ -0,0 +1,15 @@
import duckdb
def get_all_cols_types():
conn = duckdb.connect()
conn.execute("INSTALL httpfs; LOAD httpfs;")
conn.execute("SET s3_region='sgp1';")
conn.execute("SET s3_endpoint='sgp1.digitaloceanspaces.com';")
conn.execute("SET s3_url_style='path';")
rows = conn.execute("DESCRIBE SELECT * FROM read_parquet('s3://nearle/parquet/deliveries/*.parquet')").fetchall()
for row in rows:
print(f"{row[0]} ({row[1]})")
if __name__ == "__main__":
get_all_cols_types()

15
get_varchars.py Normal file
View File

@@ -0,0 +1,15 @@
import duckdb
def get_varchar_cols():
conn = duckdb.connect()
conn.execute("INSTALL httpfs; LOAD httpfs;")
conn.execute("SET s3_region='sgp1';")
conn.execute("SET s3_endpoint='sgp1.digitaloceanspaces.com';")
conn.execute("SET s3_url_style='path';")
rows = conn.execute("DESCRIBE SELECT * FROM read_parquet('s3://nearle/parquet/deliveries/*.parquet')").fetchall()
varchars = [row[0] for row in rows if row[1] == 'VARCHAR']
print("VARCHAR Columns:", varchars)
if __name__ == "__main__":
get_varchar_cols()

24
list_s3.py Normal file
View File

@@ -0,0 +1,24 @@
import duckdb
def list_s3_files():
conn = duckdb.connect()
conn.execute("INSTALL httpfs; LOAD httpfs;")
conn.execute("SET s3_region='sgp1';")
conn.execute("SET s3_endpoint='sgp1.digitaloceanspaces.com';")
conn.execute("SET s3_url_style='path';")
# This might not work directly for listing buckets, but let's try reading schema of other potential paths
paths = [
"s3://nearle/parquet/tenants/*.parquet",
"s3://nearle/parquet/riders/*.parquet",
"s3://nearle/parquet/orders/*.parquet"
]
for p in paths:
try:
res = conn.execute(f"SELECT count(*) FROM read_parquet('{p}')").fetchall()
print(f"{p}: {res[0][0]}")
except:
print(f"{p}: Not found")
if __name__ == "__main__":
list_s3_files()

18
live_schema.txt Normal file
View File

@@ -0,0 +1,18 @@
deliveryid (INTEGER)
orderheaderid (INTEGER)
configid (INTEGER)
applocationid (INTEGER)
tenantid (INTEGER)
locationid (INTEGER)
partnerid (INTEGER)
moduleid (INTEGER)
userid (INTEGER)
categoryid (INTEGER)
subcategoryid (INTEGER)
orderid (VARCHAR)
deliverydate (TIMESTAMP WITH TIME ZONE)
orderstatus (VARCHAR)
itemcount (INTEGER)
orderamount (DOUBLE)
created (TIMESTAMP WITH TIME ZONE)
updated (TIMESTAMP WITH TIME ZONE)

43
peek_for_insights.py Normal file
View File

@@ -0,0 +1,43 @@
import duckdb
def peek_data():
try:
conn = duckdb.connect()
conn.execute("INSTALL httpfs; LOAD httpfs;")
conn.execute("SET s3_region='sgp1';")
conn.execute("SET s3_endpoint='sgp1.digitaloceanspaces.com';")
conn.execute("SET s3_url_style='path';")
# Create view
s3_path = 's3://nearle/parquet/deliveries/*.parquet'
conn.execute(f"CREATE VIEW deliveries AS SELECT * FROM read_parquet('{s3_path}', union_by_name = true)")
# Test Query 1
print("Querying Avg Delivery Time...")
df1 = conn.execute("""
SELECT ridername, ROUND(AVG(ridertime), 2) AS avg_delivery_time
FROM deliveries
WHERE ridertime > 0 AND userid IS NOT NULL
GROUP BY ridername
ORDER BY avg_delivery_time ASC
LIMIT 5
""").df()
print(df1)
# Test Query 2
print("\nQuerying Total Revenue...")
df2 = conn.execute("""
SELECT ridername, ROUND(SUM(deliveryamt), 2) AS total_revenue
FROM deliveries
WHERE ridertime > 0 AND userid IS NOT NULL
GROUP BY ridername
ORDER BY total_revenue DESC
LIMIT 5
""").df()
print(df2)
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
peek_data()

15
peek_live.py Normal file
View File

@@ -0,0 +1,15 @@
import duckdb
import json
def peek_data():
conn = duckdb.connect()
conn.execute("INSTALL httpfs; LOAD httpfs;")
conn.execute("SET s3_region='sgp1';")
conn.execute("SET s3_endpoint='sgp1.digitaloceanspaces.com';")
conn.execute("SET s3_url_style='path';")
row = conn.execute("SELECT * FROM read_parquet('s3://nearle/parquet/deliveries/*.parquet') LIMIT 1").df().to_dict('records')[0]
print(json.dumps(row, indent=2, default=str))
if __name__ == "__main__":
peek_data()

5
pyrightconfig.json Normal file
View File

@@ -0,0 +1,5 @@
{
"venvPath": ".",
"venv": "venv",
"pythonPath": "./venv/Scripts/python.exe"
}

7
requirements.txt Normal file
View File

@@ -0,0 +1,7 @@
fastapi
uvicorn
pydantic
requests
duckdb
langgraph
pandas

16
save_schema.py Normal file
View File

@@ -0,0 +1,16 @@
import duckdb
def save_schema():
conn = duckdb.connect()
conn.execute("INSTALL httpfs; LOAD httpfs;")
conn.execute("SET s3_region='sgp1';")
conn.execute("SET s3_endpoint='sgp1.digitaloceanspaces.com';")
conn.execute("SET s3_url_style='path';")
rows = conn.execute("DESCRIBE SELECT * FROM read_parquet('s3://nearle/parquet/deliveries/*.parquet')").fetchall()
with open('live_schema.txt', 'w') as f:
for row in rows:
f.write(f"{row[0]} ({row[1]})\n")
if __name__ == "__main__":
save_schema()

12
test_api.py Normal file
View File

@@ -0,0 +1,12 @@
import requests
import json
url = "http://localhost:8000/chat"
payload = {"message": "Who is the top revenue tenant?"}
try:
response = requests.post(url, json=payload, timeout=120)
data = response.json()
print(f"Analysis: {data.get('analysis')}")
print(f"Generated SQL: {data.get('generated_sql')}")
except Exception as e:
print(f"Error: {e}")

16
test_repro.py Normal file
View File

@@ -0,0 +1,16 @@
import re
import json
def safe_parse_json(text: str):
start = text.find("{")
end = text.rfind("}")
print(f"text type: {type(text)}")
print(f"start: {start}, end: {end}")
if start != -1 and end != -1:
json_str = text[int(start) : int(end) + 1]
print(f"json_str: {json_str}")
return json.loads(json_str)
return None
test_text = "Filler text {\"key\": \"value\"} more filler"
print(safe_parse_json(test_text))

33
test_rider_sql.py Normal file
View File

@@ -0,0 +1,33 @@
import duckdb
def run_test():
try:
conn = duckdb.connect()
conn.execute("INSTALL httpfs; LOAD httpfs;")
conn.execute("SET s3_region='sgp1';")
conn.execute("SET s3_endpoint='sgp1.digitaloceanspaces.com';")
conn.execute("SET s3_url_style='path';")
print("Querying riders with ridertime > 0...")
sql = """
SELECT ridername, ROUND(AVG(ridertime), 2) AS avg_time
FROM read_parquet('s3://nearle/parquet/deliveries/*.parquet', union_by_name = true)
WHERE ridertime > 0 AND ridername IS NOT NULL
GROUP BY ridername
ORDER BY avg_time DESC
LIMIT 10
"""
df = conn.execute(sql).df()
if df.empty:
print("NO DATA RETURNED!")
# Check counts
print("Counts for ridertime > 0:")
print(conn.execute("SELECT COUNT(*) FROM read_parquet('s3://nearle/parquet/deliveries/*.parquet', union_by_name = true) WHERE ridertime > 0").fetchall())
else:
print(df)
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
run_test()

18
test_s3.py Normal file
View File

@@ -0,0 +1,18 @@
import duckdb
def test_s3():
try:
conn = duckdb.connect()
conn.execute("INSTALL httpfs; LOAD httpfs;")
conn.execute("SET s3_region='sgp1';")
conn.execute("SET s3_endpoint='sgp1.digitaloceanspaces.com';")
conn.execute("SET s3_url_style='path';")
# Test if we can see the files
res = conn.execute("SELECT count(*) FROM read_parquet('s3://nearle/parquet/deliveries/*.parquet')").fetchall()
print(f"S3 Count: {res[0][0]}")
except Exception as e:
print(f"Error accessing S3: {e}")
if __name__ == "__main__":
test_s3()

100
tmp_logs.log Normal file
View File

@@ -0,0 +1,100 @@
[2026-03-19 17:56:53] DuckDB ready with view 'deliveries' from s3://nearle/parquet/deliveries/*.parquet
[2026-03-19 17:56:55] NODE: Semantic Column Selector - Who is the top revenue tenant?
[2026-03-19 17:56:55] Selected Columns: ['tenantname', 'orderamount', 'deliveryamt', 'tenantid', 'tenantcontactno', 'tenanttoken', 'tenantsuburb', 'tenantcity', 'tenantaddress', 'othercharges']
[2026-03-19 17:56:55] NODE: Intent Detection - Who is the top revenue tenant?
[2026-03-19 17:56:55] NODE: Metric Selection (Semantic Layer)
[2026-03-19 17:56:55] Semantic Match Found: revenue -> SUM(deliveryamount)
[2026-03-19 17:56:55] NODE: Chart Planning
[2026-03-19 17:57:31] NODE: SQL Generation (Reduced Schema)
[2026-03-19 17:58:29] GEN SQL: Top Performance Comparison -> SELECT
DATE(deliverydate) AS day,
COUNT(deliveryid) AS delivery_count
FROM deliveries
WHERE
deliverydate >= (
SELECT
MAX(deliverydate)
FROM deliveries
) - INTERVAL '30 days'
GROUP BY
day
ORDER BY
SUM(deliveryamount) DESC
LIMIT 10
[2026-03-19 17:58:29] NODE: Batch Execute SQL with Fallback
[2026-03-19 17:58:29] Initializing DuckDB with live S3 data
[2026-03-19 17:58:31] DuckDB ready with view 'deliveries' from s3://nearle/parquet/deliveries/*.parquet
[2026-03-19 17:58:31] EXECUTING SQL for 'Top Performance Comparison':
SELECT
DATE(deliverydate) AS day,
COUNT(deliveryid) AS delivery_count
FROM deliveries
WHERE
deliverydate >= (
SELECT
MAX(deliverydate)
FROM deliveries
) - INTERVAL '30 days'
GROUP BY
day
ORDER BY
SUM(deliveryamount) DESC
LIMIT 10
[2026-03-19 17:58:33] EXEC DONE: Top Performance Comparison found 10 rows
[2026-03-19 17:58:33] NODE: Forecasting Layer
[2026-03-19 17:58:33] NODE: Anomaly Detection
[2026-03-19 17:58:33] NODE: Batch Validation & Smart Suggestions
[2026-03-19 17:58:33] NODE: Results Analysis
[2026-03-19 17:58:57] NODE: Business Insights
[2026-03-19 17:59:50] NODE: Dashboard Assembly
[2026-03-19 19:00:02] NODE: Dataset Summary
[2026-03-19 19:00:02] Initializing DuckDB with live S3 data
[2026-03-19 19:00:03] DuckDB ready with view 'deliveries' from s3://nearle/parquet/deliveries/*.parquet
[2026-03-19 19:00:06] NODE: Semantic Column Selector - Who is the top revenue tenant?
[2026-03-19 19:00:06] Selected Columns: ['tenantname', 'orderamount', 'deliveryamt', 'tenantid', 'tenantcontactno', 'tenanttoken', 'tenantsuburb', 'tenantcity', 'tenantaddress', 'othercharges']
[2026-03-19 19:00:06] NODE: Intent Detection - Who is the top revenue tenant?
[2026-03-19 19:00:06] NODE: Metric Selection (Semantic Layer)
[2026-03-19 19:00:06] Semantic Match Found: revenue -> SUM(deliveryamount)
[2026-03-19 19:00:06] NODE: Chart Planning
[2026-03-19 19:00:43] NODE: SQL Generation (Reduced Schema)
[2026-03-19 19:01:39] GEN SQL: Top Performance Comparison -> SELECT
DATE(deliverydate) AS day,
COUNT(deliveryid) AS delivery_count
FROM deliveries
WHERE
deliverydate >= (
SELECT
MAX(deliverydate)
FROM deliveries
) - INTERVAL '30 days'
GROUP BY
day
ORDER BY
SUM(deliveryamount) DESC
LIMIT 10
[2026-03-19 19:01:39] NODE: Batch Execute SQL with Fallback
[2026-03-19 19:01:39] Initializing DuckDB with live S3 data
[2026-03-19 19:01:40] DuckDB ready with view 'deliveries' from s3://nearle/parquet/deliveries/*.parquet
[2026-03-19 19:01:40] EXECUTING SQL for 'Top Performance Comparison':
SELECT
DATE(deliverydate) AS day,
COUNT(deliveryid) AS delivery_count
FROM deliveries
WHERE
deliverydate >= (
SELECT
MAX(deliverydate)
FROM deliveries
) - INTERVAL '30 days'
GROUP BY
day
ORDER BY
SUM(deliveryamount) DESC
LIMIT 10
[2026-03-19 19:01:42] EXEC DONE: Top Performance Comparison found 10 rows
[2026-03-19 19:01:42] NODE: Forecasting Layer
[2026-03-19 19:01:42] NODE: Anomaly Detection
[2026-03-19 19:01:42] NODE: Batch Validation & Smart Suggestions
[2026-03-19 19:01:42] NODE: Results Analysis
[2026-03-19 19:02:07] NODE: Business Insights
[2026-03-19 19:02:59] NODE: Dashboard Assembly

View File

@@ -0,0 +1,164 @@
/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */
/* Greenlet object interface */
#ifndef Py_GREENLETOBJECT_H
#define Py_GREENLETOBJECT_H
#include <Python.h>
#ifdef __cplusplus
extern "C" {
#endif
/* This is deprecated and undocumented. It does not change. */
#define GREENLET_VERSION "1.0.0"
#ifndef GREENLET_MODULE
#define implementation_ptr_t void*
#endif
typedef struct _greenlet {
PyObject_HEAD
PyObject* weakreflist;
PyObject* dict;
implementation_ptr_t pimpl;
} PyGreenlet;
#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type))
/* C API functions */
/* Total number of symbols that are exported */
#define PyGreenlet_API_pointers 12
#define PyGreenlet_Type_NUM 0
#define PyExc_GreenletError_NUM 1
#define PyExc_GreenletExit_NUM 2
#define PyGreenlet_New_NUM 3
#define PyGreenlet_GetCurrent_NUM 4
#define PyGreenlet_Throw_NUM 5
#define PyGreenlet_Switch_NUM 6
#define PyGreenlet_SetParent_NUM 7
#define PyGreenlet_MAIN_NUM 8
#define PyGreenlet_STARTED_NUM 9
#define PyGreenlet_ACTIVE_NUM 10
#define PyGreenlet_GET_PARENT_NUM 11
#ifndef GREENLET_MODULE
/* This section is used by modules that uses the greenlet C API */
static void** _PyGreenlet_API = NULL;
# define PyGreenlet_Type \
(*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM])
# define PyExc_GreenletError \
((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM])
# define PyExc_GreenletExit \
((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM])
/*
* PyGreenlet_New(PyObject *args)
*
* greenlet.greenlet(run, parent=None)
*/
# define PyGreenlet_New \
(*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \
_PyGreenlet_API[PyGreenlet_New_NUM])
/*
* PyGreenlet_GetCurrent(void)
*
* greenlet.getcurrent()
*/
# define PyGreenlet_GetCurrent \
(*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM])
/*
* PyGreenlet_Throw(
* PyGreenlet *greenlet,
* PyObject *typ,
* PyObject *val,
* PyObject *tb)
*
* g.throw(...)
*/
# define PyGreenlet_Throw \
(*(PyObject * (*)(PyGreenlet * self, \
PyObject * typ, \
PyObject * val, \
PyObject * tb)) \
_PyGreenlet_API[PyGreenlet_Throw_NUM])
/*
* PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args)
*
* g.switch(*args, **kwargs)
*/
# define PyGreenlet_Switch \
(*(PyObject * \
(*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \
_PyGreenlet_API[PyGreenlet_Switch_NUM])
/*
* PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent)
*
* g.parent = new_parent
*/
# define PyGreenlet_SetParent \
(*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \
_PyGreenlet_API[PyGreenlet_SetParent_NUM])
/*
* PyGreenlet_GetParent(PyObject* greenlet)
*
* return greenlet.parent;
*
* This could return NULL even if there is no exception active.
* If it does not return NULL, you are responsible for decrementing the
* reference count.
*/
# define PyGreenlet_GetParent \
(*(PyGreenlet* (*)(PyGreenlet*)) \
_PyGreenlet_API[PyGreenlet_GET_PARENT_NUM])
/*
* deprecated, undocumented alias.
*/
# define PyGreenlet_GET_PARENT PyGreenlet_GetParent
# define PyGreenlet_MAIN \
(*(int (*)(PyGreenlet*)) \
_PyGreenlet_API[PyGreenlet_MAIN_NUM])
# define PyGreenlet_STARTED \
(*(int (*)(PyGreenlet*)) \
_PyGreenlet_API[PyGreenlet_STARTED_NUM])
# define PyGreenlet_ACTIVE \
(*(int (*)(PyGreenlet*)) \
_PyGreenlet_API[PyGreenlet_ACTIVE_NUM])
/* Macro that imports greenlet and initializes C API */
/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we
keep the older definition to be sure older code that might have a copy of
the header still works. */
# define PyGreenlet_Import() \
{ \
_PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \
}
#endif /* GREENLET_MODULE */
#ifdef __cplusplus
}
#endif
#endif /* !Py_GREENLETOBJECT_H */

Binary file not shown.

View File

@@ -0,0 +1,222 @@
# don't import any costly modules
import sys
import os
is_pypy = '__pypy__' in sys.builtin_module_names
def warn_distutils_present():
if 'distutils' not in sys.modules:
return
if is_pypy and sys.version_info < (3, 7):
# PyPy for 3.6 unconditionally imports distutils, so bypass the warning
# https://foss.heptapod.net/pypy/pypy/-/blob/be829135bc0d758997b3566062999ee8b23872b4/lib-python/3/site.py#L250
return
import warnings
warnings.warn(
"Distutils was imported before Setuptools, but importing Setuptools "
"also replaces the `distutils` module in `sys.modules`. This may lead "
"to undesirable behaviors or errors. To avoid these issues, avoid "
"using distutils directly, ensure that setuptools is installed in the "
"traditional way (e.g. not an editable install), and/or make sure "
"that setuptools is always imported before distutils."
)
def clear_distutils():
if 'distutils' not in sys.modules:
return
import warnings
warnings.warn("Setuptools is replacing distutils.")
mods = [
name
for name in sys.modules
if name == "distutils" or name.startswith("distutils.")
]
for name in mods:
del sys.modules[name]
def enabled():
"""
Allow selection of distutils by environment variable.
"""
which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'local')
return which == 'local'
def ensure_local_distutils():
import importlib
clear_distutils()
# With the DistutilsMetaFinder in place,
# perform an import to cause distutils to be
# loaded from setuptools._distutils. Ref #2906.
with shim():
importlib.import_module('distutils')
# check that submodules load as expected
core = importlib.import_module('distutils.core')
assert '_distutils' in core.__file__, core.__file__
assert 'setuptools._distutils.log' not in sys.modules
def do_override():
"""
Ensure that the local copy of distutils is preferred over stdlib.
See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401
for more motivation.
"""
if enabled():
warn_distutils_present()
ensure_local_distutils()
class _TrivialRe:
def __init__(self, *patterns):
self._patterns = patterns
def match(self, string):
return all(pat in string for pat in self._patterns)
class DistutilsMetaFinder:
def find_spec(self, fullname, path, target=None):
# optimization: only consider top level modules and those
# found in the CPython test suite.
if path is not None and not fullname.startswith('test.'):
return
method_name = 'spec_for_{fullname}'.format(**locals())
method = getattr(self, method_name, lambda: None)
return method()
def spec_for_distutils(self):
if self.is_cpython():
return
import importlib
import importlib.abc
import importlib.util
try:
mod = importlib.import_module('setuptools._distutils')
except Exception:
# There are a couple of cases where setuptools._distutils
# may not be present:
# - An older Setuptools without a local distutils is
# taking precedence. Ref #2957.
# - Path manipulation during sitecustomize removes
# setuptools from the path but only after the hook
# has been loaded. Ref #2980.
# In either case, fall back to stdlib behavior.
return
class DistutilsLoader(importlib.abc.Loader):
def create_module(self, spec):
mod.__name__ = 'distutils'
return mod
def exec_module(self, module):
pass
return importlib.util.spec_from_loader(
'distutils', DistutilsLoader(), origin=mod.__file__
)
@staticmethod
def is_cpython():
"""
Suppress supplying distutils for CPython (build and tests).
Ref #2965 and #3007.
"""
return os.path.isfile('pybuilddir.txt')
def spec_for_pip(self):
"""
Ensure stdlib distutils when running under pip.
See pypa/pip#8761 for rationale.
"""
if self.pip_imported_during_build():
return
clear_distutils()
self.spec_for_distutils = lambda: None
@classmethod
def pip_imported_during_build(cls):
"""
Detect if pip is being imported in a build script. Ref #2355.
"""
import traceback
return any(
cls.frame_file_is_setup(frame) for frame, line in traceback.walk_stack(None)
)
@staticmethod
def frame_file_is_setup(frame):
"""
Return True if the indicated frame suggests a setup.py file.
"""
# some frames may not have __file__ (#2940)
return frame.f_globals.get('__file__', '').endswith('setup.py')
def spec_for_sensitive_tests(self):
"""
Ensure stdlib distutils when running select tests under CPython.
python/cpython#91169
"""
clear_distutils()
self.spec_for_distutils = lambda: None
sensitive_tests = (
[
'test.test_distutils',
'test.test_peg_generator',
'test.test_importlib',
]
if sys.version_info < (3, 10)
else [
'test.test_distutils',
]
)
for name in DistutilsMetaFinder.sensitive_tests:
setattr(
DistutilsMetaFinder,
f'spec_for_{name}',
DistutilsMetaFinder.spec_for_sensitive_tests,
)
DISTUTILS_FINDER = DistutilsMetaFinder()
def add_shim():
DISTUTILS_FINDER in sys.meta_path or insert_shim()
class shim:
def __enter__(self):
insert_shim()
def __exit__(self, exc, value, tb):
remove_shim()
def insert_shim():
sys.meta_path.insert(0, DISTUTILS_FINDER)
def remove_shim():
try:
sys.meta_path.remove(DISTUTILS_FINDER)
except ValueError:
pass

View File

@@ -0,0 +1 @@
__import__('_distutils_hack').do_override()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,46 @@
import typing as pytyping
__all__: list[str] = ["ARROW", "DEFAULT", "NATIVE", "SPECIAL", "FunctionNullHandling", "PythonUDFType"]
class FunctionNullHandling:
DEFAULT: pytyping.ClassVar[FunctionNullHandling] # value = <FunctionNullHandling.DEFAULT: 0>
SPECIAL: pytyping.ClassVar[FunctionNullHandling] # value = <FunctionNullHandling.SPECIAL: 1>
__members__: pytyping.ClassVar[
dict[str, FunctionNullHandling]
] # value = {'DEFAULT': <FunctionNullHandling.DEFAULT: 0>, 'SPECIAL': <FunctionNullHandling.SPECIAL: 1>}
def __eq__(self, other: object) -> bool: ...
def __getstate__(self) -> int: ...
def __hash__(self) -> int: ...
def __index__(self) -> int: ...
def __init__(self, value: pytyping.SupportsInt) -> None: ...
def __int__(self) -> int: ...
def __ne__(self, other: object) -> bool: ...
def __setstate__(self, state: pytyping.SupportsInt) -> None: ...
@property
def name(self) -> str: ...
@property
def value(self) -> int: ...
class PythonUDFType:
ARROW: pytyping.ClassVar[PythonUDFType] # value = <PythonUDFType.ARROW: 1>
NATIVE: pytyping.ClassVar[PythonUDFType] # value = <PythonUDFType.NATIVE: 0>
__members__: pytyping.ClassVar[
dict[str, PythonUDFType]
] # value = {'NATIVE': <PythonUDFType.NATIVE: 0>, 'ARROW': <PythonUDFType.ARROW: 1>}
def __eq__(self, other: object) -> bool: ...
def __getstate__(self) -> int: ...
def __hash__(self) -> int: ...
def __index__(self) -> int: ...
def __init__(self, value: pytyping.SupportsInt) -> None: ...
def __int__(self) -> int: ...
def __ne__(self, other: object) -> bool: ...
def __setstate__(self, state: pytyping.SupportsInt) -> None: ...
@property
def name(self) -> str: ...
@property
def value(self) -> int: ...
ARROW: PythonUDFType # value = <PythonUDFType.ARROW: 1>
DEFAULT: FunctionNullHandling # value = <FunctionNullHandling.DEFAULT: 0>
NATIVE: PythonUDFType # value = <PythonUDFType.NATIVE: 0>
SPECIAL: FunctionNullHandling # value = <FunctionNullHandling.SPECIAL: 1>

View File

@@ -0,0 +1,75 @@
import duckdb
import typing as pytyping
__all__: list[str] = [
"BIGINT",
"BIT",
"BLOB",
"BOOLEAN",
"DATE",
"DOUBLE",
"FLOAT",
"HUGEINT",
"INTEGER",
"INTERVAL",
"SMALLINT",
"SQLNULL",
"TIME",
"TIMESTAMP",
"TIMESTAMP_MS",
"TIMESTAMP_NS",
"TIMESTAMP_S",
"TIMESTAMP_TZ",
"TIME_TZ",
"TINYINT",
"UBIGINT",
"UHUGEINT",
"UINTEGER",
"USMALLINT",
"UTINYINT",
"UUID",
"VARCHAR",
"DuckDBPyType",
]
class DuckDBPyType:
def __eq__(self, other: object) -> bool: ...
def __getattr__(self, name: str) -> DuckDBPyType: ...
def __getitem__(self, name: str) -> DuckDBPyType: ...
def __hash__(self) -> int: ...
@pytyping.overload
def __init__(self, type_str: str, connection: duckdb.DuckDBPyConnection) -> None: ...
@pytyping.overload
def __init__(self, obj: object) -> None: ...
@property
def children(self) -> list[tuple[str, object]]: ...
@property
def id(self) -> str: ...
BIGINT: DuckDBPyType # value = BIGINT
BIT: DuckDBPyType # value = BIT
BLOB: DuckDBPyType # value = BLOB
BOOLEAN: DuckDBPyType # value = BOOLEAN
DATE: DuckDBPyType # value = DATE
DOUBLE: DuckDBPyType # value = DOUBLE
FLOAT: DuckDBPyType # value = FLOAT
HUGEINT: DuckDBPyType # value = HUGEINT
INTEGER: DuckDBPyType # value = INTEGER
INTERVAL: DuckDBPyType # value = INTERVAL
SMALLINT: DuckDBPyType # value = SMALLINT
SQLNULL: DuckDBPyType # value = "NULL"
TIME: DuckDBPyType # value = TIME
TIMESTAMP: DuckDBPyType # value = TIMESTAMP
TIMESTAMP_MS: DuckDBPyType # value = TIMESTAMP_MS
TIMESTAMP_NS: DuckDBPyType # value = TIMESTAMP_NS
TIMESTAMP_S: DuckDBPyType # value = TIMESTAMP_S
TIMESTAMP_TZ: DuckDBPyType # value = TIMESTAMP WITH TIME ZONE
TIME_TZ: DuckDBPyType # value = TIME WITH TIME ZONE
TINYINT: DuckDBPyType # value = TINYINT
UBIGINT: DuckDBPyType # value = UBIGINT
UHUGEINT: DuckDBPyType # value = UHUGEINT
UINTEGER: DuckDBPyType # value = UINTEGER
USMALLINT: DuckDBPyType # value = USMALLINT
UTINYINT: DuckDBPyType # value = UTINYINT
UUID: DuckDBPyType # value = UUID
VARCHAR: DuckDBPyType # value = VARCHAR

Binary file not shown.

View File

@@ -0,0 +1,33 @@
# This is a stub package designed to roughly emulate the _yaml
# extension module, which previously existed as a standalone module
# and has been moved into the `yaml` package namespace.
# It does not perfectly mimic its old counterpart, but should get
# close enough for anyone who's relying on it even when they shouldn't.
import yaml
# in some circumstances, the yaml module we imoprted may be from a different version, so we need
# to tread carefully when poking at it here (it may not have the attributes we expect)
if not getattr(yaml, '__with_libyaml__', False):
from sys import version_info
exc = ModuleNotFoundError if version_info >= (3, 6) else ImportError
raise exc("No module named '_yaml'")
else:
from yaml._yaml import *
import warnings
warnings.warn(
'The _yaml extension module is now located at yaml._yaml'
' and its location is subject to change. To use the'
' LibYAML-based parser and emitter, import from `yaml`:'
' `from yaml import CLoader as Loader, CDumper as Dumper`.',
DeprecationWarning
)
del warnings
# Don't `del yaml` here because yaml is actually an existing
# namespace member of _yaml.
__name__ = '_yaml'
# If the module is top-level (i.e. not a part of any specific package)
# then the attribute should be set to ''.
# https://docs.python.org/3.8/library/types.html
__package__ = ''

View File

@@ -0,0 +1,49 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Low-level ADBC bindings for the DuckDB driver."""
import enum
import functools
import importlib.util
import typing
import adbc_driver_manager
class StatementOptions(enum.Enum):
"""Statement options specific to the DuckDB driver."""
#: The number of rows per batch. Defaults to 2048.
BATCH_ROWS = "adbc.duckdb.query.batch_rows"
def connect(path: typing.Optional[str] = None) -> adbc_driver_manager.AdbcDatabase:
"""Create a low level ADBC connection to DuckDB."""
if path is None:
return adbc_driver_manager.AdbcDatabase(driver=driver_path(), entrypoint="duckdb_adbc_init")
return adbc_driver_manager.AdbcDatabase(driver=driver_path(), entrypoint="duckdb_adbc_init", path=path)
@functools.cache
def driver_path() -> str:
"""Get the path to the DuckDB ADBC driver."""
duckdb_module_spec = importlib.util.find_spec("_duckdb")
if duckdb_module_spec is None:
msg = "Could not find duckdb shared library. Did you pip install duckdb?"
raise ImportError(msg)
return duckdb_module_spec.origin

View File

@@ -0,0 +1,115 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""DBAPI 2.0-compatible facade for the ADBC DuckDB driver."""
import typing
import adbc_driver_manager
import adbc_driver_manager.dbapi
import adbc_driver_duckdb
__all__ = [
"BINARY",
"DATETIME",
"NUMBER",
"ROWID",
"STRING",
"Connection",
"Cursor",
"DataError",
"DatabaseError",
"Date",
"DateFromTicks",
"Error",
"IntegrityError",
"InterfaceError",
"InternalError",
"NotSupportedError",
"OperationalError",
"ProgrammingError",
"Time",
"TimeFromTicks",
"Timestamp",
"TimestampFromTicks",
"Warning",
"apilevel",
"connect",
"paramstyle",
"threadsafety",
]
# ----------------------------------------------------------
# Globals
apilevel = adbc_driver_manager.dbapi.apilevel
threadsafety = adbc_driver_manager.dbapi.threadsafety
paramstyle = "qmark"
Warning = adbc_driver_manager.dbapi.Warning
Error = adbc_driver_manager.dbapi.Error
InterfaceError = adbc_driver_manager.dbapi.InterfaceError
DatabaseError = adbc_driver_manager.dbapi.DatabaseError
DataError = adbc_driver_manager.dbapi.DataError
OperationalError = adbc_driver_manager.dbapi.OperationalError
IntegrityError = adbc_driver_manager.dbapi.IntegrityError
InternalError = adbc_driver_manager.dbapi.InternalError
ProgrammingError = adbc_driver_manager.dbapi.ProgrammingError
NotSupportedError = adbc_driver_manager.dbapi.NotSupportedError
# ----------------------------------------------------------
# Types
Date = adbc_driver_manager.dbapi.Date
Time = adbc_driver_manager.dbapi.Time
Timestamp = adbc_driver_manager.dbapi.Timestamp
DateFromTicks = adbc_driver_manager.dbapi.DateFromTicks
TimeFromTicks = adbc_driver_manager.dbapi.TimeFromTicks
TimestampFromTicks = adbc_driver_manager.dbapi.TimestampFromTicks
STRING = adbc_driver_manager.dbapi.STRING
BINARY = adbc_driver_manager.dbapi.BINARY
NUMBER = adbc_driver_manager.dbapi.NUMBER
DATETIME = adbc_driver_manager.dbapi.DATETIME
ROWID = adbc_driver_manager.dbapi.ROWID
# ----------------------------------------------------------
# Functions
def connect(path: typing.Optional[str] = None, **kwargs) -> "Connection":
"""Connect to DuckDB via ADBC."""
db = None
conn = None
try:
db = adbc_driver_duckdb.connect(path)
conn = adbc_driver_manager.AdbcConnection(db)
return adbc_driver_manager.dbapi.Connection(db, conn, **kwargs)
except Exception:
if conn:
conn.close()
if db:
db.close()
raise
# ----------------------------------------------------------
# Classes
Connection = adbc_driver_manager.dbapi.Connection
Cursor = adbc_driver_manager.dbapi.Cursor

View File

@@ -0,0 +1 @@
pip

View File

@@ -0,0 +1,279 @@
A. HISTORY OF THE SOFTWARE
==========================
Python was created in the early 1990s by Guido van Rossum at Stichting
Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands
as a successor of a language called ABC. Guido remains Python's
principal author, although it includes many contributions from others.
In 1995, Guido continued his work on Python at the Corporation for
National Research Initiatives (CNRI, see https://www.cnri.reston.va.us)
in Reston, Virginia where he released several versions of the
software.
In May 2000, Guido and the Python core development team moved to
BeOpen.com to form the BeOpen PythonLabs team. In October of the same
year, the PythonLabs team moved to Digital Creations, which became
Zope Corporation. In 2001, the Python Software Foundation (PSF, see
https://www.python.org/psf/) was formed, a non-profit organization
created specifically to own Python-related Intellectual Property.
Zope Corporation was a sponsoring member of the PSF.
All Python releases are Open Source (see https://opensource.org for
the Open Source Definition). Historically, most, but not all, Python
releases have also been GPL-compatible; the table below summarizes
the various releases.
Release Derived Year Owner GPL-
from compatible? (1)
0.9.0 thru 1.2 1991-1995 CWI yes
1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
1.6 1.5.2 2000 CNRI no
2.0 1.6 2000 BeOpen.com no
1.6.1 1.6 2001 CNRI yes (2)
2.1 2.0+1.6.1 2001 PSF no
2.0.1 2.0+1.6.1 2001 PSF yes
2.1.1 2.1+2.0.1 2001 PSF yes
2.1.2 2.1.1 2002 PSF yes
2.1.3 2.1.2 2002 PSF yes
2.2 and above 2.1.1 2001-now PSF yes
Footnotes:
(1) GPL-compatible doesn't mean that we're distributing Python under
the GPL. All Python licenses, unlike the GPL, let you distribute
a modified version without making your changes open source. The
GPL-compatible licenses make it possible to combine Python with
other software that is released under the GPL; the others don't.
(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
because its license has a choice of law clause. According to
CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
is "not incompatible" with the GPL.
Thanks to the many outside volunteers who have worked under Guido's
direction to make these releases possible.
B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
===============================================================
Python software and documentation are licensed under the
Python Software Foundation License Version 2.
Starting with Python 3.8.6, examples, recipes, and other code in
the documentation are dual licensed under the PSF License Version 2
and the Zero-Clause BSD license.
Some software incorporated into Python is under different licenses.
The licenses are listed with code falling under that license.
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
--------------------------------------------
1. This LICENSE AGREEMENT is between the Python Software Foundation
("PSF"), and the Individual or Organization ("Licensee") accessing and
otherwise using this software ("Python") in source or binary form and
its associated documentation.
2. Subject to the terms and conditions of this License Agreement, PSF hereby
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
analyze, test, perform and/or display publicly, prepare derivative works,
distribute, and otherwise use Python alone or in any derivative version,
provided, however, that PSF's License Agreement and PSF's notice of copyright,
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation;
All Rights Reserved" are retained in Python alone or in any derivative version
prepared by Licensee.
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python.
4. PSF is making Python available to Licensee on an "AS IS"
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. Nothing in this License Agreement shall be deemed to create any
relationship of agency, partnership, or joint venture between PSF and
Licensee. This License Agreement does not grant permission to use PSF
trademarks or trade name in a trademark sense to endorse or promote
products or services of Licensee, or any third party.
8. By copying, installing or otherwise using Python, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.
BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
-------------------------------------------
BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
Individual or Organization ("Licensee") accessing and otherwise using
this software in source or binary form and its associated
documentation ("the Software").
2. Subject to the terms and conditions of this BeOpen Python License
Agreement, BeOpen hereby grants Licensee a non-exclusive,
royalty-free, world-wide license to reproduce, analyze, test, perform
and/or display publicly, prepare derivative works, distribute, and
otherwise use the Software alone or in any derivative version,
provided, however, that the BeOpen Python License is retained in the
Software, alone or in any derivative version prepared by Licensee.
3. BeOpen is making the Software available to Licensee on an "AS IS"
basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
5. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
6. This License Agreement shall be governed by and interpreted in all
respects by the law of the State of California, excluding conflict of
law provisions. Nothing in this License Agreement shall be deemed to
create any relationship of agency, partnership, or joint venture
between BeOpen and Licensee. This License Agreement does not grant
permission to use BeOpen trademarks or trade names in a trademark
sense to endorse or promote products or services of Licensee, or any
third party. As an exception, the "BeOpen Python" logos available at
http://www.pythonlabs.com/logos.html may be used according to the
permissions granted on that web page.
7. By copying, installing or otherwise using the software, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.
CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
---------------------------------------
1. This LICENSE AGREEMENT is between the Corporation for National
Research Initiatives, having an office at 1895 Preston White Drive,
Reston, VA 20191 ("CNRI"), and the Individual or Organization
("Licensee") accessing and otherwise using Python 1.6.1 software in
source or binary form and its associated documentation.
2. Subject to the terms and conditions of this License Agreement, CNRI
hereby grants Licensee a nonexclusive, royalty-free, world-wide
license to reproduce, analyze, test, perform and/or display publicly,
prepare derivative works, distribute, and otherwise use Python 1.6.1
alone or in any derivative version, provided, however, that CNRI's
License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
1995-2001 Corporation for National Research Initiatives; All Rights
Reserved" are retained in Python 1.6.1 alone or in any derivative
version prepared by Licensee. Alternately, in lieu of CNRI's License
Agreement, Licensee may substitute the following text (omitting the
quotes): "Python 1.6.1 is made available subject to the terms and
conditions in CNRI's License Agreement. This Agreement together with
Python 1.6.1 may be located on the internet using the following
unique, persistent identifier (known as a handle): 1895.22/1013. This
Agreement may also be obtained from a proxy server on the internet
using the following URL: http://hdl.handle.net/1895.22/1013".
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python 1.6.1 or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python 1.6.1.
4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. This License Agreement shall be governed by the federal
intellectual property law of the United States, including without
limitation the federal copyright law, and, to the extent such
U.S. federal law does not apply, by the law of the Commonwealth of
Virginia, excluding Virginia's conflict of law provisions.
Notwithstanding the foregoing, with regard to derivative works based
on Python 1.6.1 that incorporate non-separable material that was
previously distributed under the GNU General Public License (GPL), the
law of the Commonwealth of Virginia shall govern this License
Agreement only as to issues arising under or with respect to
Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
License Agreement shall be deemed to create any relationship of
agency, partnership, or joint venture between CNRI and Licensee. This
License Agreement does not grant permission to use CNRI trademarks or
trade name in a trademark sense to endorse or promote products or
services of Licensee, or any third party.
8. By clicking on the "ACCEPT" button where indicated, or by copying,
installing or otherwise using Python 1.6.1, Licensee agrees to be
bound by the terms and conditions of this License Agreement.
ACCEPT
CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
--------------------------------------------------
Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
The Netherlands. All rights reserved.
Permission to use, copy, modify, and distribute this software and its
documentation for any purpose and without fee is hereby granted,
provided that the above copyright notice appear in all copies and that
both that copyright notice and this permission notice appear in
supporting documentation, and that the name of Stichting Mathematisch
Centrum or CWI not be used in advertising or publicity pertaining to
distribution of the software without specific, written prior
permission.
STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION
----------------------------------------------------------------------
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.

View File

@@ -0,0 +1,123 @@
Metadata-Version: 2.3
Name: aiohappyeyeballs
Version: 2.6.1
Summary: Happy Eyeballs for asyncio
License: PSF-2.0
Author: J. Nick Koston
Author-email: nick@koston.org
Requires-Python: >=3.9
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Libraries
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: License :: OSI Approved :: Python Software Foundation License
Project-URL: Bug Tracker, https://github.com/aio-libs/aiohappyeyeballs/issues
Project-URL: Changelog, https://github.com/aio-libs/aiohappyeyeballs/blob/main/CHANGELOG.md
Project-URL: Documentation, https://aiohappyeyeballs.readthedocs.io
Project-URL: Repository, https://github.com/aio-libs/aiohappyeyeballs
Description-Content-Type: text/markdown
# aiohappyeyeballs
<p align="center">
<a href="https://github.com/aio-libs/aiohappyeyeballs/actions/workflows/ci.yml?query=branch%3Amain">
<img src="https://img.shields.io/github/actions/workflow/status/aio-libs/aiohappyeyeballs/ci-cd.yml?branch=main&label=CI&logo=github&style=flat-square" alt="CI Status" >
</a>
<a href="https://aiohappyeyeballs.readthedocs.io">
<img src="https://img.shields.io/readthedocs/aiohappyeyeballs.svg?logo=read-the-docs&logoColor=fff&style=flat-square" alt="Documentation Status">
</a>
<a href="https://codecov.io/gh/aio-libs/aiohappyeyeballs">
<img src="https://img.shields.io/codecov/c/github/aio-libs/aiohappyeyeballs.svg?logo=codecov&logoColor=fff&style=flat-square" alt="Test coverage percentage">
</a>
</p>
<p align="center">
<a href="https://python-poetry.org/">
<img src="https://img.shields.io/badge/packaging-poetry-299bd7?style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAASCAYAAABrXO8xAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAJJSURBVHgBfZLPa1NBEMe/s7tNXoxW1KJQKaUHkXhQvHgW6UHQQ09CBS/6V3hKc/AP8CqCrUcpmop3Cx48eDB4yEECjVQrlZb80CRN8t6OM/teagVxYZi38+Yz853dJbzoMV3MM8cJUcLMSUKIE8AzQ2PieZzFxEJOHMOgMQQ+dUgSAckNXhapU/NMhDSWLs1B24A8sO1xrN4NECkcAC9ASkiIJc6k5TRiUDPhnyMMdhKc+Zx19l6SgyeW76BEONY9exVQMzKExGKwwPsCzza7KGSSWRWEQhyEaDXp6ZHEr416ygbiKYOd7TEWvvcQIeusHYMJGhTwF9y7sGnSwaWyFAiyoxzqW0PM/RjghPxF2pWReAowTEXnDh0xgcLs8l2YQmOrj3N7ByiqEoH0cARs4u78WgAVkoEDIDoOi3AkcLOHU60RIg5wC4ZuTC7FaHKQm8Hq1fQuSOBvX/sodmNJSB5geaF5CPIkUeecdMxieoRO5jz9bheL6/tXjrwCyX/UYBUcjCaWHljx1xiX6z9xEjkYAzbGVnB8pvLmyXm9ep+W8CmsSHQQY77Zx1zboxAV0w7ybMhQmfqdmmw3nEp1I0Z+FGO6M8LZdoyZnuzzBdjISicKRnpxzI9fPb+0oYXsNdyi+d3h9bm9MWYHFtPeIZfLwzmFDKy1ai3p+PDls1Llz4yyFpferxjnyjJDSEy9CaCx5m2cJPerq6Xm34eTrZt3PqxYO1XOwDYZrFlH1fWnpU38Y9HRze3lj0vOujZcXKuuXm3jP+s3KbZVra7y2EAAAAAASUVORK5CYII=" alt="Poetry">
</a>
<a href="https://github.com/astral-sh/ruff">
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff">
</a>
<a href="https://github.com/pre-commit/pre-commit">
<img src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=flat-square" alt="pre-commit">
</a>
</p>
<p align="center">
<a href="https://pypi.org/project/aiohappyeyeballs/">
<img src="https://img.shields.io/pypi/v/aiohappyeyeballs.svg?logo=python&logoColor=fff&style=flat-square" alt="PyPI Version">
</a>
<img src="https://img.shields.io/pypi/pyversions/aiohappyeyeballs.svg?style=flat-square&logo=python&amp;logoColor=fff" alt="Supported Python versions">
<img src="https://img.shields.io/pypi/l/aiohappyeyeballs.svg?style=flat-square" alt="License">
</p>
---
**Documentation**: <a href="https://aiohappyeyeballs.readthedocs.io" target="_blank">https://aiohappyeyeballs.readthedocs.io </a>
**Source Code**: <a href="https://github.com/aio-libs/aiohappyeyeballs" target="_blank">https://github.com/aio-libs/aiohappyeyeballs </a>
---
[Happy Eyeballs](https://en.wikipedia.org/wiki/Happy_Eyeballs)
([RFC 8305](https://www.rfc-editor.org/rfc/rfc8305.html))
## Use case
This library exists to allow connecting with
[Happy Eyeballs](https://en.wikipedia.org/wiki/Happy_Eyeballs)
([RFC 8305](https://www.rfc-editor.org/rfc/rfc8305.html))
when you
already have a list of addrinfo and not a DNS name.
The stdlib version of `loop.create_connection()`
will only work when you pass in an unresolved name which
is not a good fit when using DNS caching or resolving
names via another method such as `zeroconf`.
## Installation
Install this via pip (or your favourite package manager):
`pip install aiohappyeyeballs`
## License
[aiohappyeyeballs is licensed under the same terms as cpython itself.](https://github.com/python/cpython/blob/main/LICENSE)
## Example usage
```python
addr_infos = await loop.getaddrinfo("example.org", 80)
socket = await start_connection(addr_infos)
socket = await start_connection(addr_infos, local_addr_infos=local_addr_infos, happy_eyeballs_delay=0.2)
transport, protocol = await loop.create_connection(
MyProtocol, sock=socket, ...)
# Remove the first address for each family from addr_info
pop_addr_infos_interleave(addr_info, 1)
# Remove all matching address from addr_info
remove_addr_infos(addr_info, "dead::beef::")
# Convert a local_addr to local_addr_infos
local_addr_infos = addr_to_addr_infos(("127.0.0.1",0))
```
## Credits
This package contains code from cpython and is licensed under the same terms as cpython itself.
This package was created with
[Copier](https://copier.readthedocs.io/) and the
[browniebroke/pypackage-template](https://github.com/browniebroke/pypackage-template)
project template.

View File

@@ -0,0 +1,16 @@
aiohappyeyeballs-2.6.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
aiohappyeyeballs-2.6.1.dist-info/LICENSE,sha256=Oy-B_iHRgcSZxZolbI4ZaEVdZonSaaqFNzv7avQdo78,13936
aiohappyeyeballs-2.6.1.dist-info/METADATA,sha256=NSXlhJwAfi380eEjAo7BQ4P_TVal9xi0qkyZWibMsVM,5915
aiohappyeyeballs-2.6.1.dist-info/RECORD,,
aiohappyeyeballs-2.6.1.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
aiohappyeyeballs/__init__.py,sha256=x7kktHEtaD9quBcWDJPuLeKyjuVAI-Jj14S9B_5hcTs,361
aiohappyeyeballs/__pycache__/__init__.cpython-311.pyc,,
aiohappyeyeballs/__pycache__/_staggered.cpython-311.pyc,,
aiohappyeyeballs/__pycache__/impl.cpython-311.pyc,,
aiohappyeyeballs/__pycache__/types.cpython-311.pyc,,
aiohappyeyeballs/__pycache__/utils.cpython-311.pyc,,
aiohappyeyeballs/_staggered.py,sha256=edfVowFx-P-ywJjIEF3MdPtEMVODujV6CeMYr65otac,6900
aiohappyeyeballs/impl.py,sha256=Dlcm2mTJ28ucrGnxkb_fo9CZzLAkOOBizOt7dreBbXE,9681
aiohappyeyeballs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
aiohappyeyeballs/types.py,sha256=YZJIAnyoV4Dz0WFtlaf_OyE4EW7Xus1z7aIfNI6tDDQ,425
aiohappyeyeballs/utils.py,sha256=on9GxIR0LhEfZu8P6Twi9hepX9zDanuZM20MWsb3xlQ,3028

View File

@@ -0,0 +1,4 @@
Wheel-Version: 1.0
Generator: poetry-core 2.1.1
Root-Is-Purelib: true
Tag: py3-none-any

View File

@@ -0,0 +1,14 @@
__version__ = "2.6.1"
from .impl import start_connection
from .types import AddrInfoType, SocketFactoryType
from .utils import addr_to_addr_infos, pop_addr_infos_interleave, remove_addr_infos
__all__ = (
"AddrInfoType",
"SocketFactoryType",
"addr_to_addr_infos",
"pop_addr_infos_interleave",
"remove_addr_infos",
"start_connection",
)

View File

@@ -0,0 +1,207 @@
import asyncio
import contextlib
# PY3.9: Import Callable from typing until we drop Python 3.9 support
# https://github.com/python/cpython/issues/87131
from typing import (
TYPE_CHECKING,
Any,
Awaitable,
Callable,
Iterable,
List,
Optional,
Set,
Tuple,
TypeVar,
Union,
)
_T = TypeVar("_T")
RE_RAISE_EXCEPTIONS = (SystemExit, KeyboardInterrupt)
def _set_result(wait_next: "asyncio.Future[None]") -> None:
"""Set the result of a future if it is not already done."""
if not wait_next.done():
wait_next.set_result(None)
async def _wait_one(
futures: "Iterable[asyncio.Future[Any]]",
loop: asyncio.AbstractEventLoop,
) -> _T:
"""Wait for the first future to complete."""
wait_next = loop.create_future()
def _on_completion(fut: "asyncio.Future[Any]") -> None:
if not wait_next.done():
wait_next.set_result(fut)
for f in futures:
f.add_done_callback(_on_completion)
try:
return await wait_next
finally:
for f in futures:
f.remove_done_callback(_on_completion)
async def staggered_race(
coro_fns: Iterable[Callable[[], Awaitable[_T]]],
delay: Optional[float],
*,
loop: Optional[asyncio.AbstractEventLoop] = None,
) -> Tuple[Optional[_T], Optional[int], List[Optional[BaseException]]]:
"""
Run coroutines with staggered start times and take the first to finish.
This method takes an iterable of coroutine functions. The first one is
started immediately. From then on, whenever the immediately preceding one
fails (raises an exception), or when *delay* seconds has passed, the next
coroutine is started. This continues until one of the coroutines complete
successfully, in which case all others are cancelled, or until all
coroutines fail.
The coroutines provided should be well-behaved in the following way:
* They should only ``return`` if completed successfully.
* They should always raise an exception if they did not complete
successfully. In particular, if they handle cancellation, they should
probably reraise, like this::
try:
# do work
except asyncio.CancelledError:
# undo partially completed work
raise
Args:
----
coro_fns: an iterable of coroutine functions, i.e. callables that
return a coroutine object when called. Use ``functools.partial`` or
lambdas to pass arguments.
delay: amount of time, in seconds, between starting coroutines. If
``None``, the coroutines will run sequentially.
loop: the event loop to use. If ``None``, the running loop is used.
Returns:
-------
tuple *(winner_result, winner_index, exceptions)* where
- *winner_result*: the result of the winning coroutine, or ``None``
if no coroutines won.
- *winner_index*: the index of the winning coroutine in
``coro_fns``, or ``None`` if no coroutines won. If the winning
coroutine may return None on success, *winner_index* can be used
to definitively determine whether any coroutine won.
- *exceptions*: list of exceptions returned by the coroutines.
``len(exceptions)`` is equal to the number of coroutines actually
started, and the order is the same as in ``coro_fns``. The winning
coroutine's entry is ``None``.
"""
loop = loop or asyncio.get_running_loop()
exceptions: List[Optional[BaseException]] = []
tasks: Set[asyncio.Task[Optional[Tuple[_T, int]]]] = set()
async def run_one_coro(
coro_fn: Callable[[], Awaitable[_T]],
this_index: int,
start_next: "asyncio.Future[None]",
) -> Optional[Tuple[_T, int]]:
"""
Run a single coroutine.
If the coroutine fails, set the exception in the exceptions list and
start the next coroutine by setting the result of the start_next.
If the coroutine succeeds, return the result and the index of the
coroutine in the coro_fns list.
If SystemExit or KeyboardInterrupt is raised, re-raise it.
"""
try:
result = await coro_fn()
except RE_RAISE_EXCEPTIONS:
raise
except BaseException as e:
exceptions[this_index] = e
_set_result(start_next) # Kickstart the next coroutine
return None
return result, this_index
start_next_timer: Optional[asyncio.TimerHandle] = None
start_next: Optional[asyncio.Future[None]]
task: asyncio.Task[Optional[Tuple[_T, int]]]
done: Union[asyncio.Future[None], asyncio.Task[Optional[Tuple[_T, int]]]]
coro_iter = iter(coro_fns)
this_index = -1
try:
while True:
if coro_fn := next(coro_iter, None):
this_index += 1
exceptions.append(None)
start_next = loop.create_future()
task = loop.create_task(run_one_coro(coro_fn, this_index, start_next))
tasks.add(task)
start_next_timer = (
loop.call_later(delay, _set_result, start_next) if delay else None
)
elif not tasks:
# We exhausted the coro_fns list and no tasks are running
# so we have no winner and all coroutines failed.
break
while tasks or start_next:
done = await _wait_one(
(*tasks, start_next) if start_next else tasks, loop
)
if done is start_next:
# The current task has failed or the timer has expired
# so we need to start the next task.
start_next = None
if start_next_timer:
start_next_timer.cancel()
start_next_timer = None
# Break out of the task waiting loop to start the next
# task.
break
if TYPE_CHECKING:
assert isinstance(done, asyncio.Task)
tasks.remove(done)
if winner := done.result():
return *winner, exceptions
finally:
# We either have:
# - a winner
# - all tasks failed
# - a KeyboardInterrupt or SystemExit.
#
# If the timer is still running, cancel it.
#
if start_next_timer:
start_next_timer.cancel()
#
# If there are any tasks left, cancel them and than
# wait them so they fill the exceptions list.
#
for task in tasks:
task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await task
return None, None, exceptions

View File

@@ -0,0 +1,259 @@
"""Base implementation."""
import asyncio
import collections
import contextlib
import functools
import itertools
import socket
from typing import List, Optional, Sequence, Set, Union
from . import _staggered
from .types import AddrInfoType, SocketFactoryType
async def start_connection(
addr_infos: Sequence[AddrInfoType],
*,
local_addr_infos: Optional[Sequence[AddrInfoType]] = None,
happy_eyeballs_delay: Optional[float] = None,
interleave: Optional[int] = None,
loop: Optional[asyncio.AbstractEventLoop] = None,
socket_factory: Optional[SocketFactoryType] = None,
) -> socket.socket:
"""
Connect to a TCP server.
Create a socket connection to a specified destination. The
destination is specified as a list of AddrInfoType tuples as
returned from getaddrinfo().
The arguments are, in order:
* ``family``: the address family, e.g. ``socket.AF_INET`` or
``socket.AF_INET6``.
* ``type``: the socket type, e.g. ``socket.SOCK_STREAM`` or
``socket.SOCK_DGRAM``.
* ``proto``: the protocol, e.g. ``socket.IPPROTO_TCP`` or
``socket.IPPROTO_UDP``.
* ``canonname``: the canonical name of the address, e.g.
``"www.python.org"``.
* ``sockaddr``: the socket address
This method is a coroutine which will try to establish the connection
in the background. When successful, the coroutine returns a
socket.
The expected use case is to use this method in conjunction with
loop.create_connection() to establish a connection to a server::
socket = await start_connection(addr_infos)
transport, protocol = await loop.create_connection(
MyProtocol, sock=socket, ...)
"""
if not (current_loop := loop):
current_loop = asyncio.get_running_loop()
single_addr_info = len(addr_infos) == 1
if happy_eyeballs_delay is not None and interleave is None:
# If using happy eyeballs, default to interleave addresses by family
interleave = 1
if interleave and not single_addr_info:
addr_infos = _interleave_addrinfos(addr_infos, interleave)
sock: Optional[socket.socket] = None
# uvloop can raise RuntimeError instead of OSError
exceptions: List[List[Union[OSError, RuntimeError]]] = []
if happy_eyeballs_delay is None or single_addr_info:
# not using happy eyeballs
for addrinfo in addr_infos:
try:
sock = await _connect_sock(
current_loop,
exceptions,
addrinfo,
local_addr_infos,
None,
socket_factory,
)
break
except (RuntimeError, OSError):
continue
else: # using happy eyeballs
open_sockets: Set[socket.socket] = set()
try:
sock, _, _ = await _staggered.staggered_race(
(
functools.partial(
_connect_sock,
current_loop,
exceptions,
addrinfo,
local_addr_infos,
open_sockets,
socket_factory,
)
for addrinfo in addr_infos
),
happy_eyeballs_delay,
)
finally:
# If we have a winner, staggered_race will
# cancel the other tasks, however there is a
# small race window where any of the other tasks
# can be done before they are cancelled which
# will leave the socket open. To avoid this problem
# we pass a set to _connect_sock to keep track of
# the open sockets and close them here if there
# are any "runner up" sockets.
for s in open_sockets:
if s is not sock:
with contextlib.suppress(OSError):
s.close()
open_sockets = None # type: ignore[assignment]
if sock is None:
all_exceptions = [exc for sub in exceptions for exc in sub]
try:
first_exception = all_exceptions[0]
if len(all_exceptions) == 1:
raise first_exception
else:
# If they all have the same str(), raise one.
model = str(first_exception)
if all(str(exc) == model for exc in all_exceptions):
raise first_exception
# Raise a combined exception so the user can see all
# the various error messages.
msg = "Multiple exceptions: {}".format(
", ".join(str(exc) for exc in all_exceptions)
)
# If the errno is the same for all exceptions, raise
# an OSError with that errno.
if isinstance(first_exception, OSError):
first_errno = first_exception.errno
if all(
isinstance(exc, OSError) and exc.errno == first_errno
for exc in all_exceptions
):
raise OSError(first_errno, msg)
elif isinstance(first_exception, RuntimeError) and all(
isinstance(exc, RuntimeError) for exc in all_exceptions
):
raise RuntimeError(msg)
# We have a mix of OSError and RuntimeError
# so we have to pick which one to raise.
# and we raise OSError for compatibility
raise OSError(msg)
finally:
all_exceptions = None # type: ignore[assignment]
exceptions = None # type: ignore[assignment]
return sock
async def _connect_sock(
loop: asyncio.AbstractEventLoop,
exceptions: List[List[Union[OSError, RuntimeError]]],
addr_info: AddrInfoType,
local_addr_infos: Optional[Sequence[AddrInfoType]] = None,
open_sockets: Optional[Set[socket.socket]] = None,
socket_factory: Optional[SocketFactoryType] = None,
) -> socket.socket:
"""
Create, bind and connect one socket.
If open_sockets is passed, add the socket to the set of open sockets.
Any failure caught here will remove the socket from the set and close it.
Callers can use this set to close any sockets that are not the winner
of all staggered tasks in the result there are runner up sockets aka
multiple winners.
"""
my_exceptions: List[Union[OSError, RuntimeError]] = []
exceptions.append(my_exceptions)
family, type_, proto, _, address = addr_info
sock = None
try:
if socket_factory is not None:
sock = socket_factory(addr_info)
else:
sock = socket.socket(family=family, type=type_, proto=proto)
if open_sockets is not None:
open_sockets.add(sock)
sock.setblocking(False)
if local_addr_infos is not None:
for lfamily, _, _, _, laddr in local_addr_infos:
# skip local addresses of different family
if lfamily != family:
continue
try:
sock.bind(laddr)
break
except OSError as exc:
msg = (
f"error while attempting to bind on "
f"address {laddr!r}: "
f"{(exc.strerror or '').lower()}"
)
exc = OSError(exc.errno, msg)
my_exceptions.append(exc)
else: # all bind attempts failed
if my_exceptions:
raise my_exceptions.pop()
else:
raise OSError(f"no matching local address with {family=} found")
await loop.sock_connect(sock, address)
return sock
except (RuntimeError, OSError) as exc:
my_exceptions.append(exc)
if sock is not None:
if open_sockets is not None:
open_sockets.remove(sock)
try:
sock.close()
except OSError as e:
my_exceptions.append(e)
raise
raise
except:
if sock is not None:
if open_sockets is not None:
open_sockets.remove(sock)
try:
sock.close()
except OSError as e:
my_exceptions.append(e)
raise
raise
finally:
exceptions = my_exceptions = None # type: ignore[assignment]
def _interleave_addrinfos(
addrinfos: Sequence[AddrInfoType], first_address_family_count: int = 1
) -> List[AddrInfoType]:
"""Interleave list of addrinfo tuples by family."""
# Group addresses by family
addrinfos_by_family: collections.OrderedDict[int, List[AddrInfoType]] = (
collections.OrderedDict()
)
for addr in addrinfos:
family = addr[0]
if family not in addrinfos_by_family:
addrinfos_by_family[family] = []
addrinfos_by_family[family].append(addr)
addrinfos_lists = list(addrinfos_by_family.values())
reordered: List[AddrInfoType] = []
if first_address_family_count > 1:
reordered.extend(addrinfos_lists[0][: first_address_family_count - 1])
del addrinfos_lists[0][: first_address_family_count - 1]
reordered.extend(
a
for a in itertools.chain.from_iterable(itertools.zip_longest(*addrinfos_lists))
if a is not None
)
return reordered

View File

@@ -0,0 +1,17 @@
"""Types for aiohappyeyeballs."""
import socket
# PY3.9: Import Callable from typing until we drop Python 3.9 support
# https://github.com/python/cpython/issues/87131
from typing import Callable, Tuple, Union
AddrInfoType = Tuple[
Union[int, socket.AddressFamily],
Union[int, socket.SocketKind],
int,
str,
Tuple, # type: ignore[type-arg]
]
SocketFactoryType = Callable[[AddrInfoType], socket.socket]

View File

@@ -0,0 +1,97 @@
"""Utility functions for aiohappyeyeballs."""
import ipaddress
import socket
from typing import Dict, List, Optional, Tuple, Union
from .types import AddrInfoType
def addr_to_addr_infos(
addr: Optional[
Union[Tuple[str, int, int, int], Tuple[str, int, int], Tuple[str, int]]
],
) -> Optional[List[AddrInfoType]]:
"""Convert an address tuple to a list of addr_info tuples."""
if addr is None:
return None
host = addr[0]
port = addr[1]
is_ipv6 = ":" in host
if is_ipv6:
flowinfo = 0
scopeid = 0
addr_len = len(addr)
if addr_len >= 4:
scopeid = addr[3] # type: ignore[misc]
if addr_len >= 3:
flowinfo = addr[2] # type: ignore[misc]
addr = (host, port, flowinfo, scopeid)
family = socket.AF_INET6
else:
addr = (host, port)
family = socket.AF_INET
return [(family, socket.SOCK_STREAM, socket.IPPROTO_TCP, "", addr)]
def pop_addr_infos_interleave(
addr_infos: List[AddrInfoType], interleave: Optional[int] = None
) -> None:
"""
Pop addr_info from the list of addr_infos by family up to interleave times.
The interleave parameter is used to know how many addr_infos for
each family should be popped of the top of the list.
"""
seen: Dict[int, int] = {}
if interleave is None:
interleave = 1
to_remove: List[AddrInfoType] = []
for addr_info in addr_infos:
family = addr_info[0]
if family not in seen:
seen[family] = 0
if seen[family] < interleave:
to_remove.append(addr_info)
seen[family] += 1
for addr_info in to_remove:
addr_infos.remove(addr_info)
def _addr_tuple_to_ip_address(
addr: Union[Tuple[str, int], Tuple[str, int, int, int]],
) -> Union[
Tuple[ipaddress.IPv4Address, int], Tuple[ipaddress.IPv6Address, int, int, int]
]:
"""Convert an address tuple to an IPv4Address."""
return (ipaddress.ip_address(addr[0]), *addr[1:])
def remove_addr_infos(
addr_infos: List[AddrInfoType],
addr: Union[Tuple[str, int], Tuple[str, int, int, int]],
) -> None:
"""
Remove an address from the list of addr_infos.
The addr value is typically the return value of
sock.getpeername().
"""
bad_addrs_infos: List[AddrInfoType] = []
for addr_info in addr_infos:
if addr_info[-1] == addr:
bad_addrs_infos.append(addr_info)
if bad_addrs_infos:
for bad_addr_info in bad_addrs_infos:
addr_infos.remove(bad_addr_info)
return
# Slow path in case addr is formatted differently
match_addr = _addr_tuple_to_ip_address(addr)
for addr_info in addr_infos:
if match_addr == _addr_tuple_to_ip_address(addr_info[-1]):
bad_addrs_infos.append(addr_info)
if bad_addrs_infos:
for bad_addr_info in bad_addrs_infos:
addr_infos.remove(bad_addr_info)
return
raise ValueError(f"Address {addr} not found in addr_infos")

View File

@@ -0,0 +1 @@
pip

View File

@@ -0,0 +1,262 @@
Metadata-Version: 2.4
Name: aiohttp
Version: 3.13.3
Summary: Async http client/server framework (asyncio)
Maintainer-email: aiohttp team <team@aiohttp.org>
License: Apache-2.0 AND MIT
Project-URL: Homepage, https://github.com/aio-libs/aiohttp
Project-URL: Chat: Matrix, https://matrix.to/#/#aio-libs:matrix.org
Project-URL: Chat: Matrix Space, https://matrix.to/#/#aio-libs-space:matrix.org
Project-URL: CI: GitHub Actions, https://github.com/aio-libs/aiohttp/actions?query=workflow%3ACI
Project-URL: Coverage: codecov, https://codecov.io/github/aio-libs/aiohttp
Project-URL: Docs: Changelog, https://docs.aiohttp.org/en/stable/changes.html
Project-URL: Docs: RTD, https://docs.aiohttp.org
Project-URL: GitHub: issues, https://github.com/aio-libs/aiohttp/issues
Project-URL: GitHub: repo, https://github.com/aio-libs/aiohttp
Classifier: Development Status :: 5 - Production/Stable
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: Operating System :: POSIX
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet :: WWW/HTTP
Requires-Python: >=3.9
Description-Content-Type: text/x-rst
License-File: LICENSE.txt
License-File: vendor/llhttp/LICENSE
Requires-Dist: aiohappyeyeballs>=2.5.0
Requires-Dist: aiosignal>=1.4.0
Requires-Dist: async-timeout<6.0,>=4.0; python_version < "3.11"
Requires-Dist: attrs>=17.3.0
Requires-Dist: frozenlist>=1.1.1
Requires-Dist: multidict<7.0,>=4.5
Requires-Dist: propcache>=0.2.0
Requires-Dist: yarl<2.0,>=1.17.0
Provides-Extra: speedups
Requires-Dist: aiodns>=3.3.0; extra == "speedups"
Requires-Dist: Brotli>=1.2; platform_python_implementation == "CPython" and extra == "speedups"
Requires-Dist: brotlicffi>=1.2; platform_python_implementation != "CPython" and extra == "speedups"
Requires-Dist: backports.zstd; (platform_python_implementation == "CPython" and python_version < "3.14") and extra == "speedups"
Dynamic: license-file
==================================
Async http client/server framework
==================================
.. image:: https://raw.githubusercontent.com/aio-libs/aiohttp/master/docs/aiohttp-plain.svg
:height: 64px
:width: 64px
:alt: aiohttp logo
|
.. image:: https://github.com/aio-libs/aiohttp/workflows/CI/badge.svg
:target: https://github.com/aio-libs/aiohttp/actions?query=workflow%3ACI
:alt: GitHub Actions status for master branch
.. image:: https://codecov.io/gh/aio-libs/aiohttp/branch/master/graph/badge.svg
:target: https://codecov.io/gh/aio-libs/aiohttp
:alt: codecov.io status for master branch
.. image:: https://badge.fury.io/py/aiohttp.svg
:target: https://pypi.org/project/aiohttp
:alt: Latest PyPI package version
.. image:: https://img.shields.io/pypi/dm/aiohttp
:target: https://pypistats.org/packages/aiohttp
:alt: Downloads count
.. image:: https://readthedocs.org/projects/aiohttp/badge/?version=latest
:target: https://docs.aiohttp.org/
:alt: Latest Read The Docs
.. image:: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json
:target: https://codspeed.io/aio-libs/aiohttp
:alt: Codspeed.io status for aiohttp
Key Features
============
- Supports both client and server side of HTTP protocol.
- Supports both client and server Web-Sockets out-of-the-box and avoids
Callback Hell.
- Provides Web-server with middleware and pluggable routing.
Getting started
===============
Client
------
To get something from the web:
.. code-block:: python
import aiohttp
import asyncio
async def main():
async with aiohttp.ClientSession() as session:
async with session.get('http://python.org') as response:
print("Status:", response.status)
print("Content-type:", response.headers['content-type'])
html = await response.text()
print("Body:", html[:15], "...")
asyncio.run(main())
This prints:
.. code-block::
Status: 200
Content-type: text/html; charset=utf-8
Body: <!doctype html> ...
Coming from `requests <https://requests.readthedocs.io/>`_ ? Read `why we need so many lines <https://aiohttp.readthedocs.io/en/latest/http_request_lifecycle.html>`_.
Server
------
An example using a simple server:
.. code-block:: python
# examples/server_simple.py
from aiohttp import web
async def handle(request):
name = request.match_info.get('name', "Anonymous")
text = "Hello, " + name
return web.Response(text=text)
async def wshandle(request):
ws = web.WebSocketResponse()
await ws.prepare(request)
async for msg in ws:
if msg.type == web.WSMsgType.text:
await ws.send_str("Hello, {}".format(msg.data))
elif msg.type == web.WSMsgType.binary:
await ws.send_bytes(msg.data)
elif msg.type == web.WSMsgType.close:
break
return ws
app = web.Application()
app.add_routes([web.get('/', handle),
web.get('/echo', wshandle),
web.get('/{name}', handle)])
if __name__ == '__main__':
web.run_app(app)
Documentation
=============
https://aiohttp.readthedocs.io/
Demos
=====
https://github.com/aio-libs/aiohttp-demos
External links
==============
* `Third party libraries
<http://aiohttp.readthedocs.io/en/latest/third_party.html>`_
* `Built with aiohttp
<http://aiohttp.readthedocs.io/en/latest/built_with.html>`_
* `Powered by aiohttp
<http://aiohttp.readthedocs.io/en/latest/powered_by.html>`_
Feel free to make a Pull Request for adding your link to these pages!
Communication channels
======================
*aio-libs Discussions*: https://github.com/aio-libs/aiohttp/discussions
*Matrix*: `#aio-libs:matrix.org <https://matrix.to/#/#aio-libs:matrix.org>`_
We support `Stack Overflow
<https://stackoverflow.com/questions/tagged/aiohttp>`_.
Please add *aiohttp* tag to your question there.
Requirements
============
- attrs_
- multidict_
- yarl_
- frozenlist_
Optionally you may install the aiodns_ library (highly recommended for sake of speed).
.. _aiodns: https://pypi.python.org/pypi/aiodns
.. _attrs: https://github.com/python-attrs/attrs
.. _multidict: https://pypi.python.org/pypi/multidict
.. _frozenlist: https://pypi.org/project/frozenlist/
.. _yarl: https://pypi.python.org/pypi/yarl
.. _async-timeout: https://pypi.python.org/pypi/async_timeout
License
=======
``aiohttp`` is offered under the Apache 2 license.
Keepsafe
========
The aiohttp community would like to thank Keepsafe
(https://www.getkeepsafe.com) for its support in the early days of
the project.
Source code
===========
The latest developer version is available in a GitHub repository:
https://github.com/aio-libs/aiohttp
Benchmarks
==========
If you are interested in efficiency, the AsyncIO community maintains a
list of benchmarks on the official wiki:
https://github.com/python/asyncio/wiki/Benchmarks
--------
.. image:: https://img.shields.io/matrix/aio-libs:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat
:target: https://matrix.to/#/%23aio-libs:matrix.org
:alt: Matrix Room — #aio-libs:matrix.org
.. image:: https://img.shields.io/matrix/aio-libs-space:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs-space%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat
:target: https://matrix.to/#/%23aio-libs-space:matrix.org
:alt: Matrix Space — #aio-libs-space:matrix.org
.. image:: https://insights.linuxfoundation.org/api/badge/health-score?project=aiohttp
:target: https://insights.linuxfoundation.org/project/aiohttp
:alt: LFX Health Score

View File

@@ -0,0 +1,138 @@
aiohttp-3.13.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
aiohttp-3.13.3.dist-info/METADATA,sha256=jkzui8KtHZ32gb8TfFZwIW4-zZ6Sr1eh1R6wYZW79Sg,8407
aiohttp-3.13.3.dist-info/RECORD,,
aiohttp-3.13.3.dist-info/WHEEL,sha256=JLOMsP7F5qtkAkINx5UnzbFguf8CqZeraV8o04b0I8I,101
aiohttp-3.13.3.dist-info/licenses/LICENSE.txt,sha256=wUk-nxDVnR-6n53ygAjhVX4zz5-6yM4SY6ozk5goA94,601
aiohttp-3.13.3.dist-info/licenses/vendor/llhttp/LICENSE,sha256=bd-mKNt20th7iWi6-61g9RxOyIEA3Xu5b5chbYivCAg,1127
aiohttp-3.13.3.dist-info/top_level.txt,sha256=iv-JIaacmTl-hSho3QmphcKnbRRYx1st47yjz_178Ro,8
aiohttp/.hash/_cparser.pxd.hash,sha256=eJQ2z7M7WoAng7D5ukCXzE3Yx22bLgv1PyOe0YbbQTM,108
aiohttp/.hash/_find_header.pxd.hash,sha256=TxG5w4etbVd6sfm5JWbdf5PW6LnuXRQnlMoFBVGKN2E,112
aiohttp/.hash/_http_parser.pyx.hash,sha256=NYbk_8ETW0vAtpTcxRVuWVmKJr9CUh2fR8I9emVQck4,112
aiohttp/.hash/_http_writer.pyx.hash,sha256=J4W44iDZQwIyZ0rGO5v-_sKIfPtAwqn99EwgaevQmo8,112
aiohttp/.hash/hdrs.py.hash,sha256=c2N-IMHz4dvAGL36CUyEw15noHE2AkJTeSBy3IxcCec,103
aiohttp/__init__.py,sha256=wTWxnyVGn59VuoFuK1m2_jJ-Cw5Be9ktp7h5Hfvyaas,8580
aiohttp/__pycache__/__init__.cpython-311.pyc,,
aiohttp/__pycache__/_cookie_helpers.cpython-311.pyc,,
aiohttp/__pycache__/abc.cpython-311.pyc,,
aiohttp/__pycache__/base_protocol.cpython-311.pyc,,
aiohttp/__pycache__/client.cpython-311.pyc,,
aiohttp/__pycache__/client_exceptions.cpython-311.pyc,,
aiohttp/__pycache__/client_middleware_digest_auth.cpython-311.pyc,,
aiohttp/__pycache__/client_middlewares.cpython-311.pyc,,
aiohttp/__pycache__/client_proto.cpython-311.pyc,,
aiohttp/__pycache__/client_reqrep.cpython-311.pyc,,
aiohttp/__pycache__/client_ws.cpython-311.pyc,,
aiohttp/__pycache__/compression_utils.cpython-311.pyc,,
aiohttp/__pycache__/connector.cpython-311.pyc,,
aiohttp/__pycache__/cookiejar.cpython-311.pyc,,
aiohttp/__pycache__/formdata.cpython-311.pyc,,
aiohttp/__pycache__/hdrs.cpython-311.pyc,,
aiohttp/__pycache__/helpers.cpython-311.pyc,,
aiohttp/__pycache__/http.cpython-311.pyc,,
aiohttp/__pycache__/http_exceptions.cpython-311.pyc,,
aiohttp/__pycache__/http_parser.cpython-311.pyc,,
aiohttp/__pycache__/http_websocket.cpython-311.pyc,,
aiohttp/__pycache__/http_writer.cpython-311.pyc,,
aiohttp/__pycache__/log.cpython-311.pyc,,
aiohttp/__pycache__/multipart.cpython-311.pyc,,
aiohttp/__pycache__/payload.cpython-311.pyc,,
aiohttp/__pycache__/payload_streamer.cpython-311.pyc,,
aiohttp/__pycache__/pytest_plugin.cpython-311.pyc,,
aiohttp/__pycache__/resolver.cpython-311.pyc,,
aiohttp/__pycache__/streams.cpython-311.pyc,,
aiohttp/__pycache__/tcp_helpers.cpython-311.pyc,,
aiohttp/__pycache__/test_utils.cpython-311.pyc,,
aiohttp/__pycache__/tracing.cpython-311.pyc,,
aiohttp/__pycache__/typedefs.cpython-311.pyc,,
aiohttp/__pycache__/web.cpython-311.pyc,,
aiohttp/__pycache__/web_app.cpython-311.pyc,,
aiohttp/__pycache__/web_exceptions.cpython-311.pyc,,
aiohttp/__pycache__/web_fileresponse.cpython-311.pyc,,
aiohttp/__pycache__/web_log.cpython-311.pyc,,
aiohttp/__pycache__/web_middlewares.cpython-311.pyc,,
aiohttp/__pycache__/web_protocol.cpython-311.pyc,,
aiohttp/__pycache__/web_request.cpython-311.pyc,,
aiohttp/__pycache__/web_response.cpython-311.pyc,,
aiohttp/__pycache__/web_routedef.cpython-311.pyc,,
aiohttp/__pycache__/web_runner.cpython-311.pyc,,
aiohttp/__pycache__/web_server.cpython-311.pyc,,
aiohttp/__pycache__/web_urldispatcher.cpython-311.pyc,,
aiohttp/__pycache__/web_ws.cpython-311.pyc,,
aiohttp/__pycache__/worker.cpython-311.pyc,,
aiohttp/_cookie_helpers.py,sha256=x6tVKd6fgqjIFQzQ_z-t_CRl-Pnar7qJh8HUwroSKIA,13997
aiohttp/_cparser.pxd,sha256=GP0Y9NqZYQGkJtS81XDzU70e7rRMb34TR7yGMmx5_zs,4453
aiohttp/_find_header.pxd,sha256=BFUSmxhemBtblqxzjzH3x03FfxaWlTyuAIOz8YZ5_nM,70
aiohttp/_headers.pxi,sha256=1MhCe6Un_KI1tpO85HnDfzVO94BhcirLanAOys5FIHA,2090
aiohttp/_http_parser.cp311-win_amd64.pyd,sha256=dT4gnGJDvQ0KtOLOAXOm6DpJQF_0BwMbyosvsZlLffc,246272
aiohttp/_http_parser.pyx,sha256=9-jyYF9-4i7ToMV0mvVgQ_rqNa8KGJfhQVY0GGrZuGg,29096
aiohttp/_http_writer.cp311-win_amd64.pyd,sha256=6mq-oxVxMiDKUbvJSbu_Mr8YVhD0Nr-3vzlEhz0Rigc,46080
aiohttp/_http_writer.pyx,sha256=WWdOf19QPqScBkifDhJynqPPOAmwB9sKJAO0Kkor4tE,4826
aiohttp/_websocket/.hash/mask.pxd.hash,sha256=TL0gGYyJWxqG8dWwa08B74WGg6-0M6_Breqrff-AiZg,115
aiohttp/_websocket/.hash/mask.pyx.hash,sha256=7xo6f01JaOQmaUNij3dQlOgxkEC1edkAIhwpeOvimLI,115
aiohttp/_websocket/.hash/reader_c.pxd.hash,sha256=RzhqjHN1HadWDeMHVQvaf-XLlGxF6nm5u-HJHGsx2aE,119
aiohttp/_websocket/__init__.py,sha256=R51KWH5kkdtDLb7T-ilztksbfweKCy3t22SgxGtiY-4,45
aiohttp/_websocket/__pycache__/__init__.cpython-311.pyc,,
aiohttp/_websocket/__pycache__/helpers.cpython-311.pyc,,
aiohttp/_websocket/__pycache__/models.cpython-311.pyc,,
aiohttp/_websocket/__pycache__/reader.cpython-311.pyc,,
aiohttp/_websocket/__pycache__/reader_c.cpython-311.pyc,,
aiohttp/_websocket/__pycache__/reader_py.cpython-311.pyc,,
aiohttp/_websocket/__pycache__/writer.cpython-311.pyc,,
aiohttp/_websocket/helpers.py,sha256=amqvDhoAKAi8ptB4qUNuQhkaOn-4JxSh_VLAqytmEfw,5185
aiohttp/_websocket/mask.cp311-win_amd64.pyd,sha256=OS4oeawxeMRTuZO7uy2QGGQLGMrBAToOsN212alcC5s,36352
aiohttp/_websocket/mask.pxd,sha256=41TdSZvhcbYSW_Vrw7bF4r_yoor2njtdaZ3bmvK6-jw,115
aiohttp/_websocket/mask.pyx,sha256=Ro7dOOv43HAAqNMz3xyCA11ppcn-vARIvjycStTEYww,1445
aiohttp/_websocket/models.py,sha256=Pz8qvnU43VUCNZcY4g03VwTsHOsb_jSN8iG69xMAc_A,2205
aiohttp/_websocket/reader.py,sha256=1r0cJ-jdFgbSrC6-jI0zjEA1CppzoUn8u_wiebrVVO0,1061
aiohttp/_websocket/reader_c.cp311-win_amd64.pyd,sha256=EwCfwbD6cV0QHtbOOKdgfzhnt8D9XDz1GaFWKNH3ebA,146432
aiohttp/_websocket/reader_c.pxd,sha256=HNOl4gRWtNBNEYNbK9PGOfFEQwUqJGexBbDKB_20sl0,2735
aiohttp/_websocket/reader_c.py,sha256=UKfslJuANla_CQMe7yIJzE8vp7bpzz9TLr-lH87XW6U,19346
aiohttp/_websocket/reader_py.py,sha256=UKfslJuANla_CQMe7yIJzE8vp7bpzz9TLr-lH87XW6U,19346
aiohttp/_websocket/writer.py,sha256=MpuNvG_t34CaDTAzW5FZJaRME8sL19rZotxSbXz2aas,11523
aiohttp/abc.py,sha256=01N6Y63o2bBC8Vi0ZjO6Jw0V9kXZfy3egwzKFW-tv9c,7417
aiohttp/base_protocol.py,sha256=8vNIv6QV_SDCW-8tfhlyxSwiBD7dAiMTqJI1GI8RG5s,3125
aiohttp/client.py,sha256=KlWhIZt935YpOZcXOOZl3eIRkuO-l0z2BH7arfhGg-A,59992
aiohttp/client_exceptions.py,sha256=sJcuvYKaB2nwuSdP7k18y3wc74aU0xAzdJikzzesrPE,11788
aiohttp/client_middleware_digest_auth.py,sha256=K4TPt4-rPQ0jjSHx3UFguMN7n31LpCC_o6JA-Hrg_Pc,18107
aiohttp/client_middlewares.py,sha256=FEVIXFkQ58n5bhK4BGEqqDCWnDh-GNJmWq20I5Yt6SU,1973
aiohttp/client_proto.py,sha256=rfbg8nUsfpCMM_zGpQygiFn8nzSdBI-731rmXVGHwLc,12469
aiohttp/client_reqrep.py,sha256=BUrqo2BJbrNazrIJr-ZgMLRTvE2fSON3zPQSq1dfgfU,54927
aiohttp/client_ws.py,sha256=9DraHuupuJcT7NOgyeGml8SBr7V5D5ID5-piY1fQMdA,15537
aiohttp/compression_utils.py,sha256=w0ECGGLVjtCXdYg-U_9DBn-DASzDPaWEVRx1HlwWslk,12086
aiohttp/connector.py,sha256=X2sRe6EAeWiaP6eaK9hWvLtSbdiJfNhK3bWl7XbR_V4,70846
aiohttp/cookiejar.py,sha256=C2fVzQGFieFP9mFDTOvfEc6fb5kPS2ijL2tFKAUW7Sw,19444
aiohttp/formdata.py,sha256=sz3VaTHVk11z_5G1LaDhUwrONJ8zRAGlZGg3hcCApzA,6563
aiohttp/hdrs.py,sha256=7htmhgZyE9HqWbPpxHU0r7kAIdT2kpOXQa1AadDh2W8,5232
aiohttp/helpers.py,sha256=1tXIvGSRWJD9wsS7GUVHLfJEsDM_XigurpgjxajkH0g,31615
aiohttp/http.py,sha256=DGKcwDbgIMpasv7s2jeKCRuixyj7W-RIrihRFjj0xcY,1914
aiohttp/http_exceptions.py,sha256=J3v-1S9S22GfAEtx0pEqp6d4G1Lqi2-gOrdLtuGlEhY,3185
aiohttp/http_parser.py,sha256=O5ud4wO80WLFe9kpXU0xGhjczUfrb7BAr0XAP7rBn7E,39263
aiohttp/http_websocket.py,sha256=b9kBmxPLPFQP_nu_sMhIMIeqDOm0ug8G4prbrhEMHZ0,878
aiohttp/http_writer.py,sha256=jA_aJW7JdH1mihrIYdJcLOHVKQ4Agg3g993v50eITBs,12824
aiohttp/log.py,sha256=zYUTvXsMQ9Sz1yNN8kXwd5Qxu49a1FzjZ_wQqriEc8M,333
aiohttp/multipart.py,sha256=UvcLOX3lO3ad3nfODsdlyvYWMAZHdUZ-wlZ5w1TbD2E,41634
aiohttp/payload.py,sha256=Xbs_2l0wDaThFG-ehNlvzQUkHuBPpc5FxpJnJa3ZPcs,41994
aiohttp/payload_streamer.py,sha256=K0iV85iW0vEG3rDkcopruidspynzQvrwW8mJvgPHisg,2289
aiohttp/py.typed,sha256=3VVwXUAWVEVX7sDwyYDnW5ZdBC9_Z9AJAFfLCleUW0k,8
aiohttp/pytest_plugin.py,sha256=ymhjbYHz2Kf0ZU_4Ly0hAp73dhsgrQIzJDo4Aot3_TI,13345
aiohttp/resolver.py,sha256=ePJgZAN5EQY4YuFiuZmVZM6p3UuzJ4qMWM1fu8DJ2Fc,10305
aiohttp/streams.py,sha256=J0G4ZJPdRScOPtnaB1ixhQYjLunLk8z70mfN9bc5K_o,24424
aiohttp/tcp_helpers.py,sha256=K-hhGh3jd6qCEnHJo8LvFyfJwBjh99UKI7A0aSRVhj4,998
aiohttp/test_utils.py,sha256=zFWAb-rPz1fWRUHnrjnfUH7ORlfIgZ2UZbEGe4YTa9I,23790
aiohttp/tracing.py,sha256=Kb-N32aMmYqC2Yc82NV6l0mIcavSQst1BHSFj94Apl0,15013
aiohttp/typedefs.py,sha256=Sx5v2yUyLu8nbabqtJRWj1M1_uW0IZACu78uYD7LBy0,1726
aiohttp/web.py,sha256=BQ96NEuTWikKGN5NnnTHjFLt07GUMWvvn42iFuIS3Mg,18444
aiohttp/web_app.py,sha256=WwEEzUg34j81kK2dPFnhlqx_z6nGjnHZDweZJF65pKc,20072
aiohttp/web_exceptions.py,sha256=itNRhCMDJFhnMWftr5SyTsoqh-i0n9rzTj0sjcAEUjo,10812
aiohttp/web_fileresponse.py,sha256=QIIbcIruCgfYrc8ZDvOgNlZzLbAagwXA9FrNI7NKNPY,16780
aiohttp/web_log.py,sha256=G5ugloW9noUxPft0SmVWOXw30MviL6rqZc3XrKN_T1U,8081
aiohttp/web_middlewares.py,sha256=mM2-R8eaV2r6Mi9Zc2bDG8QnhE9h0IzPvtDX_fkKR5s,4286
aiohttp/web_protocol.py,sha256=gJaDFtYPA-1gz35fwchjLhxrkmXXMOzFMCDHLQ1FHiI,27802
aiohttp/web_request.py,sha256=9zqyP32ScMUylQ_ta4tBHpWmoprhSB4jTgj2ixmGK74,30763
aiohttp/web_response.py,sha256=WJVumt-P0uMaFSbef_owvOXpq90E4VMl3RvSOWh0nJE,30197
aiohttp/web_routedef.py,sha256=XC10f57Q36JmYaaQqrecsyfIxHMepCKaKkBEB7hLzJI,6324
aiohttp/web_runner.py,sha256=zyVYVzCgnopiGwnIhKlNZHtLV_IYQ9aC-Vm43j_HRoA,12185
aiohttp/web_server.py,sha256=RZSWt_Mj-Lu89bFYsr_T3rjxW2VNN7PHNJ2mvv2qELs,2972
aiohttp/web_urldispatcher.py,sha256=4FiNFUWU_jITYl_DnObptuF5c0ShXAEiWyLVmE-GtN0,45595
aiohttp/web_ws.py,sha256=VXHGDtfy_jrBByLvuhnL-A_PmpcoT_ZLyYdj_EcL3Hw,23370
aiohttp/worker.py,sha256=N_9iyS_tR9U0pf3BRaIH2nzA1pjN1Xfi2gGmRrMhnho,8407

View File

@@ -0,0 +1,5 @@
Wheel-Version: 1.0
Generator: setuptools (80.9.0)
Root-Is-Purelib: false
Tag: cp311-cp311-win_amd64

View File

@@ -0,0 +1,13 @@
Copyright aio-libs contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,22 @@
This software is licensed under the MIT License.
Copyright Fedor Indutny, 2018.
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1 @@
aiohttp

View File

@@ -0,0 +1 @@
18fd18f4da996101a426d4bcd570f353bd1eeeb44c6f7e1347bc86326c79ff3b *D:/a/aiohttp/aiohttp/aiohttp/_cparser.pxd

View File

@@ -0,0 +1 @@
0455129b185e981b5b96ac738f31f7c74dc57f1696953cae0083b3f18679fe73 *D:/a/aiohttp/aiohttp/aiohttp/_find_header.pxd

View File

@@ -0,0 +1 @@
f7e8f2605f7ee22ed3a0c5749af56043faea35af0a1897e1415634186ad9b868 *D:/a/aiohttp/aiohttp/aiohttp/_http_parser.pyx

View File

@@ -0,0 +1 @@
59674e7f5f503ea49c06489f0e12729ea3cf3809b007db0a2403b42a4a2be2d1 *D:/a/aiohttp/aiohttp/aiohttp/_http_writer.pyx

View File

@@ -0,0 +1 @@
ee1b6686067213d1ea59b3e9c47534afb90021d4f692939741ad4069d0e1d96f *D:/a/aiohttp/aiohttp/aiohttp/hdrs.py

View File

@@ -0,0 +1,278 @@
__version__ = "3.13.3"
from typing import TYPE_CHECKING, Tuple
from . import hdrs as hdrs
from .client import (
BaseConnector,
ClientConnectionError,
ClientConnectionResetError,
ClientConnectorCertificateError,
ClientConnectorDNSError,
ClientConnectorError,
ClientConnectorSSLError,
ClientError,
ClientHttpProxyError,
ClientOSError,
ClientPayloadError,
ClientProxyConnectionError,
ClientRequest,
ClientResponse,
ClientResponseError,
ClientSession,
ClientSSLError,
ClientTimeout,
ClientWebSocketResponse,
ClientWSTimeout,
ConnectionTimeoutError,
ContentTypeError,
Fingerprint,
InvalidURL,
InvalidUrlClientError,
InvalidUrlRedirectClientError,
NamedPipeConnector,
NonHttpUrlClientError,
NonHttpUrlRedirectClientError,
RedirectClientError,
RequestInfo,
ServerConnectionError,
ServerDisconnectedError,
ServerFingerprintMismatch,
ServerTimeoutError,
SocketTimeoutError,
TCPConnector,
TooManyRedirects,
UnixConnector,
WSMessageTypeError,
WSServerHandshakeError,
request,
)
from .client_middleware_digest_auth import DigestAuthMiddleware
from .client_middlewares import ClientHandlerType, ClientMiddlewareType
from .compression_utils import set_zlib_backend
from .connector import (
AddrInfoType as AddrInfoType,
SocketFactoryType as SocketFactoryType,
)
from .cookiejar import CookieJar as CookieJar, DummyCookieJar as DummyCookieJar
from .formdata import FormData as FormData
from .helpers import BasicAuth, ChainMapProxy, ETag
from .http import (
HttpVersion as HttpVersion,
HttpVersion10 as HttpVersion10,
HttpVersion11 as HttpVersion11,
WebSocketError as WebSocketError,
WSCloseCode as WSCloseCode,
WSMessage as WSMessage,
WSMsgType as WSMsgType,
)
from .multipart import (
BadContentDispositionHeader as BadContentDispositionHeader,
BadContentDispositionParam as BadContentDispositionParam,
BodyPartReader as BodyPartReader,
MultipartReader as MultipartReader,
MultipartWriter as MultipartWriter,
content_disposition_filename as content_disposition_filename,
parse_content_disposition as parse_content_disposition,
)
from .payload import (
PAYLOAD_REGISTRY as PAYLOAD_REGISTRY,
AsyncIterablePayload as AsyncIterablePayload,
BufferedReaderPayload as BufferedReaderPayload,
BytesIOPayload as BytesIOPayload,
BytesPayload as BytesPayload,
IOBasePayload as IOBasePayload,
JsonPayload as JsonPayload,
Payload as Payload,
StringIOPayload as StringIOPayload,
StringPayload as StringPayload,
TextIOPayload as TextIOPayload,
get_payload as get_payload,
payload_type as payload_type,
)
from .payload_streamer import streamer as streamer
from .resolver import (
AsyncResolver as AsyncResolver,
DefaultResolver as DefaultResolver,
ThreadedResolver as ThreadedResolver,
)
from .streams import (
EMPTY_PAYLOAD as EMPTY_PAYLOAD,
DataQueue as DataQueue,
EofStream as EofStream,
FlowControlDataQueue as FlowControlDataQueue,
StreamReader as StreamReader,
)
from .tracing import (
TraceConfig as TraceConfig,
TraceConnectionCreateEndParams as TraceConnectionCreateEndParams,
TraceConnectionCreateStartParams as TraceConnectionCreateStartParams,
TraceConnectionQueuedEndParams as TraceConnectionQueuedEndParams,
TraceConnectionQueuedStartParams as TraceConnectionQueuedStartParams,
TraceConnectionReuseconnParams as TraceConnectionReuseconnParams,
TraceDnsCacheHitParams as TraceDnsCacheHitParams,
TraceDnsCacheMissParams as TraceDnsCacheMissParams,
TraceDnsResolveHostEndParams as TraceDnsResolveHostEndParams,
TraceDnsResolveHostStartParams as TraceDnsResolveHostStartParams,
TraceRequestChunkSentParams as TraceRequestChunkSentParams,
TraceRequestEndParams as TraceRequestEndParams,
TraceRequestExceptionParams as TraceRequestExceptionParams,
TraceRequestHeadersSentParams as TraceRequestHeadersSentParams,
TraceRequestRedirectParams as TraceRequestRedirectParams,
TraceRequestStartParams as TraceRequestStartParams,
TraceResponseChunkReceivedParams as TraceResponseChunkReceivedParams,
)
if TYPE_CHECKING:
# At runtime these are lazy-loaded at the bottom of the file.
from .worker import (
GunicornUVLoopWebWorker as GunicornUVLoopWebWorker,
GunicornWebWorker as GunicornWebWorker,
)
__all__: Tuple[str, ...] = (
"hdrs",
# client
"AddrInfoType",
"BaseConnector",
"ClientConnectionError",
"ClientConnectionResetError",
"ClientConnectorCertificateError",
"ClientConnectorDNSError",
"ClientConnectorError",
"ClientConnectorSSLError",
"ClientError",
"ClientHttpProxyError",
"ClientOSError",
"ClientPayloadError",
"ClientProxyConnectionError",
"ClientResponse",
"ClientRequest",
"ClientResponseError",
"ClientSSLError",
"ClientSession",
"ClientTimeout",
"ClientWebSocketResponse",
"ClientWSTimeout",
"ConnectionTimeoutError",
"ContentTypeError",
"Fingerprint",
"FlowControlDataQueue",
"InvalidURL",
"InvalidUrlClientError",
"InvalidUrlRedirectClientError",
"NonHttpUrlClientError",
"NonHttpUrlRedirectClientError",
"RedirectClientError",
"RequestInfo",
"ServerConnectionError",
"ServerDisconnectedError",
"ServerFingerprintMismatch",
"ServerTimeoutError",
"SocketFactoryType",
"SocketTimeoutError",
"TCPConnector",
"TooManyRedirects",
"UnixConnector",
"NamedPipeConnector",
"WSServerHandshakeError",
"request",
# client_middleware
"ClientMiddlewareType",
"ClientHandlerType",
# cookiejar
"CookieJar",
"DummyCookieJar",
# formdata
"FormData",
# helpers
"BasicAuth",
"ChainMapProxy",
"DigestAuthMiddleware",
"ETag",
"set_zlib_backend",
# http
"HttpVersion",
"HttpVersion10",
"HttpVersion11",
"WSMsgType",
"WSCloseCode",
"WSMessage",
"WebSocketError",
# multipart
"BadContentDispositionHeader",
"BadContentDispositionParam",
"BodyPartReader",
"MultipartReader",
"MultipartWriter",
"content_disposition_filename",
"parse_content_disposition",
# payload
"AsyncIterablePayload",
"BufferedReaderPayload",
"BytesIOPayload",
"BytesPayload",
"IOBasePayload",
"JsonPayload",
"PAYLOAD_REGISTRY",
"Payload",
"StringIOPayload",
"StringPayload",
"TextIOPayload",
"get_payload",
"payload_type",
# payload_streamer
"streamer",
# resolver
"AsyncResolver",
"DefaultResolver",
"ThreadedResolver",
# streams
"DataQueue",
"EMPTY_PAYLOAD",
"EofStream",
"StreamReader",
# tracing
"TraceConfig",
"TraceConnectionCreateEndParams",
"TraceConnectionCreateStartParams",
"TraceConnectionQueuedEndParams",
"TraceConnectionQueuedStartParams",
"TraceConnectionReuseconnParams",
"TraceDnsCacheHitParams",
"TraceDnsCacheMissParams",
"TraceDnsResolveHostEndParams",
"TraceDnsResolveHostStartParams",
"TraceRequestChunkSentParams",
"TraceRequestEndParams",
"TraceRequestExceptionParams",
"TraceRequestHeadersSentParams",
"TraceRequestRedirectParams",
"TraceRequestStartParams",
"TraceResponseChunkReceivedParams",
# workers (imported lazily with __getattr__)
"GunicornUVLoopWebWorker",
"GunicornWebWorker",
"WSMessageTypeError",
)
def __dir__() -> Tuple[str, ...]:
return __all__ + ("__doc__",)
def __getattr__(name: str) -> object:
global GunicornUVLoopWebWorker, GunicornWebWorker
# Importing gunicorn takes a long time (>100ms), so only import if actually needed.
if name in ("GunicornUVLoopWebWorker", "GunicornWebWorker"):
try:
from .worker import GunicornUVLoopWebWorker as guv, GunicornWebWorker as gw
except ImportError:
return None
GunicornUVLoopWebWorker = guv # type: ignore[misc]
GunicornWebWorker = gw # type: ignore[misc]
return guv if name == "GunicornUVLoopWebWorker" else gw
raise AttributeError(f"module {__name__} has no attribute {name}")

Some files were not shown because too many files have changed in this diff Show More