Skip to content

Commit 0b0e94b

Browse files
authored
Merge pull request #245 from hand-e-fr/fix_typed_dict
Fix typed dict
2 parents 72ff768 + 7fd3ee9 commit 0b0e94b

14 files changed

Lines changed: 1117 additions & 105 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ capabilities.yaml
2727
final_capabilities.yaml
2828
**/.scannerwork
2929
logs/
30+
.supported_providers.yaml

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "OpenHosta"
7-
version = "4.0.3"
7+
version = "4.0.4"
88
description = "A lightweight library integrating LLM natively into Python"
99
keywords = ["AI", "GPT", "Natural language", "Autommatic", "Easy"]
1010
authors = [
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
#!/bin/bash
2+
# scripts/build_compat_matrix_types.sh
3+
#
4+
# Generates a markdown type-compatibility matrix.
5+
# Rows = one Python type per row (driven by COMPAT_META in test_compat_logistics.py)
6+
# Cols = one column per LLM model discovered via GET <base_url>/models
7+
#
8+
# Phase 0 — Setup: create an isolated venv (uv, Python 3.12 by default)
9+
# Phase 1 — Discover: call /models on every endpoint in .supported_providers.yaml
10+
# Phase 2 — Test: run each pytest test individually per (endpoint, model) pair
11+
# Phase 3 — Report: write docs/compat_matrix_types.md
12+
#
13+
# Usage:
14+
# bash scripts/build_compat_matrix_types.sh
15+
# bash scripts/build_compat_matrix_types.sh --python 3.11
16+
# bash scripts/build_compat_matrix_types.sh --models "gpt-4.1,gpt-4.1-mini"
17+
# bash scripts/build_compat_matrix_types.sh --config .supported_providers.yaml
18+
19+
set -uo pipefail
20+
21+
# ── Paths ──────────────────────────────────────────────────────────────────────
22+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
24+
cd "$REPO_ROOT" || exit 1
25+
26+
CONFIG_FILE="$REPO_ROOT/.supported_providers.yaml"
27+
TEST_FILE="tests/typing/test_compat_logistics.py"
28+
RESULTS_FILE="$REPO_ROOT/docs/compat_matrix_types.md"
29+
LOG_DIR="$REPO_ROOT/logs"
30+
LOG="$LOG_DIR/compat_types.log"
31+
STATUS_DIR="$LOG_DIR/compat_type_status"
32+
VENV_DIR="$REPO_ROOT/.venv_compat_types"
33+
34+
# ── CLI argument parsing ───────────────────────────────────────────────────────
35+
PY_VERSION="3.12"
36+
CLI_MODELS_FILTER="NONE"
37+
38+
while [[ $# -gt 0 ]]; do
39+
case "$1" in
40+
--python) PY_VERSION="$2"; shift 2 ;;
41+
--models) CLI_MODELS_FILTER="$2"; shift 2 ;;
42+
--config) CONFIG_FILE="$2"; shift 2 ;;
43+
*)
44+
echo "Unknown option: $1" >&2
45+
echo "Usage: $0 [--python 3.12] [--models \"m1,m2\"] [--config file]" >&2
46+
exit 1 ;;
47+
esac
48+
done
49+
50+
# ── Sanity checks ─────────────────────────────────────────────────────────────
51+
if [[ ! -f "$CONFIG_FILE" ]]; then
52+
echo "❌ Config file not found: $CONFIG_FILE" >&2; exit 1
53+
fi
54+
if [[ ! -f "$TEST_FILE" ]]; then
55+
echo "❌ Test file not found: $TEST_FILE" >&2; exit 1
56+
fi
57+
58+
mkdir -p "$LOG_DIR" "$STATUS_DIR" "$(dirname "$RESULTS_FILE")"
59+
rm -f "$LOG"
60+
rm -rf "$STATUS_DIR"/*
61+
touch "$LOG"
62+
63+
echo "🔧 Type compatibility matrix builder" | tee -a "$LOG"
64+
echo "📂 Repo root : $REPO_ROOT" | tee -a "$LOG"
65+
echo "📄 Config : $CONFIG_FILE" | tee -a "$LOG"
66+
echo "🧪 Test file : $TEST_FILE" | tee -a "$LOG"
67+
echo "🐍 Python : $PY_VERSION" | tee -a "$LOG"
68+
[[ "$CLI_MODELS_FILTER" != "NONE" ]] && \
69+
echo "🔍 Filter : $CLI_MODELS_FILTER" | tee -a "$LOG"
70+
71+
# ── Phase 0 — Venv setup ──────────────────────────────────────────────────────
72+
echo "" | tee -a "$LOG"
73+
echo "━━━ Phase 0: Environment setup ━━━" | tee -a "$LOG"
74+
75+
if ! uv python --help &>/dev/null; then
76+
echo "❌ uv does not support 'uv python'." | tee -a "$LOG"; exit 1
77+
fi
78+
79+
echo "📦 Installing Python $PY_VERSION via uv..." | tee -a "$LOG"
80+
uv python install "$PY_VERSION" &>> "$LOG" \
81+
|| { echo "❌ Failed to install Python $PY_VERSION." | tee -a "$LOG"; exit 1; }
82+
83+
echo "🏗️ Creating venv $VENV_DIR ..." | tee -a "$LOG"
84+
rm -rf "$VENV_DIR"
85+
uv venv "$VENV_DIR" --python "$PY_VERSION" &>> "$LOG" \
86+
|| { echo "❌ Failed to create venv." | tee -a "$LOG"; exit 1; }
87+
88+
PYTHON="$VENV_DIR/bin/python3"
89+
PYTEST="$VENV_DIR/bin/pytest"
90+
91+
echo "📥 Installing project, pytest, pyyaml, pydantic..." | tee -a "$LOG"
92+
uv pip install -e . --python "$PYTHON" &>> "$LOG" \
93+
|| { echo "❌ Failed to install project." | tee -a "$LOG"; exit 1; }
94+
uv pip install pytest pyyaml pydantic --python "$PYTHON" &>> "$LOG"
95+
96+
echo "✅ Environment ready: $("$PYTHON" --version)" | tee -a "$LOG"
97+
98+
# ── Embedded Python helpers ────────────────────────────────────────────────────
99+
100+
# Helper: parse .supported_providers.yaml → tab-separated lines
101+
# <id> <base_url> <api_key|-> <yaml_model_filter_csv|NONE>
102+
parse_providers() {
103+
"$PYTHON" - "$CONFIG_FILE" <<'PYEOF'
104+
import sys
105+
try:
106+
import yaml
107+
except ImportError:
108+
sys.exit("❌ PyYAML not found in venv.")
109+
with open(sys.argv[1]) as f:
110+
data = yaml.safe_load(f)
111+
for ep in data.get("endpoints", []):
112+
eid = ep.get("id", "unknown")
113+
url = ep.get("base_url", "").rstrip("/")
114+
key = ep.get("api_key", "-")
115+
models = ep.get("models", [])
116+
filter_csv = ",".join(models) if models else "NONE"
117+
print(f"{eid}\t{url}\t{key}\t{filter_csv}")
118+
PYEOF
119+
}
120+
121+
# Helper: GET <base_url>/models → one retained model ID per line
122+
list_models() {
123+
local base_url="$1" api_key="$2" yaml_filter="$3" cli_filter="$4"
124+
"$PYTHON" - "$base_url" "$api_key" "$yaml_filter" "$cli_filter" <<'PYEOF'
125+
import sys, json, urllib.request, urllib.error
126+
base_url, api_key, yaml_filter, cli_filter = sys.argv[1:]
127+
base_url = base_url.rstrip("/")
128+
req = urllib.request.Request(f"{base_url}/models")
129+
if api_key != "-":
130+
req.add_header("Authorization", f"Bearer {api_key}")
131+
try:
132+
with urllib.request.urlopen(req, timeout=15) as resp:
133+
body = json.loads(resp.read())
134+
except urllib.error.URLError as e:
135+
print(f"ERROR: {e}", file=sys.stderr); sys.exit(1)
136+
all_ids = [m["id"] for m in body.get("data", [])]
137+
if cli_filter != "NONE":
138+
wanted = set(cli_filter.split(","))
139+
elif yaml_filter != "NONE":
140+
wanted = set(yaml_filter.split(","))
141+
else:
142+
wanted = None
143+
for mid in all_ids:
144+
if wanted is None or mid in wanted:
145+
print(mid)
146+
PYEOF
147+
}
148+
149+
# ── Phase 1 — Model discovery ─────────────────────────────────────────────────
150+
echo "" | tee -a "$LOG"
151+
echo "━━━ Phase 1: Model discovery ━━━" | tee -a "$LOG"
152+
153+
declare -a PAIRS=() # "eid|model_id|base_url|api_key"
154+
declare -a ALL_MODEL_LABELS=()
155+
156+
while IFS=$'\t' read -r eid url api_key yaml_filter; do
157+
echo "🔍 [$eid] Calling $url/models ..." | tee -a "$LOG"
158+
mapfile -t models_found < <(list_models "$url" "$api_key" "$yaml_filter" "$CLI_MODELS_FILTER" 2>>"$LOG")
159+
160+
if [[ ${#models_found[@]} -eq 0 ]]; then
161+
echo " ⚠️ No models retained — skipping." | tee -a "$LOG"; continue
162+
fi
163+
164+
echo "${#models_found[@]} model(s) retained." | tee -a "$LOG"
165+
for mid in "${models_found[@]}"; do
166+
echo "$mid" | tee -a "$LOG"
167+
PAIRS+=("${eid}|${mid}|${url}|${api_key}")
168+
ALL_MODEL_LABELS+=("$mid")
169+
done
170+
done < <(parse_providers)
171+
172+
if [[ ${#PAIRS[@]} -eq 0 ]]; then
173+
echo "❌ No models to test. Exiting." | tee -a "$LOG"; exit 1
174+
fi
175+
echo "Total: ${#PAIRS[@]} (endpoint × model) pair(s)." | tee -a "$LOG"
176+
177+
# ── Phase 1b — Collect individual test IDs ────────────────────────────────────
178+
echo "" | tee -a "$LOG"
179+
echo "📋 Collecting test IDs from $TEST_FILE ..." | tee -a "$LOG"
180+
181+
mapfile -t RAW_IDS < <(
182+
"$PYTEST" "$TEST_FILE" --collect-only -q 2>/dev/null \
183+
| grep "::" | grep -v "^$" | awk '{print $1}'
184+
)
185+
186+
if [[ ${#RAW_IDS[@]} -eq 0 ]]; then
187+
echo "❌ No tests found in $TEST_FILE." | tee -a "$LOG"; exit 1
188+
fi
189+
190+
# Strip the file prefix to keep only TestClass::test_name
191+
declare -a TEST_IDS=()
192+
for raw in "${RAW_IDS[@]}"; do
193+
# raw = tests/typing/test_compat_logistics.py::TestCompatLogistics::test_int
194+
short="${raw##*test_compat_logistics.py::}"
195+
TEST_IDS+=("$short")
196+
done
197+
198+
echo " Found ${#TEST_IDS[@]} test(s):" | tee -a "$LOG"
199+
for tid in "${TEST_IDS[@]}"; do echo "$tid" | tee -a "$LOG"; done
200+
201+
# ── Phase 2 — Run tests ───────────────────────────────────────────────────────
202+
echo "" | tee -a "$LOG"
203+
echo "━━━ Phase 2: Running type tests ━━━" | tee -a "$LOG"
204+
205+
for pair in "${PAIRS[@]}"; do
206+
IFS='|' read -r eid mid base_url api_key <<< "$pair"
207+
mid_safe="${mid//\//_}"
208+
mkdir -p "$STATUS_DIR/$mid_safe"
209+
210+
echo "" | tee -a "$LOG"
211+
echo "🤖 Model: $mid (endpoint: $eid)" | tee -a "$LOG"
212+
213+
for tid in "${TEST_IDS[@]}"; do
214+
# tid = TestCompatLogistics::test_int
215+
test_safe="${tid//::/_}"
216+
test_log="$LOG_DIR/types_${mid_safe}_${test_safe}.log"
217+
218+
PYTEST_ARGS=(
219+
"${TEST_FILE}::${tid}"
220+
"--model-name=${mid}"
221+
"--endpoint-url=${base_url}"
222+
"-v" "--tb=short"
223+
)
224+
[[ "$api_key" != "-" ]] && PYTEST_ARGS+=("--endpoint-api-key=${api_key}")
225+
226+
echo -n " 🧪 $tid ... " | tee -a "$LOG"
227+
if "$PYTEST" "${PYTEST_ARGS[@]}" > "$test_log" 2>&1; then
228+
echo "PASS" > "$STATUS_DIR/$mid_safe/$test_safe"
229+
echo "" | tee -a "$LOG"
230+
else
231+
echo "FAIL" > "$STATUS_DIR/$mid_safe/$test_safe"
232+
echo "" | tee -a "$LOG"
233+
fi
234+
done
235+
done
236+
237+
# ── Phase 3 — Build the report ────────────────────────────────────────────────
238+
echo "" | tee -a "$LOG"
239+
echo "━━━ Phase 3: Writing report ━━━" | tee -a "$LOG"
240+
241+
"$PYTHON" - "$TEST_FILE" "$STATUS_DIR" "${ALL_MODEL_LABELS[@]}" <<'PYEOF' > "$RESULTS_FILE"
242+
import sys, os
243+
244+
test_file = sys.argv[1]
245+
status_dir = sys.argv[2]
246+
model_list = sys.argv[3:] # one arg per model label
247+
248+
# ── Load COMPAT_META from the test file ──
249+
import importlib.util
250+
mod_name = "test_compat_logistics"
251+
spec = importlib.util.spec_from_file_location(mod_name, test_file)
252+
mod = importlib.util.module_from_spec(spec)
253+
sys.modules[mod_name] = mod
254+
spec.loader.exec_module(mod)
255+
meta = mod.COMPAT_META # list of (test_id, type_display, signature)
256+
257+
# ── Read status files ──
258+
# Status files: status_dir/<mid_safe>/<test_safe> → "PASS" | "FAIL"
259+
def read_status(mid_safe, test_id):
260+
test_safe = test_id.replace("::", "_")
261+
p = os.path.join(status_dir, mid_safe, test_safe)
262+
if not os.path.exists(p):
263+
return "⚠️"
264+
return "✅" if open(p).read().strip() == "PASS" else "❌"
265+
266+
# Sanitize model names for dir lookup
267+
def mid_safe(m):
268+
return m.replace("/", "_")
269+
270+
# ── Render markdown ──
271+
lines = []
272+
lines.append("# Type Compatibility Matrix")
273+
lines.append("")
274+
lines.append("Type support for `emulate()` — nominal logistics document management cases.")
275+
lines.append("")
276+
277+
# Header
278+
cols = " | ".join(f"`{m}`" for m in model_list)
279+
lines.append(f"| Python type | Fonction testée (`return emulate()`) | {cols} |")
280+
281+
sep_models = " | ".join(":---:" for _ in model_list)
282+
lines.append(f"| --- | --- | {sep_models} |")
283+
284+
# Rows
285+
for (test_id, type_display, signature) in meta:
286+
cells = " | ".join(read_status(mid_safe(m), test_id) for m in model_list)
287+
lines.append(f"| `{type_display}` | `{signature}` | {cells} |")
288+
289+
lines.append("")
290+
lines.append("### Summary")
291+
lines.append("")
292+
lines.append(f"- **Models:** {', '.join(f'`{m}`' for m in model_list)}")
293+
lines.append(f"- **Types tested:** {len(meta)}")
294+
lines.append(f"- 📅 Updated: {__import__('datetime').datetime.now().strftime('%Y-%m-%d %H:%M')}")
295+
lines.append("")
296+
lines.append("_Detailed logs: `logs/types_<model>_<TestClass_test_name>.log`_")
297+
298+
print("\n".join(lines))
299+
PYEOF
300+
301+
echo "✨ Done! Report: $RESULTS_FILE" | tee -a "$LOG"

src/OpenHosta/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "4.0.3"
1+
__version__ = "4.0.4"
22

33
from .defaults import config
44
from .defaults import reload_dotenv

src/OpenHosta/core/analizer.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11

22
import inspect
3+
import typing
34

45
from dataclasses import dataclass, field
56
from typing import Any, List, Optional, Union
@@ -149,6 +150,14 @@ def hosta_analyze_update(frame, analyse: AnalyzedFunction) -> AnalyzedFunction:
149150
)
150151

151152
def hosta_analyze(frame=None, function_pointer=None) -> AnalyzedFunction:
153+
try:
154+
if frame is not None:
155+
hints = typing.get_type_hints(function_pointer, globalns=frame.f_globals, localns=frame.f_locals)
156+
else:
157+
hints = typing.get_type_hints(function_pointer)
158+
except Exception:
159+
hints = {}
160+
152161
sig = inspect.signature(function_pointer)
153162

154163
result_args_value_table = []
@@ -158,18 +167,18 @@ def hosta_analyze(frame=None, function_pointer=None) -> AnalyzedFunction:
158167
result_args_value_table.append(AnalyzedArgument(
159168
name=arg,
160169
value=args_info.locals[arg],
161-
type=sig.parameters[arg].annotation
170+
type=hints.get(arg, sig.parameters[arg].annotation)
162171
))
163172
else:
164173
for name, param in sig.parameters.items():
165174
result_args_value_table.append(AnalyzedArgument(
166175
name=name,
167176
value=inspect._empty,
168-
type=param.annotation
177+
type=hints.get(name, param.annotation)
169178
))
170179

171180
result_function_name = function_pointer.__name__
172-
result_return_type = sig.return_annotation
181+
result_return_type = hints.get('return', sig.return_annotation)
173182
result_docstring = function_pointer.__doc__
174183

175184
return AnalyzedFunction(

0 commit comments

Comments
 (0)