mfcli 0.2.1__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.
- mfcli/.env.example +72 -0
- mfcli/__init__.py +0 -0
- mfcli/agents/__init__.py +0 -0
- mfcli/agents/controller/__init__.py +0 -0
- mfcli/agents/controller/agent.py +19 -0
- mfcli/agents/controller/config.yaml +27 -0
- mfcli/agents/controller/tools.py +42 -0
- mfcli/agents/tools/general.py +118 -0
- mfcli/alembic/env.py +61 -0
- mfcli/alembic/script.py.mako +28 -0
- mfcli/alembic/versions/6ccc0c7c397c_added_fields_to_pdf_parts_model.py +39 -0
- mfcli/alembic/versions/769019ef4870_added_gemini_file_path_to_pdf_part_model.py +33 -0
- mfcli/alembic/versions/7a2e3a779fdc_added_functional_block_and_component_.py +54 -0
- mfcli/alembic/versions/7d5adb2a47a7_added_pdf_parts_model.py +41 -0
- mfcli/alembic/versions/7fcb7d6a5836_init.py +167 -0
- mfcli/alembic/versions/e0f2b5765c72_added_cascade_delete_for_models_that_.py +32 -0
- mfcli/alembic.ini +147 -0
- mfcli/cli/__init__.py +0 -0
- mfcli/cli/dependencies.py +59 -0
- mfcli/cli/main.py +200 -0
- mfcli/client/__init__.py +0 -0
- mfcli/client/chroma_db.py +184 -0
- mfcli/client/docling.py +44 -0
- mfcli/client/gemini.py +252 -0
- mfcli/client/llama_parse.py +38 -0
- mfcli/client/vector_db.py +93 -0
- mfcli/constants/__init__.py +0 -0
- mfcli/constants/base_enum.py +18 -0
- mfcli/constants/directory_names.py +1 -0
- mfcli/constants/file_types.py +189 -0
- mfcli/constants/gemini.py +1 -0
- mfcli/constants/openai.py +6 -0
- mfcli/constants/pipeline_run_status.py +3 -0
- mfcli/crud/__init__.py +0 -0
- mfcli/crud/file.py +42 -0
- mfcli/crud/functional_blocks.py +26 -0
- mfcli/crud/netlist.py +18 -0
- mfcli/crud/pipeline_run.py +17 -0
- mfcli/crud/project.py +144 -0
- mfcli/digikey/__init__.py +0 -0
- mfcli/digikey/digikey.py +105 -0
- mfcli/main.py +5 -0
- mfcli/mcp/__init__.py +0 -0
- mfcli/mcp/configs/cline_mcp_settings.json +11 -0
- mfcli/mcp/configs/mfcli.mcp.json +7 -0
- mfcli/mcp/mcp_instance.py +6 -0
- mfcli/mcp/server.py +37 -0
- mfcli/mcp/state_manager.py +51 -0
- mfcli/mcp/tools/__init__.py +0 -0
- mfcli/mcp/tools/query_knowledgebase.py +108 -0
- mfcli/models/__init__.py +10 -0
- mfcli/models/base.py +10 -0
- mfcli/models/bom.py +71 -0
- mfcli/models/datasheet.py +10 -0
- mfcli/models/debug_setup.py +64 -0
- mfcli/models/file.py +43 -0
- mfcli/models/file_docket.py +94 -0
- mfcli/models/file_metadata.py +19 -0
- mfcli/models/functional_blocks.py +94 -0
- mfcli/models/llm_response.py +5 -0
- mfcli/models/mcu.py +97 -0
- mfcli/models/mcu_errata.py +26 -0
- mfcli/models/netlist.py +59 -0
- mfcli/models/pdf_parts.py +25 -0
- mfcli/models/pipeline_run.py +34 -0
- mfcli/models/project.py +27 -0
- mfcli/models/project_metadata.py +15 -0
- mfcli/pipeline/__init__.py +0 -0
- mfcli/pipeline/analysis/__init__.py +0 -0
- mfcli/pipeline/analysis/bom_netlist_mapper.py +28 -0
- mfcli/pipeline/analysis/generators/__init__.py +0 -0
- mfcli/pipeline/analysis/generators/bom/__init__.py +0 -0
- mfcli/pipeline/analysis/generators/bom/bom.py +74 -0
- mfcli/pipeline/analysis/generators/debug_setup/__init__.py +0 -0
- mfcli/pipeline/analysis/generators/debug_setup/debug_setup.py +71 -0
- mfcli/pipeline/analysis/generators/debug_setup/instructions.py +150 -0
- mfcli/pipeline/analysis/generators/functional_blocks/__init__.py +0 -0
- mfcli/pipeline/analysis/generators/functional_blocks/functional_blocks.py +93 -0
- mfcli/pipeline/analysis/generators/functional_blocks/instructions.py +34 -0
- mfcli/pipeline/analysis/generators/functional_blocks/validator.py +94 -0
- mfcli/pipeline/analysis/generators/generator.py +258 -0
- mfcli/pipeline/analysis/generators/generator_base.py +18 -0
- mfcli/pipeline/analysis/generators/mcu/__init__.py +0 -0
- mfcli/pipeline/analysis/generators/mcu/instructions.py +156 -0
- mfcli/pipeline/analysis/generators/mcu/mcu.py +84 -0
- mfcli/pipeline/analysis/generators/mcu_errata/__init__.py +1 -0
- mfcli/pipeline/analysis/generators/mcu_errata/instructions.py +77 -0
- mfcli/pipeline/analysis/generators/mcu_errata/mcu_errata.py +95 -0
- mfcli/pipeline/analysis/generators/summary/__init__.py +0 -0
- mfcli/pipeline/analysis/generators/summary/summary.py +47 -0
- mfcli/pipeline/classifier.py +93 -0
- mfcli/pipeline/data_enricher.py +15 -0
- mfcli/pipeline/extractor.py +34 -0
- mfcli/pipeline/extractors/__init__.py +0 -0
- mfcli/pipeline/extractors/pdf.py +12 -0
- mfcli/pipeline/parser.py +120 -0
- mfcli/pipeline/parsers/__init__.py +0 -0
- mfcli/pipeline/parsers/netlist/__init__.py +0 -0
- mfcli/pipeline/parsers/netlist/edif.py +93 -0
- mfcli/pipeline/parsers/netlist/kicad_legacy_net.py +326 -0
- mfcli/pipeline/parsers/netlist/kicad_spice.py +135 -0
- mfcli/pipeline/parsers/netlist/pads.py +185 -0
- mfcli/pipeline/parsers/netlist/protel.py +166 -0
- mfcli/pipeline/parsers/netlist/protel_detector.py +29 -0
- mfcli/pipeline/pipeline.py +470 -0
- mfcli/pipeline/preprocessors/__init__.py +0 -0
- mfcli/pipeline/preprocessors/user_guide.py +127 -0
- mfcli/pipeline/run_context.py +32 -0
- mfcli/pipeline/schema_mapper.py +89 -0
- mfcli/pipeline/sub_classifier.py +115 -0
- mfcli/utils/__init__.py +0 -0
- mfcli/utils/cline_rules.py +256 -0
- mfcli/utils/config.py +33 -0
- mfcli/utils/configurator.py +324 -0
- mfcli/utils/data_cleaner.py +114 -0
- mfcli/utils/datasheet_vectorizer.py +283 -0
- mfcli/utils/directory_manager.py +116 -0
- mfcli/utils/file_upload.py +298 -0
- mfcli/utils/files.py +16 -0
- mfcli/utils/http_requests.py +54 -0
- mfcli/utils/kb_lister.py +89 -0
- mfcli/utils/kb_remover.py +173 -0
- mfcli/utils/logger.py +28 -0
- mfcli/utils/mcp_configurator.py +394 -0
- mfcli/utils/migrations.py +18 -0
- mfcli/utils/orm.py +43 -0
- mfcli/utils/pdf_splitter.py +63 -0
- mfcli/utils/pre_uninstall.py +167 -0
- mfcli/utils/query_service.py +22 -0
- mfcli/utils/system_check.py +306 -0
- mfcli/utils/tools.py +98 -0
- mfcli/utils/vectorizer.py +28 -0
- mfcli-0.2.1.dist-info/METADATA +956 -0
- mfcli-0.2.1.dist-info/RECORD +138 -0
- mfcli-0.2.1.dist-info/WHEEL +5 -0
- mfcli-0.2.1.dist-info/entry_points.txt +4 -0
- mfcli-0.2.1.dist-info/licenses/LICENSE +21 -0
- mfcli-0.2.1.dist-info/top_level.txt +1 -0
|
@@ -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
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Protel/Altium Designer Netlist Parser
|
|
4
|
+
|
|
5
|
+
Parses Protel/Altium Designer netlist files and extracts:
|
|
6
|
+
- Reference designators (ref_des)
|
|
7
|
+
- Part numbers/footprints
|
|
8
|
+
- Pin connections (pin number + net name)
|
|
9
|
+
|
|
10
|
+
Format example:
|
|
11
|
+
{COMPONENT PROTEL.PCB
|
|
12
|
+
{DETAIL
|
|
13
|
+
{SUBCOMP
|
|
14
|
+
{I <footprint>.PRT <ref_des>
|
|
15
|
+
{CN
|
|
16
|
+
<pin> <net>
|
|
17
|
+
...
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Dict
|
|
27
|
+
|
|
28
|
+
from mfcli.models.netlist import Component, NetlistSchema, Pin
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ProtelParser:
|
|
32
|
+
"""Parser for Protel/Altium Designer netlist files."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, protel_content: str):
|
|
35
|
+
self.content = protel_content
|
|
36
|
+
self.lines = [line.strip() for line in protel_content.strip().split('\n')]
|
|
37
|
+
self.components: Dict[str, Component] = {}
|
|
38
|
+
|
|
39
|
+
def parse(self) -> NetlistSchema:
|
|
40
|
+
"""Parse Protel content and return validated schema."""
|
|
41
|
+
# Validate header
|
|
42
|
+
if not self._validate_header():
|
|
43
|
+
raise ValueError("Not a valid Protel/Altium netlist file (missing {COMPONENT PROTEL.PCB header)")
|
|
44
|
+
|
|
45
|
+
# Parse components
|
|
46
|
+
self._parse_components()
|
|
47
|
+
|
|
48
|
+
# Validate and return
|
|
49
|
+
components_list = list(self.components.values())
|
|
50
|
+
return NetlistSchema(components=components_list)
|
|
51
|
+
|
|
52
|
+
def _validate_header(self) -> bool:
|
|
53
|
+
"""Check if file has Protel/Altium header."""
|
|
54
|
+
return len(self.lines) > 0 and '{COMPONENT PROTEL.PCB' in self.lines[0]
|
|
55
|
+
|
|
56
|
+
def _parse_components(self):
|
|
57
|
+
"""Parse components from the netlist."""
|
|
58
|
+
i = 0
|
|
59
|
+
while i < len(self.lines):
|
|
60
|
+
line = self.lines[i]
|
|
61
|
+
|
|
62
|
+
# Look for component definition: {I <footprint>.PRT <ref_des>
|
|
63
|
+
if line.startswith('{I ') and '.PRT ' in line:
|
|
64
|
+
# Extract footprint and ref_des
|
|
65
|
+
parts = line.split()
|
|
66
|
+
if len(parts) >= 3:
|
|
67
|
+
footprint = parts[1] # e.g., "0603.PRT"
|
|
68
|
+
ref_des = parts[2] # e.g., "C1"
|
|
69
|
+
|
|
70
|
+
# Remove .PRT extension from footprint
|
|
71
|
+
if footprint.endswith('.PRT'):
|
|
72
|
+
footprint = footprint[:-4]
|
|
73
|
+
|
|
74
|
+
# Create component
|
|
75
|
+
self.components[ref_des] = Component(
|
|
76
|
+
ref_des=ref_des,
|
|
77
|
+
part_number=footprint,
|
|
78
|
+
pins=[]
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Parse pins for this component
|
|
82
|
+
i = self._parse_pins(i + 1, ref_des)
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
i += 1
|
|
86
|
+
|
|
87
|
+
def _parse_pins(self, start_idx: int, ref_des: str) -> int:
|
|
88
|
+
"""
|
|
89
|
+
Parse pins for a component starting from the {CN block.
|
|
90
|
+
Returns the index after parsing all pins.
|
|
91
|
+
"""
|
|
92
|
+
i = start_idx
|
|
93
|
+
in_cn_block = False
|
|
94
|
+
|
|
95
|
+
while i < len(self.lines):
|
|
96
|
+
line = self.lines[i]
|
|
97
|
+
|
|
98
|
+
# Check if we're entering the {CN block
|
|
99
|
+
if line == '{CN':
|
|
100
|
+
in_cn_block = True
|
|
101
|
+
i += 1
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
# Check if we're exiting the {CN block or component block
|
|
105
|
+
if line == '}':
|
|
106
|
+
if in_cn_block:
|
|
107
|
+
in_cn_block = False
|
|
108
|
+
return i + 1 # Exit after {CN block closes
|
|
109
|
+
else:
|
|
110
|
+
return i + 1 # Exit after component block closes
|
|
111
|
+
|
|
112
|
+
# Parse pin connections within {CN block
|
|
113
|
+
if in_cn_block and line:
|
|
114
|
+
# Format: <pin_number> <net_name>
|
|
115
|
+
# Example: "1 3V3" or "2 GND"
|
|
116
|
+
parts = line.split(None, 1)
|
|
117
|
+
if len(parts) >= 1:
|
|
118
|
+
pin_number = parts[0]
|
|
119
|
+
net_name = parts[1] if len(parts) > 1 else ""
|
|
120
|
+
|
|
121
|
+
# Skip lines that don't start with a number (metadata)
|
|
122
|
+
if not pin_number or not pin_number[0].isdigit():
|
|
123
|
+
i += 1
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
# Add pin to component
|
|
127
|
+
if ref_des in self.components and net_name:
|
|
128
|
+
pin = Pin(pin=pin_number, net=net_name)
|
|
129
|
+
|
|
130
|
+
# Avoid duplicates
|
|
131
|
+
existing_pins = self.components[ref_des].pins
|
|
132
|
+
if not any(p.pin == pin.pin and p.net == pin.net for p in existing_pins):
|
|
133
|
+
self.components[ref_des].pins.append(pin)
|
|
134
|
+
|
|
135
|
+
i += 1
|
|
136
|
+
|
|
137
|
+
return i
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def parse_protel_file(filepath: Path) -> NetlistSchema:
|
|
141
|
+
"""
|
|
142
|
+
Parse a Protel/Altium Designer netlist file and return validated netlist schema.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
filepath: Path to Protel/Altium netlist file
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
NetlistSchema with components and pins
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
FileNotFoundError: If file doesn't exist
|
|
152
|
+
ValidationError: If parsed data doesn't match schema
|
|
153
|
+
ValueError: If file is not a valid Protel/Altium netlist
|
|
154
|
+
"""
|
|
155
|
+
if not filepath.exists():
|
|
156
|
+
raise FileNotFoundError(f"Protel/Altium netlist file not found: {filepath}")
|
|
157
|
+
|
|
158
|
+
# Read file content
|
|
159
|
+
with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
|
|
160
|
+
content = f.read()
|
|
161
|
+
|
|
162
|
+
# Parse
|
|
163
|
+
parser = ProtelParser(content)
|
|
164
|
+
schema = parser.parse()
|
|
165
|
+
|
|
166
|
+
return schema
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Protel/Altium Designer Netlist Detector
|
|
3
|
+
|
|
4
|
+
Helper function to detect if a file is a Protel/Altium Designer netlist.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def is_protel_netlist(content: str) -> bool:
|
|
9
|
+
"""
|
|
10
|
+
Detect if content is from a Protel/Altium Designer netlist file.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
content: File content to check
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
True if content appears to be a Protel/Altium netlist
|
|
17
|
+
"""
|
|
18
|
+
lines = content.strip().split('\n')
|
|
19
|
+
|
|
20
|
+
if not lines:
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
# Check for Protel/Altium header in first few lines
|
|
24
|
+
for line in lines[:5]:
|
|
25
|
+
line = line.strip()
|
|
26
|
+
if '{COMPONENT PROTEL.PCB' in line:
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
return False
|