mfcli 0.2.0__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.
Files changed (136) hide show
  1. mfcli/.env.example +72 -0
  2. mfcli/__init__.py +0 -0
  3. mfcli/agents/__init__.py +0 -0
  4. mfcli/agents/controller/__init__.py +0 -0
  5. mfcli/agents/controller/agent.py +19 -0
  6. mfcli/agents/controller/config.yaml +27 -0
  7. mfcli/agents/controller/tools.py +42 -0
  8. mfcli/agents/tools/general.py +118 -0
  9. mfcli/alembic/env.py +61 -0
  10. mfcli/alembic/script.py.mako +28 -0
  11. mfcli/alembic/versions/6ccc0c7c397c_added_fields_to_pdf_parts_model.py +39 -0
  12. mfcli/alembic/versions/769019ef4870_added_gemini_file_path_to_pdf_part_model.py +33 -0
  13. mfcli/alembic/versions/7a2e3a779fdc_added_functional_block_and_component_.py +54 -0
  14. mfcli/alembic/versions/7d5adb2a47a7_added_pdf_parts_model.py +41 -0
  15. mfcli/alembic/versions/7fcb7d6a5836_init.py +167 -0
  16. mfcli/alembic/versions/e0f2b5765c72_added_cascade_delete_for_models_that_.py +32 -0
  17. mfcli/alembic.ini +147 -0
  18. mfcli/cli/__init__.py +0 -0
  19. mfcli/cli/dependencies.py +59 -0
  20. mfcli/cli/main.py +192 -0
  21. mfcli/client/__init__.py +0 -0
  22. mfcli/client/chroma_db.py +184 -0
  23. mfcli/client/docling.py +44 -0
  24. mfcli/client/gemini.py +252 -0
  25. mfcli/client/llama_parse.py +38 -0
  26. mfcli/client/vector_db.py +93 -0
  27. mfcli/constants/__init__.py +0 -0
  28. mfcli/constants/base_enum.py +18 -0
  29. mfcli/constants/directory_names.py +1 -0
  30. mfcli/constants/file_types.py +189 -0
  31. mfcli/constants/gemini.py +1 -0
  32. mfcli/constants/openai.py +6 -0
  33. mfcli/constants/pipeline_run_status.py +3 -0
  34. mfcli/crud/__init__.py +0 -0
  35. mfcli/crud/file.py +42 -0
  36. mfcli/crud/functional_blocks.py +26 -0
  37. mfcli/crud/netlist.py +18 -0
  38. mfcli/crud/pipeline_run.py +17 -0
  39. mfcli/crud/project.py +99 -0
  40. mfcli/digikey/__init__.py +0 -0
  41. mfcli/digikey/digikey.py +105 -0
  42. mfcli/main.py +5 -0
  43. mfcli/mcp/__init__.py +0 -0
  44. mfcli/mcp/configs/cline_mcp_settings.json +11 -0
  45. mfcli/mcp/configs/mfcli.mcp.json +7 -0
  46. mfcli/mcp/mcp_instance.py +6 -0
  47. mfcli/mcp/server.py +37 -0
  48. mfcli/mcp/state_manager.py +51 -0
  49. mfcli/mcp/tools/__init__.py +0 -0
  50. mfcli/mcp/tools/query_knowledgebase.py +108 -0
  51. mfcli/models/__init__.py +10 -0
  52. mfcli/models/base.py +10 -0
  53. mfcli/models/bom.py +71 -0
  54. mfcli/models/datasheet.py +10 -0
  55. mfcli/models/debug_setup.py +64 -0
  56. mfcli/models/file.py +43 -0
  57. mfcli/models/file_docket.py +94 -0
  58. mfcli/models/file_metadata.py +19 -0
  59. mfcli/models/functional_blocks.py +94 -0
  60. mfcli/models/llm_response.py +5 -0
  61. mfcli/models/mcu.py +97 -0
  62. mfcli/models/mcu_errata.py +26 -0
  63. mfcli/models/netlist.py +59 -0
  64. mfcli/models/pdf_parts.py +25 -0
  65. mfcli/models/pipeline_run.py +34 -0
  66. mfcli/models/project.py +27 -0
  67. mfcli/models/project_metadata.py +15 -0
  68. mfcli/pipeline/__init__.py +0 -0
  69. mfcli/pipeline/analysis/__init__.py +0 -0
  70. mfcli/pipeline/analysis/bom_netlist_mapper.py +28 -0
  71. mfcli/pipeline/analysis/generators/__init__.py +0 -0
  72. mfcli/pipeline/analysis/generators/bom/__init__.py +0 -0
  73. mfcli/pipeline/analysis/generators/bom/bom.py +74 -0
  74. mfcli/pipeline/analysis/generators/debug_setup/__init__.py +0 -0
  75. mfcli/pipeline/analysis/generators/debug_setup/debug_setup.py +71 -0
  76. mfcli/pipeline/analysis/generators/debug_setup/instructions.py +150 -0
  77. mfcli/pipeline/analysis/generators/functional_blocks/__init__.py +0 -0
  78. mfcli/pipeline/analysis/generators/functional_blocks/functional_blocks.py +93 -0
  79. mfcli/pipeline/analysis/generators/functional_blocks/instructions.py +34 -0
  80. mfcli/pipeline/analysis/generators/functional_blocks/validator.py +94 -0
  81. mfcli/pipeline/analysis/generators/generator.py +258 -0
  82. mfcli/pipeline/analysis/generators/generator_base.py +18 -0
  83. mfcli/pipeline/analysis/generators/mcu/__init__.py +0 -0
  84. mfcli/pipeline/analysis/generators/mcu/instructions.py +156 -0
  85. mfcli/pipeline/analysis/generators/mcu/mcu.py +84 -0
  86. mfcli/pipeline/analysis/generators/mcu_errata/__init__.py +1 -0
  87. mfcli/pipeline/analysis/generators/mcu_errata/instructions.py +77 -0
  88. mfcli/pipeline/analysis/generators/mcu_errata/mcu_errata.py +95 -0
  89. mfcli/pipeline/analysis/generators/summary/__init__.py +0 -0
  90. mfcli/pipeline/analysis/generators/summary/summary.py +47 -0
  91. mfcli/pipeline/classifier.py +93 -0
  92. mfcli/pipeline/data_enricher.py +15 -0
  93. mfcli/pipeline/extractor.py +34 -0
  94. mfcli/pipeline/extractors/__init__.py +0 -0
  95. mfcli/pipeline/extractors/pdf.py +12 -0
  96. mfcli/pipeline/parser.py +120 -0
  97. mfcli/pipeline/parsers/__init__.py +0 -0
  98. mfcli/pipeline/parsers/netlist/__init__.py +0 -0
  99. mfcli/pipeline/parsers/netlist/edif.py +93 -0
  100. mfcli/pipeline/parsers/netlist/kicad_legacy_net.py +326 -0
  101. mfcli/pipeline/parsers/netlist/kicad_spice.py +135 -0
  102. mfcli/pipeline/parsers/netlist/pads.py +185 -0
  103. mfcli/pipeline/parsers/netlist/protel.py +166 -0
  104. mfcli/pipeline/parsers/netlist/protel_detector.py +29 -0
  105. mfcli/pipeline/pipeline.py +419 -0
  106. mfcli/pipeline/preprocessors/__init__.py +0 -0
  107. mfcli/pipeline/preprocessors/user_guide.py +127 -0
  108. mfcli/pipeline/run_context.py +32 -0
  109. mfcli/pipeline/schema_mapper.py +89 -0
  110. mfcli/pipeline/sub_classifier.py +115 -0
  111. mfcli/utils/__init__.py +0 -0
  112. mfcli/utils/config.py +33 -0
  113. mfcli/utils/configurator.py +324 -0
  114. mfcli/utils/data_cleaner.py +82 -0
  115. mfcli/utils/datasheet_vectorizer.py +281 -0
  116. mfcli/utils/directory_manager.py +96 -0
  117. mfcli/utils/file_upload.py +298 -0
  118. mfcli/utils/files.py +16 -0
  119. mfcli/utils/http_requests.py +54 -0
  120. mfcli/utils/kb_lister.py +89 -0
  121. mfcli/utils/kb_remover.py +173 -0
  122. mfcli/utils/logger.py +28 -0
  123. mfcli/utils/mcp_configurator.py +311 -0
  124. mfcli/utils/migrations.py +18 -0
  125. mfcli/utils/orm.py +43 -0
  126. mfcli/utils/pdf_splitter.py +63 -0
  127. mfcli/utils/query_service.py +22 -0
  128. mfcli/utils/system_check.py +306 -0
  129. mfcli/utils/tools.py +31 -0
  130. mfcli/utils/vectorizer.py +28 -0
  131. mfcli-0.2.0.dist-info/METADATA +841 -0
  132. mfcli-0.2.0.dist-info/RECORD +136 -0
  133. mfcli-0.2.0.dist-info/WHEEL +5 -0
  134. mfcli-0.2.0.dist-info/entry_points.txt +3 -0
  135. mfcli-0.2.0.dist-info/licenses/LICENSE +21 -0
  136. mfcli-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,326 @@
1
+ """
2
+ KiCad Legacy Netlist Parser
3
+
4
+ Parses KiCad legacy netlist files (.net) in S-expression format.
5
+ Extracts:
6
+ - Reference designators (ref_des)
7
+ - Part numbers (from value or PARTNUMBER property)
8
+ - Pin connections from nets section
9
+
10
+ Format:
11
+ (export (version "E")
12
+ (components
13
+ (comp (ref "C1")
14
+ (value "PART_NUMBER")
15
+ (property (name "PARTNUMBER") (value "..."))))
16
+ (nets
17
+ (net (code "1") (name "GND")
18
+ (node (ref "C1") (pin "1")))))
19
+ """
20
+
21
+ from collections import defaultdict
22
+ from pathlib import Path
23
+ from typing import Dict, List, Optional, Tuple
24
+
25
+ from mfcli.models.netlist import Component, NetlistSchema, Pin
26
+
27
+
28
+ # ============================================================================
29
+ # S-Expression Parser
30
+ # ============================================================================
31
+
32
+ class SExpression:
33
+ """Represents a parsed S-expression."""
34
+
35
+ def __init__(self, value: Optional[str] = None, children: Optional[List['SExpression']] = None):
36
+ self.value = value # String value for atoms, None for lists
37
+ self.children = children or [] # Child expressions for lists
38
+
39
+ def is_list(self) -> bool:
40
+ """Check if this is a list (not an atom)."""
41
+ return self.value is None
42
+
43
+ def find(self, key: str) -> Optional['SExpression']:
44
+ """Find first child with matching value."""
45
+ for child in self.children:
46
+ if child.is_list() and child.children and child.children[0].value == key:
47
+ return child
48
+ return None
49
+
50
+ def find_all(self, key: str) -> List['SExpression']:
51
+ """Find all children with matching value."""
52
+ results = []
53
+ for child in self.children:
54
+ if child.is_list() and child.children and child.children[0].value == key:
55
+ results.append(child)
56
+ return results
57
+
58
+ def get_value(self, index: int = 0) -> Optional[str]:
59
+ """Get value at index from children."""
60
+ if index < len(self.children):
61
+ return self.children[index].value
62
+ return None
63
+
64
+ @staticmethod
65
+ def parse(text: str) -> List['SExpression']:
66
+ """Parse S-expression text into tree structure."""
67
+ tokens = SExpression._tokenize(text)
68
+ expressions, _ = SExpression._parse_tokens(tokens, 0)
69
+ return expressions
70
+
71
+ @staticmethod
72
+ def _tokenize(text: str) -> List[str]:
73
+ """Tokenize S-expression text."""
74
+ tokens = []
75
+ i = 0
76
+ while i < len(text):
77
+ char = text[i]
78
+
79
+ # Skip whitespace
80
+ if char.isspace():
81
+ i += 1
82
+ continue
83
+
84
+ # Handle parentheses
85
+ if char in '()':
86
+ tokens.append(char)
87
+ i += 1
88
+ continue
89
+
90
+ # Handle quoted strings
91
+ if char == '"':
92
+ j = i + 1
93
+ while j < len(text) and text[j] != '"':
94
+ if text[j] == '\\':
95
+ j += 2 # Skip escaped character
96
+ else:
97
+ j += 1
98
+ tokens.append(text[i:j + 1]) # Include quotes
99
+ i = j + 1
100
+ continue
101
+
102
+ # Handle atoms (unquoted strings)
103
+ j = i
104
+ while j < len(text) and not text[j].isspace() and text[j] not in '()':
105
+ j += 1
106
+ tokens.append(text[i:j])
107
+ i = j
108
+
109
+ return tokens
110
+
111
+ @staticmethod
112
+ def _parse_tokens(tokens: List[str], pos: int) -> Tuple[List['SExpression'], int]:
113
+ """Parse tokens into S-expressions, returns (expressions, next_pos)."""
114
+ expressions = []
115
+
116
+ while pos < len(tokens):
117
+ token = tokens[pos]
118
+
119
+ if token == '(':
120
+ # Start of list
121
+ children, pos = SExpression._parse_tokens(tokens, pos + 1)
122
+ expressions.append(SExpression(children=children))
123
+
124
+ elif token == ')':
125
+ # End of list
126
+ return expressions, pos + 1
127
+
128
+ else:
129
+ # Atom - remove quotes if present
130
+ value = token.strip('"')
131
+ expressions.append(SExpression(value=value))
132
+ pos += 1
133
+
134
+ return expressions, pos
135
+
136
+
137
+ # ============================================================================
138
+ # KiCad Legacy Netlist Parser
139
+ # ============================================================================
140
+
141
+ class KiCadLegacyNetParser:
142
+ """Parser for KiCad legacy netlist files."""
143
+
144
+ def __init__(self, content: str):
145
+ self.content = content
146
+ self.root: Optional[SExpression] = None
147
+ self.components_map: Dict[str, str] = {} # ref_des -> part_number
148
+ self.nets_map: Dict[str, List[Tuple[str, str]]] = defaultdict(list) # net_name -> [(ref_des, pin), ...]
149
+
150
+ def parse(self) -> NetlistSchema:
151
+ """Parse content and return validated schema."""
152
+ # Parse S-expressions
153
+ self.root = SExpression.parse(self.content)
154
+
155
+ if not self.root:
156
+ raise ValueError("Failed to parse S-expression")
157
+
158
+ # Find export section
159
+ export = None
160
+ for expr in self.root:
161
+ if expr.is_list() and expr.get_value(0) == "export":
162
+ export = expr
163
+ break
164
+
165
+ if not export:
166
+ raise ValueError("No (export ...) section found")
167
+
168
+ # Extract components and nets
169
+ self._extract_components(export)
170
+ self._extract_nets(export)
171
+
172
+ # Build schema
173
+ return self._build_schema()
174
+
175
+ def _extract_components(self, export: SExpression):
176
+ """Extract components from (components ...) section."""
177
+ components_section = export.find("components")
178
+ if not components_section:
179
+ return
180
+
181
+ # Find all (comp ...) entries
182
+ for comp in components_section.find_all("comp"):
183
+ ref_des = self._get_component_ref(comp)
184
+ part_number = self._get_component_part_number(comp)
185
+
186
+ if ref_des and part_number:
187
+ self.components_map[ref_des] = part_number
188
+
189
+ def _get_component_ref(self, comp: SExpression) -> Optional[str]:
190
+ """Extract reference designator from (comp ...) section."""
191
+ ref_expr = comp.find("ref")
192
+ if ref_expr and len(ref_expr.children) > 1:
193
+ return ref_expr.children[1].value
194
+ return None
195
+
196
+ def _get_component_part_number(self, comp: SExpression) -> Optional[str]:
197
+ """
198
+ Extract part number from (comp ...) section.
199
+ Try (value ...) first, then (property (name "PARTNUMBER") ...).
200
+ """
201
+ # Try (value ...) - most common
202
+ value_expr = comp.find("value")
203
+ if value_expr and len(value_expr.children) > 1:
204
+ part_number = value_expr.children[1].value
205
+ if part_number:
206
+ return part_number
207
+
208
+ # Try (property (name "PARTNUMBER") (value "..."))
209
+ for prop in comp.find_all("property"):
210
+ name_expr = prop.find("name")
211
+ if name_expr and len(name_expr.children) > 1:
212
+ if name_expr.children[1].value == "PARTNUMBER":
213
+ value_expr = prop.find("value")
214
+ if value_expr and len(value_expr.children) > 1:
215
+ return value_expr.children[1].value
216
+
217
+ return None
218
+
219
+ def _extract_nets(self, export: SExpression):
220
+ """Extract nets from (nets ...) section."""
221
+ nets_section = export.find("nets")
222
+ if not nets_section:
223
+ return
224
+
225
+ # Find all (net ...) entries
226
+ for net in nets_section.find_all("net"):
227
+ net_name = self._get_net_name(net)
228
+ if not net_name:
229
+ continue
230
+
231
+ # Find all (node ...) entries
232
+ for node in net.find_all("node"):
233
+ ref_des, pin = self._get_node_info(node)
234
+ if ref_des and pin:
235
+ self.nets_map[net_name].append((ref_des, pin))
236
+
237
+ def _get_net_name(self, net: SExpression) -> Optional[str]:
238
+ """Extract net name from (net ...) section."""
239
+ name_expr = net.find("name")
240
+ if name_expr and len(name_expr.children) > 1:
241
+ return name_expr.children[1].value
242
+ return None
243
+
244
+ def _get_node_info(self, node: SExpression) -> Tuple[Optional[str], Optional[str]]:
245
+ """Extract ref_des and pin from (node ...) section."""
246
+ ref_des = None
247
+ pin = None
248
+
249
+ ref_expr = node.find("ref")
250
+ if ref_expr and len(ref_expr.children) > 1:
251
+ ref_des = ref_expr.children[1].value
252
+
253
+ pin_expr = node.find("pin")
254
+ if pin_expr and len(pin_expr.children) > 1:
255
+ pin = pin_expr.children[1].value
256
+
257
+ return ref_des, pin
258
+
259
+ def _build_schema(self) -> NetlistSchema:
260
+ """Build NetlistSchema from extracted data."""
261
+ # Build pin list per component
262
+ component_pins: Dict[str, List[Pin]] = defaultdict(list)
263
+
264
+ for net_name, nodes in self.nets_map.items():
265
+ for ref_des, pin_number in nodes:
266
+ # Only add pins for known components
267
+ if ref_des in self.components_map:
268
+ pin = Pin(
269
+ pin=pin_number,
270
+ net=net_name
271
+ )
272
+ component_pins[ref_des].append(pin)
273
+
274
+ # Build components list
275
+ components = []
276
+ for ref_des, part_number in self.components_map.items():
277
+ component = Component(
278
+ ref_des=ref_des,
279
+ part_number=part_number,
280
+ pins=component_pins[ref_des]
281
+ )
282
+ components.append(component)
283
+
284
+ return NetlistSchema(components=components)
285
+
286
+
287
+ # ============================================================================
288
+ # Main Function
289
+ # ============================================================================
290
+
291
+ def is_kicad_legacy_netlist(text: str) -> bool:
292
+ stripped = text.lstrip()
293
+ if not stripped.startswith("(export"):
294
+ return False
295
+
296
+ # MUST contain these blocks
297
+ keywords = ["(design", "(components", "(nets"]
298
+ return all(k in text for k in keywords)
299
+
300
+
301
+ def parse_kicad_legacy_net_file(filepath: Path) -> NetlistSchema:
302
+ """
303
+ Parse a KiCad legacy netlist file and return validated netlist schema.
304
+
305
+ Args:
306
+ filepath: Path to KiCad legacy netlist file (.net)
307
+
308
+ Returns:
309
+ NetlistSchema with components and pins
310
+
311
+ Raises:
312
+ FileNotFoundError: If file doesn't exist
313
+ ValidationError: If parsed data doesn't match schema
314
+ """
315
+ if not filepath.exists():
316
+ raise FileNotFoundError(f"KiCad legacy netlist file not found: {filepath}")
317
+
318
+ # Read file content
319
+ with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
320
+ content = f.read()
321
+
322
+ # Parse
323
+ parser = KiCadLegacyNetParser(content)
324
+ schema = parser.parse()
325
+
326
+ return schema
@@ -0,0 +1,135 @@
1
+ """
2
+ KiCad SPICE Netlist Parser
3
+
4
+ Parses KiCad-generated SPICE netlist files (.cir) and extracts:
5
+ - Reference designators (ref_des)
6
+ - Part numbers
7
+ - Pin connections (inferred from node positions)
8
+
9
+ SPICE Format:
10
+ RefDes Node1 Node2 ... PartNumber
11
+ Example: R14 Net-_J3-Pad1_ Net-_LED2-A-Pad6_ CRCW0402330RJNED
12
+
13
+ Pin Mapping:
14
+ - 2-terminal components (R, C): pin1=node1, pin2=node2
15
+ - 3-terminal components (Q, transistors): pin1=node1, pin2=node2, pin3=node3
16
+ - Multi-terminal: numbered sequentially
17
+ """
18
+
19
+ from pathlib import Path
20
+ from typing import Dict
21
+
22
+ from mfcli.models.netlist import Component, NetlistSchema, Pin
23
+
24
+
25
+ # ============================================================================
26
+ # KiCad SPICE Parser
27
+ # ============================================================================
28
+
29
+ class KiCadSpiceParser:
30
+ """Parser for KiCad SPICE netlist files."""
31
+
32
+ def __init__(self, spice_content: str):
33
+ self.content = spice_content
34
+ self.lines = [line.strip() for line in spice_content.strip().split('\n')]
35
+ self.components: Dict[str, Component] = {}
36
+
37
+ def parse(self) -> NetlistSchema:
38
+ """Parse SPICE content and return validated schema."""
39
+ for line in self.lines:
40
+ # Skip empty lines
41
+ if not line:
42
+ continue
43
+
44
+ # Skip SPICE directives (start with .)
45
+ if line.startswith('.'):
46
+ continue
47
+
48
+ # Parse component line
49
+ self._parse_component_line(line)
50
+
51
+ # Convert to list and return
52
+ components_list = list(self.components.values())
53
+ return NetlistSchema(components=components_list)
54
+
55
+ def _parse_component_line(self, line: str):
56
+ """
57
+ Parse a component line from SPICE netlist.
58
+ Format: RefDes Node1 Node2 ... NodeN PartNumber
59
+ """
60
+ tokens = line.split()
61
+
62
+ if len(tokens) < 2:
63
+ return # Invalid line
64
+
65
+ ref_des = tokens[0]
66
+
67
+ # Last token is part number, middle tokens are nodes
68
+ if len(tokens) == 2:
69
+ # Special case: component with no connections (placeholder)
70
+ # Example: LED2 __LED2
71
+ part_number = tokens[1]
72
+ nodes = []
73
+ else:
74
+ part_number = tokens[-1]
75
+ nodes = tokens[1:-1]
76
+
77
+ # Skip placeholder components (nodes starting with __)
78
+ if nodes and all(node.startswith('__') for node in nodes):
79
+ return
80
+
81
+ # Create component
82
+ component = Component(
83
+ ref_des=ref_des,
84
+ part_number=part_number,
85
+ pins=[]
86
+ )
87
+
88
+ # Map nodes to pins
89
+ # Pin numbers are inferred from node position (1, 2, 3, ...)
90
+ for pin_num, node_name in enumerate(nodes, start=1):
91
+ # Skip placeholder nodes
92
+ if node_name.startswith('__'):
93
+ continue
94
+
95
+ pin = Pin(
96
+ pin=str(pin_num),
97
+ net=node_name
98
+ )
99
+ component.pins.append(pin)
100
+
101
+ # Only add component if it has pins (skip placeholders)
102
+ if component.pins:
103
+ self.components[ref_des] = component
104
+
105
+
106
+ # ============================================================================
107
+ # Main Function
108
+ # ============================================================================
109
+
110
+ def parse_kicad_spice_file(filepath: Path) -> NetlistSchema:
111
+ """
112
+ Parse a KiCad SPICE netlist file and return validated netlist schema.
113
+
114
+ Args:
115
+ filepath: Path to KiCad SPICE netlist file (.cir)
116
+
117
+ Returns:
118
+ NetlistSchema with components and pins
119
+
120
+ Raises:
121
+ FileNotFoundError: If file doesn't exist
122
+ ValidationError: If parsed data doesn't match schema
123
+ """
124
+ if not filepath.exists():
125
+ raise FileNotFoundError(f"KiCad SPICE netlist file not found: {filepath}")
126
+
127
+ # Read file content
128
+ with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
129
+ content = f.read()
130
+
131
+ # Parse
132
+ parser = KiCadSpiceParser(content)
133
+ schema = parser.parse()
134
+
135
+ return schema
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ PADS Netlist Parser
4
+
5
+ Parses PADS netlist files (Mentor Graphics) and extracts:
6
+ - Reference designators (ref_des)
7
+ - Part numbers/footprints
8
+ - Pin connections (pin number + net name)
9
+
10
+ Usage:
11
+ python pads_parser.py <netlist.txt> [--output <output.json>]
12
+
13
+ Example:
14
+ python pads_parser.py design.asc --output parsed.json
15
+ """
16
+
17
+ from pathlib import Path
18
+ from typing import Dict
19
+
20
+ from mfcli.models.netlist import Component, NetlistSchema, Pin
21
+
22
+
23
+ # ============================================================================
24
+ # PADS Parser
25
+ # ============================================================================
26
+
27
+ class PADSParser:
28
+ """Parser for PADS netlist files."""
29
+
30
+ def __init__(self, pads_content: str):
31
+ self.content = pads_content
32
+ self.lines = [line.strip() for line in pads_content.strip().split('\n')]
33
+ self.components: Dict[str, Component] = {}
34
+
35
+ def parse(self) -> NetlistSchema:
36
+ """Parse PADS content and return validated schema."""
37
+ # Step 1: Validate header
38
+ if not self._validate_header():
39
+ raise ValueError("Not a valid PADS netlist file (missing *PADS-PCB* header)")
40
+
41
+ # Step 2: Find section boundaries
42
+ part_start, part_end = self._find_section('*PART*')
43
+ net_start, net_end = self._find_section('*NET*')
44
+
45
+ # Step 3: Parse components
46
+ if part_start is not None and part_end is not None:
47
+ self._parse_parts(part_start, part_end)
48
+
49
+ # Step 4: Parse nets and add pins to components
50
+ if net_start is not None and net_end is not None:
51
+ self._parse_nets(net_start, net_end)
52
+
53
+ # Step 5: Validate and return
54
+ components_list = list(self.components.values())
55
+ return NetlistSchema(components=components_list)
56
+
57
+ def _validate_header(self) -> bool:
58
+ """Check if file has PADS header."""
59
+ return len(self.lines) > 0 and self.lines[0].startswith('*PADS-PCB*')
60
+
61
+ def _find_section(self, section_name: str) -> tuple:
62
+ """Find start and end indices of a section."""
63
+ try:
64
+ start_idx = self.lines.index(section_name) + 1
65
+
66
+ # Find end of section (next TOP-LEVEL section marker or *END*)
67
+ # Top-level markers: *PART*, *NET*, *END* (not *SIGNAL*)
68
+ top_level_markers = ['*PART*', '*NET*', '*END*']
69
+ end_idx = len(self.lines)
70
+ for i in range(start_idx, len(self.lines)):
71
+ if self.lines[i] in top_level_markers and self.lines[i] != section_name:
72
+ end_idx = i
73
+ break
74
+
75
+ return start_idx, end_idx
76
+ except ValueError:
77
+ return None, None
78
+
79
+ def _parse_parts(self, start_idx: int, end_idx: int):
80
+ """Parse the *PART* section to extract components."""
81
+ for i in range(start_idx, end_idx):
82
+ line = self.lines[i]
83
+
84
+ # Skip empty lines and section markers
85
+ if not line or line.startswith('*'):
86
+ continue
87
+
88
+ # Split line into ref_des and part_number
89
+ parts = line.split(None, 1) # Split on first whitespace
90
+ if len(parts) >= 1:
91
+ ref_des = parts[0]
92
+ part_number = parts[1] if len(parts) > 1 else ""
93
+
94
+ # Create component
95
+ self.components[ref_des] = Component(
96
+ ref_des=ref_des,
97
+ part_number=part_number,
98
+ pins=[]
99
+ )
100
+
101
+ def _parse_nets(self, start_idx: int, end_idx: int):
102
+ """Parse the *NET* section to extract pin connections."""
103
+ current_net = None
104
+
105
+ for i in range(start_idx, end_idx):
106
+ line = self.lines[i]
107
+
108
+ # Skip empty lines
109
+ if not line:
110
+ continue
111
+
112
+ # Check for signal/net definition
113
+ if line.startswith('*SIGNAL*'):
114
+ # Extract net name (everything after *SIGNAL*)
115
+ parts = line.split(None, 1)
116
+ if len(parts) > 1:
117
+ current_net = parts[1]
118
+ continue
119
+
120
+ # Skip other section markers
121
+ if line.startswith('*'):
122
+ current_net = None
123
+ continue
124
+
125
+ # Parse pin references if we have a current net
126
+ if current_net:
127
+ self._parse_pin_references(line, current_net)
128
+
129
+ def _parse_pin_references(self, line: str, net_name: str):
130
+ """Parse pin references from a line and add to components."""
131
+ # Split line into individual pin references
132
+ pin_refs = line.split()
133
+
134
+ for pin_ref in pin_refs:
135
+ # Skip empty strings
136
+ if not pin_ref:
137
+ continue
138
+
139
+ # Pin format: RefDes.PinNumber (e.g., C1.2, R14.1)
140
+ if '.' in pin_ref:
141
+ parts = pin_ref.split('.', 1)
142
+ ref_des = parts[0]
143
+ pin_number = parts[1]
144
+
145
+ # Add pin to component if component exists
146
+ if ref_des in self.components:
147
+ pin = Pin(pin=pin_number, net=net_name)
148
+
149
+ # Avoid duplicates
150
+ existing_pins = self.components[ref_des].pins
151
+ if not any(p.pin == pin.pin and p.net == pin.net for p in existing_pins):
152
+ self.components[ref_des].pins.append(pin)
153
+
154
+
155
+ # ============================================================================
156
+ # Main Functions
157
+ # ============================================================================
158
+
159
+ def parse_pads_file(filepath: Path) -> NetlistSchema:
160
+ """
161
+ Parse a PADS netlist file and return validated netlist schema.
162
+
163
+ Args:
164
+ filepath: Path to PADS netlist file
165
+
166
+ Returns:
167
+ NetlistSchema with components and pins
168
+
169
+ Raises:
170
+ FileNotFoundError: If file doesn't exist
171
+ ValidationError: If parsed data doesn't match schema
172
+ ValueError: If file is not a valid PADS netlist
173
+ """
174
+ if not filepath.exists():
175
+ raise FileNotFoundError(f"PADS netlist file not found: {filepath}")
176
+
177
+ # Read file content
178
+ with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
179
+ content = f.read()
180
+
181
+ # Parse
182
+ parser = PADSParser(content)
183
+ schema = parser.parse()
184
+
185
+ return schema