kicad-sch-api 0.2.0__py3-none-any.whl → 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kicad-sch-api might be problematic. Click here for more details.
- kicad_sch_api/__init__.py +6 -2
- kicad_sch_api/cli.py +67 -62
- kicad_sch_api/core/component_bounds.py +477 -0
- kicad_sch_api/core/components.py +22 -10
- kicad_sch_api/core/config.py +127 -0
- kicad_sch_api/core/formatter.py +190 -24
- kicad_sch_api/core/geometry.py +111 -0
- kicad_sch_api/core/ic_manager.py +43 -37
- kicad_sch_api/core/junctions.py +17 -22
- kicad_sch_api/core/manhattan_routing.py +430 -0
- kicad_sch_api/core/parser.py +587 -197
- kicad_sch_api/core/pin_utils.py +149 -0
- kicad_sch_api/core/schematic.py +683 -207
- kicad_sch_api/core/simple_manhattan.py +228 -0
- kicad_sch_api/core/types.py +44 -4
- kicad_sch_api/core/wire_routing.py +380 -0
- kicad_sch_api/core/wires.py +29 -25
- kicad_sch_api/discovery/__init__.py +1 -1
- kicad_sch_api/discovery/search_index.py +142 -107
- kicad_sch_api/library/cache.py +70 -62
- {kicad_sch_api-0.2.0.dist-info → kicad_sch_api-0.2.2.dist-info}/METADATA +212 -40
- kicad_sch_api-0.2.2.dist-info/RECORD +31 -0
- kicad_sch_api-0.2.0.dist-info/RECORD +0 -24
- {kicad_sch_api-0.2.0.dist-info → kicad_sch_api-0.2.2.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.2.0.dist-info → kicad_sch_api-0.2.2.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.2.0.dist-info → kicad_sch_api-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.2.0.dist-info → kicad_sch_api-0.2.2.dist-info}/top_level.txt +0 -0
kicad_sch_api/core/wires.py
CHANGED
|
@@ -36,10 +36,10 @@ class WireCollection:
|
|
|
36
36
|
self._wires: List[Wire] = wires or []
|
|
37
37
|
self._uuid_index: Dict[str, int] = {}
|
|
38
38
|
self._modified = False
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
# Build UUID index
|
|
41
41
|
self._rebuild_index()
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
logger.debug(f"WireCollection initialized with {len(self._wires)} wires")
|
|
44
44
|
|
|
45
45
|
def _rebuild_index(self):
|
|
@@ -67,7 +67,7 @@ class WireCollection:
|
|
|
67
67
|
points: Optional[List[Union[Point, Tuple[float, float]]]] = None,
|
|
68
68
|
wire_type: WireType = WireType.WIRE,
|
|
69
69
|
stroke_width: float = 0.0,
|
|
70
|
-
uuid: Optional[str] = None
|
|
70
|
+
uuid: Optional[str] = None,
|
|
71
71
|
) -> str:
|
|
72
72
|
"""
|
|
73
73
|
Add a wire to the collection.
|
|
@@ -112,12 +112,7 @@ class WireCollection:
|
|
|
112
112
|
raise ValueError("Must provide either start/end points or points list")
|
|
113
113
|
|
|
114
114
|
# Create wire
|
|
115
|
-
wire = Wire(
|
|
116
|
-
uuid=uuid,
|
|
117
|
-
points=wire_points,
|
|
118
|
-
wire_type=wire_type,
|
|
119
|
-
stroke_width=stroke_width
|
|
120
|
-
)
|
|
115
|
+
wire = Wire(uuid=uuid, points=wire_points, wire_type=wire_type, stroke_width=stroke_width)
|
|
121
116
|
|
|
122
117
|
# Add to collection
|
|
123
118
|
self._wires.append(wire)
|
|
@@ -148,17 +143,23 @@ class WireCollection:
|
|
|
148
143
|
logger.debug(f"Removed wire: {uuid}")
|
|
149
144
|
return True
|
|
150
145
|
|
|
151
|
-
def get_by_point(
|
|
146
|
+
def get_by_point(
|
|
147
|
+
self, point: Union[Point, Tuple[float, float]], tolerance: float = None
|
|
148
|
+
) -> List[Wire]:
|
|
152
149
|
"""
|
|
153
150
|
Find wires that pass through or near a point.
|
|
154
151
|
|
|
155
152
|
Args:
|
|
156
153
|
point: Point to search near
|
|
157
|
-
tolerance: Distance tolerance
|
|
154
|
+
tolerance: Distance tolerance (uses config default if None)
|
|
158
155
|
|
|
159
156
|
Returns:
|
|
160
157
|
List of wires near the point
|
|
161
158
|
"""
|
|
159
|
+
if tolerance is None:
|
|
160
|
+
from .config import config
|
|
161
|
+
|
|
162
|
+
tolerance = config.tolerance.position_tolerance
|
|
162
163
|
if isinstance(point, tuple):
|
|
163
164
|
point = Point(point[0], point[1])
|
|
164
165
|
|
|
@@ -178,33 +179,36 @@ class WireCollection:
|
|
|
178
179
|
|
|
179
180
|
return matching_wires
|
|
180
181
|
|
|
181
|
-
def _point_on_segment(
|
|
182
|
+
def _point_on_segment(
|
|
183
|
+
self, point: Point, seg_start: Point, seg_end: Point, tolerance: float
|
|
184
|
+
) -> bool:
|
|
182
185
|
"""Check if point lies on line segment within tolerance."""
|
|
183
186
|
# Vector from seg_start to seg_end
|
|
184
187
|
seg_vec = Point(seg_end.x - seg_start.x, seg_end.y - seg_start.y)
|
|
185
188
|
seg_length = seg_start.distance_to(seg_end)
|
|
186
|
-
|
|
187
|
-
|
|
189
|
+
|
|
190
|
+
from .config import config
|
|
191
|
+
|
|
192
|
+
if seg_length < config.tolerance.wire_segment_min: # Very short segment
|
|
188
193
|
return seg_start.distance_to(point) <= tolerance
|
|
189
|
-
|
|
194
|
+
|
|
190
195
|
# Vector from seg_start to point
|
|
191
196
|
point_vec = Point(point.x - seg_start.x, point.y - seg_start.y)
|
|
192
|
-
|
|
197
|
+
|
|
193
198
|
# Project point onto segment
|
|
194
|
-
dot_product =
|
|
199
|
+
dot_product = point_vec.x * seg_vec.x + point_vec.y * seg_vec.y
|
|
195
200
|
projection = dot_product / (seg_length * seg_length)
|
|
196
|
-
|
|
201
|
+
|
|
197
202
|
# Check if projection is within segment bounds
|
|
198
203
|
if projection < 0 or projection > 1:
|
|
199
204
|
return False
|
|
200
|
-
|
|
205
|
+
|
|
201
206
|
# Calculate distance from point to line
|
|
202
207
|
proj_point = Point(
|
|
203
|
-
seg_start.x + projection * seg_vec.x,
|
|
204
|
-
seg_start.y + projection * seg_vec.y
|
|
208
|
+
seg_start.x + projection * seg_vec.x, seg_start.y + projection * seg_vec.y
|
|
205
209
|
)
|
|
206
210
|
distance = point.distance_to(proj_point)
|
|
207
|
-
|
|
211
|
+
|
|
208
212
|
return distance <= tolerance
|
|
209
213
|
|
|
210
214
|
def get_horizontal_wires(self) -> List[Wire]:
|
|
@@ -220,7 +224,7 @@ class WireCollection:
|
|
|
220
224
|
total_length = sum(wire.length for wire in self._wires)
|
|
221
225
|
simple_wires = sum(1 for wire in self._wires if wire.is_simple())
|
|
222
226
|
multi_point_wires = len(self._wires) - simple_wires
|
|
223
|
-
|
|
227
|
+
|
|
224
228
|
return {
|
|
225
229
|
"total_wires": len(self._wires),
|
|
226
230
|
"simple_wires": simple_wires,
|
|
@@ -228,7 +232,7 @@ class WireCollection:
|
|
|
228
232
|
"total_length": total_length,
|
|
229
233
|
"avg_length": total_length / len(self._wires) if self._wires else 0,
|
|
230
234
|
"horizontal_wires": len(self.get_horizontal_wires()),
|
|
231
|
-
"vertical_wires": len(self.get_vertical_wires())
|
|
235
|
+
"vertical_wires": len(self.get_vertical_wires()),
|
|
232
236
|
}
|
|
233
237
|
|
|
234
238
|
def clear(self):
|
|
@@ -245,4 +249,4 @@ class WireCollection:
|
|
|
245
249
|
|
|
246
250
|
def mark_saved(self):
|
|
247
251
|
"""Mark collection as saved (reset modified flag)."""
|
|
248
|
-
self._modified = False
|
|
252
|
+
self._modified = False
|
|
@@ -18,19 +18,20 @@ logger = logging.getLogger(__name__)
|
|
|
18
18
|
|
|
19
19
|
class ComponentSearchIndex:
|
|
20
20
|
"""Fast SQLite-based search index for KiCAD components."""
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
def __init__(self, cache_dir: Optional[Path] = None):
|
|
23
23
|
"""Initialize the search index."""
|
|
24
24
|
self.cache_dir = cache_dir or Path.home() / ".cache" / "kicad-sch-api"
|
|
25
25
|
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
self.db_path = self.cache_dir / "search_index.db"
|
|
28
28
|
self._init_database()
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
def _init_database(self):
|
|
31
31
|
"""Initialize the SQLite database schema."""
|
|
32
32
|
with sqlite3.connect(str(self.db_path)) as conn:
|
|
33
|
-
conn.execute(
|
|
33
|
+
conn.execute(
|
|
34
|
+
"""
|
|
34
35
|
CREATE TABLE IF NOT EXISTS components (
|
|
35
36
|
lib_id TEXT PRIMARY KEY,
|
|
36
37
|
name TEXT NOT NULL,
|
|
@@ -42,110 +43,136 @@ class ComponentSearchIndex:
|
|
|
42
43
|
category TEXT DEFAULT '',
|
|
43
44
|
last_updated REAL DEFAULT 0
|
|
44
45
|
)
|
|
45
|
-
"""
|
|
46
|
-
|
|
46
|
+
"""
|
|
47
|
+
)
|
|
48
|
+
|
|
47
49
|
# Create search indexes for fast queries
|
|
48
|
-
conn.execute(
|
|
50
|
+
conn.execute(
|
|
51
|
+
"""
|
|
49
52
|
CREATE INDEX IF NOT EXISTS idx_name
|
|
50
53
|
ON components(name COLLATE NOCASE)
|
|
51
|
-
"""
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
"""
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
conn.execute(
|
|
58
|
+
"""
|
|
54
59
|
CREATE INDEX IF NOT EXISTS idx_description
|
|
55
60
|
ON components(description COLLATE NOCASE)
|
|
56
|
-
"""
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
"""
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
conn.execute(
|
|
65
|
+
"""
|
|
59
66
|
CREATE INDEX IF NOT EXISTS idx_library
|
|
60
67
|
ON components(library)
|
|
61
|
-
"""
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
"""
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
conn.execute(
|
|
72
|
+
"""
|
|
64
73
|
CREATE INDEX IF NOT EXISTS idx_category
|
|
65
74
|
ON components(category)
|
|
66
|
-
"""
|
|
67
|
-
|
|
75
|
+
"""
|
|
76
|
+
)
|
|
77
|
+
|
|
68
78
|
# Full-text search virtual table for advanced queries
|
|
69
|
-
conn.execute(
|
|
79
|
+
conn.execute(
|
|
80
|
+
"""
|
|
70
81
|
CREATE VIRTUAL TABLE IF NOT EXISTS components_fts
|
|
71
82
|
USING fts5(lib_id, name, description, keywords, content=components)
|
|
72
|
-
"""
|
|
73
|
-
|
|
83
|
+
"""
|
|
84
|
+
)
|
|
85
|
+
|
|
74
86
|
conn.commit()
|
|
75
87
|
logger.debug("Initialized search index database")
|
|
76
|
-
|
|
88
|
+
|
|
77
89
|
def rebuild_index(self, progress_callback: Optional[callable] = None) -> int:
|
|
78
90
|
"""Rebuild the search index from the symbol cache."""
|
|
79
91
|
start_time = time.time()
|
|
80
92
|
symbol_cache = get_symbol_cache()
|
|
81
|
-
|
|
93
|
+
|
|
82
94
|
# Get all cached symbols
|
|
83
95
|
symbols = []
|
|
84
96
|
for lib_name in symbol_cache._library_index.keys():
|
|
85
97
|
try:
|
|
86
98
|
lib_symbols = symbol_cache.get_library_symbols(lib_name)
|
|
87
99
|
symbols.extend(lib_symbols)
|
|
88
|
-
|
|
100
|
+
|
|
89
101
|
if progress_callback:
|
|
90
102
|
progress_callback(f"Indexing {lib_name}: {len(lib_symbols)} symbols")
|
|
91
|
-
|
|
103
|
+
|
|
92
104
|
except Exception as e:
|
|
93
105
|
logger.warning(f"Failed to load library {lib_name}: {e}")
|
|
94
|
-
|
|
106
|
+
|
|
95
107
|
# Clear and rebuild index
|
|
96
108
|
with sqlite3.connect(str(self.db_path)) as conn:
|
|
97
109
|
conn.execute("DELETE FROM components")
|
|
98
110
|
conn.execute("DELETE FROM components_fts")
|
|
99
|
-
|
|
111
|
+
|
|
100
112
|
# Insert symbols in batches for better performance
|
|
101
113
|
batch_size = 100
|
|
102
114
|
for i in range(0, len(symbols), batch_size):
|
|
103
|
-
batch = symbols[i:i + batch_size]
|
|
104
|
-
|
|
115
|
+
batch = symbols[i : i + batch_size]
|
|
116
|
+
|
|
105
117
|
# Prepare batch data
|
|
106
118
|
batch_data = []
|
|
107
119
|
for symbol in batch:
|
|
108
|
-
batch_data.append(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
+
batch_data.append(
|
|
121
|
+
(
|
|
122
|
+
symbol.lib_id,
|
|
123
|
+
symbol.name,
|
|
124
|
+
symbol.library,
|
|
125
|
+
symbol.description,
|
|
126
|
+
symbol.keywords,
|
|
127
|
+
symbol.reference_prefix,
|
|
128
|
+
len(symbol.pins),
|
|
129
|
+
self._categorize_component(symbol),
|
|
130
|
+
time.time(),
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
|
|
120
134
|
# Insert batch
|
|
121
|
-
conn.executemany(
|
|
135
|
+
conn.executemany(
|
|
136
|
+
"""
|
|
122
137
|
INSERT OR REPLACE INTO components
|
|
123
138
|
(lib_id, name, library, description, keywords, reference_prefix,
|
|
124
139
|
pin_count, category, last_updated)
|
|
125
140
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
126
|
-
""",
|
|
127
|
-
|
|
141
|
+
""",
|
|
142
|
+
batch_data,
|
|
143
|
+
)
|
|
144
|
+
|
|
128
145
|
# Update FTS table
|
|
129
|
-
conn.executemany(
|
|
146
|
+
conn.executemany(
|
|
147
|
+
"""
|
|
130
148
|
INSERT OR REPLACE INTO components_fts
|
|
131
149
|
(lib_id, name, description, keywords)
|
|
132
150
|
VALUES (?, ?, ?, ?)
|
|
133
|
-
""",
|
|
134
|
-
|
|
151
|
+
""",
|
|
152
|
+
[(data[0], data[1], data[3], data[4]) for data in batch_data],
|
|
153
|
+
)
|
|
154
|
+
|
|
135
155
|
if progress_callback:
|
|
136
|
-
progress_callback(
|
|
137
|
-
|
|
156
|
+
progress_callback(
|
|
157
|
+
f"Indexed {min(i + batch_size, len(symbols))}/{len(symbols)} components"
|
|
158
|
+
)
|
|
159
|
+
|
|
138
160
|
conn.commit()
|
|
139
|
-
|
|
161
|
+
|
|
140
162
|
elapsed = time.time() - start_time
|
|
141
163
|
logger.info(f"Rebuilt search index with {len(symbols)} components in {elapsed:.2f}s")
|
|
142
164
|
return len(symbols)
|
|
143
|
-
|
|
144
|
-
def search(
|
|
145
|
-
|
|
165
|
+
|
|
166
|
+
def search(
|
|
167
|
+
self,
|
|
168
|
+
query: str,
|
|
169
|
+
library: Optional[str] = None,
|
|
170
|
+
category: Optional[str] = None,
|
|
171
|
+
limit: int = 20,
|
|
172
|
+
) -> List[Dict[str, Any]]:
|
|
146
173
|
"""Search components using multiple strategies."""
|
|
147
174
|
results = []
|
|
148
|
-
|
|
175
|
+
|
|
149
176
|
# Try different search strategies
|
|
150
177
|
strategies = [
|
|
151
178
|
self._search_exact_match,
|
|
@@ -153,39 +180,40 @@ class ComponentSearchIndex:
|
|
|
153
180
|
self._search_contains,
|
|
154
181
|
self._search_fts,
|
|
155
182
|
]
|
|
156
|
-
|
|
183
|
+
|
|
157
184
|
for strategy in strategies:
|
|
158
185
|
try:
|
|
159
186
|
strategy_results = strategy(query, library, category, limit - len(results))
|
|
160
|
-
|
|
187
|
+
|
|
161
188
|
# Avoid duplicates
|
|
162
189
|
existing_ids = {r["lib_id"] for r in results}
|
|
163
190
|
new_results = [r for r in strategy_results if r["lib_id"] not in existing_ids]
|
|
164
|
-
|
|
191
|
+
|
|
165
192
|
results.extend(new_results)
|
|
166
|
-
|
|
193
|
+
|
|
167
194
|
if len(results) >= limit:
|
|
168
195
|
break
|
|
169
|
-
|
|
196
|
+
|
|
170
197
|
except Exception as e:
|
|
171
198
|
logger.debug(f"Search strategy failed: {e}")
|
|
172
|
-
|
|
199
|
+
|
|
173
200
|
return results[:limit]
|
|
174
|
-
|
|
175
|
-
def _search_exact_match(
|
|
176
|
-
|
|
201
|
+
|
|
202
|
+
def _search_exact_match(
|
|
203
|
+
self, query: str, library: Optional[str], category: Optional[str], limit: int
|
|
204
|
+
) -> List[Dict[str, Any]]:
|
|
177
205
|
"""Search for exact name matches."""
|
|
178
206
|
conditions = ["name = ? COLLATE NOCASE"]
|
|
179
207
|
params = [query]
|
|
180
|
-
|
|
208
|
+
|
|
181
209
|
if library:
|
|
182
210
|
conditions.append("library = ?")
|
|
183
211
|
params.append(library)
|
|
184
|
-
|
|
212
|
+
|
|
185
213
|
if category:
|
|
186
214
|
conditions.append("category = ?")
|
|
187
215
|
params.append(category)
|
|
188
|
-
|
|
216
|
+
|
|
189
217
|
sql = f"""
|
|
190
218
|
SELECT lib_id, name, library, description, keywords, reference_prefix,
|
|
191
219
|
pin_count, category, 1.0 as match_score
|
|
@@ -195,25 +223,26 @@ class ComponentSearchIndex:
|
|
|
195
223
|
LIMIT ?
|
|
196
224
|
"""
|
|
197
225
|
params.append(limit)
|
|
198
|
-
|
|
226
|
+
|
|
199
227
|
with sqlite3.connect(str(self.db_path)) as conn:
|
|
200
228
|
conn.row_factory = sqlite3.Row
|
|
201
229
|
return [dict(row) for row in conn.execute(sql, params)]
|
|
202
|
-
|
|
203
|
-
def _search_prefix_match(
|
|
204
|
-
|
|
230
|
+
|
|
231
|
+
def _search_prefix_match(
|
|
232
|
+
self, query: str, library: Optional[str], category: Optional[str], limit: int
|
|
233
|
+
) -> List[Dict[str, Any]]:
|
|
205
234
|
"""Search for components starting with query."""
|
|
206
235
|
conditions = ["name LIKE ? COLLATE NOCASE"]
|
|
207
236
|
params = [f"{query}%"]
|
|
208
|
-
|
|
237
|
+
|
|
209
238
|
if library:
|
|
210
239
|
conditions.append("library = ?")
|
|
211
240
|
params.append(library)
|
|
212
|
-
|
|
241
|
+
|
|
213
242
|
if category:
|
|
214
243
|
conditions.append("category = ?")
|
|
215
244
|
params.append(category)
|
|
216
|
-
|
|
245
|
+
|
|
217
246
|
sql = f"""
|
|
218
247
|
SELECT lib_id, name, library, description, keywords, reference_prefix,
|
|
219
248
|
pin_count, category, 0.8 as match_score
|
|
@@ -223,25 +252,26 @@ class ComponentSearchIndex:
|
|
|
223
252
|
LIMIT ?
|
|
224
253
|
"""
|
|
225
254
|
params.append(limit)
|
|
226
|
-
|
|
255
|
+
|
|
227
256
|
with sqlite3.connect(str(self.db_path)) as conn:
|
|
228
257
|
conn.row_factory = sqlite3.Row
|
|
229
258
|
return [dict(row) for row in conn.execute(sql, params)]
|
|
230
|
-
|
|
231
|
-
def _search_contains(
|
|
232
|
-
|
|
259
|
+
|
|
260
|
+
def _search_contains(
|
|
261
|
+
self, query: str, library: Optional[str], category: Optional[str], limit: int
|
|
262
|
+
) -> List[Dict[str, Any]]:
|
|
233
263
|
"""Search for components containing query in name or description."""
|
|
234
264
|
conditions = ["(name LIKE ? COLLATE NOCASE OR description LIKE ? COLLATE NOCASE)"]
|
|
235
265
|
params = [f"%{query}%", f"%{query}%"]
|
|
236
|
-
|
|
266
|
+
|
|
237
267
|
if library:
|
|
238
268
|
conditions.append("library = ?")
|
|
239
269
|
params.append(library)
|
|
240
|
-
|
|
270
|
+
|
|
241
271
|
if category:
|
|
242
272
|
conditions.append("category = ?")
|
|
243
273
|
params.append(category)
|
|
244
|
-
|
|
274
|
+
|
|
245
275
|
sql = f"""
|
|
246
276
|
SELECT lib_id, name, library, description, keywords, reference_prefix,
|
|
247
277
|
pin_count, category, 0.6 as match_score
|
|
@@ -253,17 +283,18 @@ class ComponentSearchIndex:
|
|
|
253
283
|
LIMIT ?
|
|
254
284
|
"""
|
|
255
285
|
params.extend([f"%{query}%", limit])
|
|
256
|
-
|
|
286
|
+
|
|
257
287
|
with sqlite3.connect(str(self.db_path)) as conn:
|
|
258
288
|
conn.row_factory = sqlite3.Row
|
|
259
289
|
return [dict(row) for row in conn.execute(sql, params)]
|
|
260
|
-
|
|
261
|
-
def _search_fts(
|
|
262
|
-
|
|
290
|
+
|
|
291
|
+
def _search_fts(
|
|
292
|
+
self, query: str, library: Optional[str], category: Optional[str], limit: int
|
|
293
|
+
) -> List[Dict[str, Any]]:
|
|
263
294
|
"""Full-text search using FTS5."""
|
|
264
295
|
# Build FTS query
|
|
265
|
-
fts_query =
|
|
266
|
-
|
|
296
|
+
fts_query = " ".join(f'"{term}"*' for term in query.split())
|
|
297
|
+
|
|
267
298
|
sql = """
|
|
268
299
|
SELECT c.lib_id, c.name, c.library, c.description, c.keywords,
|
|
269
300
|
c.reference_prefix, c.pin_count, c.category,
|
|
@@ -273,18 +304,18 @@ class ComponentSearchIndex:
|
|
|
273
304
|
WHERE fts MATCH ?
|
|
274
305
|
"""
|
|
275
306
|
params = [fts_query]
|
|
276
|
-
|
|
307
|
+
|
|
277
308
|
if library:
|
|
278
309
|
sql += " AND c.library = ?"
|
|
279
310
|
params.append(library)
|
|
280
|
-
|
|
311
|
+
|
|
281
312
|
if category:
|
|
282
313
|
sql += " AND c.category = ?"
|
|
283
314
|
params.append(category)
|
|
284
|
-
|
|
315
|
+
|
|
285
316
|
sql += " ORDER BY fts.rank LIMIT ?"
|
|
286
317
|
params.append(limit)
|
|
287
|
-
|
|
318
|
+
|
|
288
319
|
try:
|
|
289
320
|
with sqlite3.connect(str(self.db_path)) as conn:
|
|
290
321
|
conn.row_factory = sqlite3.Row
|
|
@@ -292,7 +323,7 @@ class ComponentSearchIndex:
|
|
|
292
323
|
except sqlite3.OperationalError:
|
|
293
324
|
# FTS query failed, return empty results
|
|
294
325
|
return []
|
|
295
|
-
|
|
326
|
+
|
|
296
327
|
def get_libraries(self) -> List[Dict[str, Any]]:
|
|
297
328
|
"""Get all available libraries with component counts."""
|
|
298
329
|
sql = """
|
|
@@ -301,11 +332,11 @@ class ComponentSearchIndex:
|
|
|
301
332
|
GROUP BY library
|
|
302
333
|
ORDER BY library
|
|
303
334
|
"""
|
|
304
|
-
|
|
335
|
+
|
|
305
336
|
with sqlite3.connect(str(self.db_path)) as conn:
|
|
306
337
|
conn.row_factory = sqlite3.Row
|
|
307
338
|
return [dict(row) for row in conn.execute(sql)]
|
|
308
|
-
|
|
339
|
+
|
|
309
340
|
def get_categories(self) -> List[Dict[str, Any]]:
|
|
310
341
|
"""Get all component categories with counts."""
|
|
311
342
|
sql = """
|
|
@@ -315,11 +346,11 @@ class ComponentSearchIndex:
|
|
|
315
346
|
GROUP BY category
|
|
316
347
|
ORDER BY component_count DESC
|
|
317
348
|
"""
|
|
318
|
-
|
|
349
|
+
|
|
319
350
|
with sqlite3.connect(str(self.db_path)) as conn:
|
|
320
351
|
conn.row_factory = sqlite3.Row
|
|
321
352
|
return [dict(row) for row in conn.execute(sql)]
|
|
322
|
-
|
|
353
|
+
|
|
323
354
|
def validate_component(self, lib_id: str) -> Optional[Dict[str, Any]]:
|
|
324
355
|
"""Check if a component exists in the index."""
|
|
325
356
|
sql = """
|
|
@@ -328,41 +359,45 @@ class ComponentSearchIndex:
|
|
|
328
359
|
FROM components
|
|
329
360
|
WHERE lib_id = ?
|
|
330
361
|
"""
|
|
331
|
-
|
|
362
|
+
|
|
332
363
|
with sqlite3.connect(str(self.db_path)) as conn:
|
|
333
364
|
conn.row_factory = sqlite3.Row
|
|
334
365
|
result = conn.execute(sql, [lib_id]).fetchone()
|
|
335
366
|
return dict(result) if result else None
|
|
336
|
-
|
|
367
|
+
|
|
337
368
|
def get_stats(self) -> Dict[str, Any]:
|
|
338
369
|
"""Get search index statistics."""
|
|
339
370
|
with sqlite3.connect(str(self.db_path)) as conn:
|
|
340
371
|
total_components = conn.execute("SELECT COUNT(*) FROM components").fetchone()[0]
|
|
341
|
-
total_libraries = conn.execute(
|
|
342
|
-
|
|
372
|
+
total_libraries = conn.execute(
|
|
373
|
+
"SELECT COUNT(DISTINCT library) FROM components"
|
|
374
|
+
).fetchone()[0]
|
|
375
|
+
|
|
343
376
|
# Get library breakdown
|
|
344
|
-
library_stats = conn.execute(
|
|
377
|
+
library_stats = conn.execute(
|
|
378
|
+
"""
|
|
345
379
|
SELECT library, COUNT(*) as count
|
|
346
380
|
FROM components
|
|
347
381
|
GROUP BY library
|
|
348
382
|
ORDER BY count DESC
|
|
349
383
|
LIMIT 10
|
|
350
|
-
"""
|
|
351
|
-
|
|
384
|
+
"""
|
|
385
|
+
).fetchall()
|
|
386
|
+
|
|
352
387
|
return {
|
|
353
388
|
"total_components": total_components,
|
|
354
389
|
"total_libraries": total_libraries,
|
|
355
390
|
"top_libraries": [{"library": lib, "count": count} for lib, count in library_stats],
|
|
356
391
|
"database_path": str(self.db_path),
|
|
357
|
-
"database_size_mb": round(self.db_path.stat().st_size / (1024 * 1024), 2)
|
|
392
|
+
"database_size_mb": round(self.db_path.stat().st_size / (1024 * 1024), 2),
|
|
358
393
|
}
|
|
359
|
-
|
|
394
|
+
|
|
360
395
|
def _categorize_component(self, symbol: SymbolDefinition) -> str:
|
|
361
396
|
"""Categorize a component based on its properties."""
|
|
362
397
|
prefix = symbol.reference_prefix.upper()
|
|
363
398
|
name_lower = symbol.name.lower()
|
|
364
399
|
desc_lower = symbol.description.lower()
|
|
365
|
-
|
|
400
|
+
|
|
366
401
|
# Category mapping based on reference prefix and description
|
|
367
402
|
if prefix == "R":
|
|
368
403
|
return "resistor"
|
|
@@ -410,7 +445,7 @@ def get_search_index() -> ComponentSearchIndex:
|
|
|
410
445
|
def ensure_index_built(rebuild: bool = False) -> int:
|
|
411
446
|
"""Ensure the search index is built and up-to-date."""
|
|
412
447
|
index = get_search_index()
|
|
413
|
-
|
|
448
|
+
|
|
414
449
|
if rebuild or not index.db_path.exists():
|
|
415
450
|
logger.info("Building component search index...")
|
|
416
451
|
return index.rebuild_index()
|
|
@@ -418,4 +453,4 @@ def ensure_index_built(rebuild: bool = False) -> int:
|
|
|
418
453
|
# Check if index needs updating based on symbol cache
|
|
419
454
|
stats = index.get_stats()
|
|
420
455
|
logger.info(f"Search index ready: {stats['total_components']} components")
|
|
421
|
-
return stats["total_components"]
|
|
456
|
+
return stats["total_components"]
|