feat: add pcf analysis web app and api
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
from app.config import AppConfig
|
||||
from app.deepseek import persist_high_confidence_mappings
|
||||
from app.models import MappingDecision
|
||||
import yaml
|
||||
|
||||
|
||||
def test_high_confidence_mapping_is_accepted(tmp_path):
|
||||
source = AppConfig.load()
|
||||
config_path = tmp_path / "mapping.yaml"
|
||||
config_path.write_text(source.path.read_text(encoding="utf-8"), encoding="utf-8")
|
||||
config = AppConfig.load(config_path)
|
||||
|
||||
accepted = persist_high_confidence_mappings(
|
||||
config,
|
||||
[
|
||||
MappingDecision(
|
||||
source_field="LINE-NO",
|
||||
target_field="line_no",
|
||||
confidence=0.96,
|
||||
reason="字段名和样例都表示管线号",
|
||||
),
|
||||
MappingDecision(
|
||||
source_field="UNKNOWN-X",
|
||||
target_field="diameter",
|
||||
confidence=0.3,
|
||||
),
|
||||
],
|
||||
threshold=0.85,
|
||||
)
|
||||
|
||||
assert accepted == {"LINE-NO": "line_no"}
|
||||
reloaded = AppConfig.load(config_path)
|
||||
assert reloaded.dynamic_field_mappings["LINE-NO"] == "line_no"
|
||||
assert "UNKNOWN-X" not in reloaded.dynamic_field_mappings
|
||||
|
||||
|
||||
def test_deepseek_key_reads_from_config(tmp_path):
|
||||
source = AppConfig.load()
|
||||
config_path = tmp_path / "mapping.yaml"
|
||||
data = yaml.safe_load(source.path.read_text(encoding="utf-8"))
|
||||
data.setdefault("deepseek", {})["api_key"] = "test-key"
|
||||
config_path.write_text(yaml.safe_dump(data, allow_unicode=True, sort_keys=False), encoding="utf-8")
|
||||
config = AppConfig.load(config_path)
|
||||
|
||||
from app.deepseek import DeepSeekMapper
|
||||
|
||||
mapper = DeepSeekMapper(config)
|
||||
assert mapper.api_key == "test-key"
|
||||
assert mapper.enabled
|
||||
@@ -0,0 +1,25 @@
|
||||
from pathlib import Path
|
||||
|
||||
import xlrd
|
||||
|
||||
from app.config import AppConfig
|
||||
from app.excel_writer import XlsTemplateWriter
|
||||
from app.pcf_parser import PcfParser
|
||||
|
||||
|
||||
def test_write_sample_xls(tmp_path):
|
||||
config = AppConfig.load()
|
||||
parser = PcfParser(config)
|
||||
files = list(Path("PCF文件样本(1)").glob("*.pcf"))
|
||||
records, _candidates, _messages = parser.parse_files(files)
|
||||
|
||||
output = tmp_path / "result.xls"
|
||||
XlsTemplateWriter(config).write(records, output)
|
||||
|
||||
book = xlrd.open_workbook(output)
|
||||
sheet = book.sheet_by_name("Sheet1")
|
||||
assert sheet.cell_value(0, 0) == "单位代码"
|
||||
assert sheet.nrows >= 170
|
||||
assert sheet.cell_value(1, 2)
|
||||
assert sheet.cell_value(1, 3)
|
||||
assert sheet.cell_value(1, 11) in {"安装", "预制"}
|
||||
@@ -0,0 +1,45 @@
|
||||
from pathlib import Path
|
||||
|
||||
from app.config import AppConfig
|
||||
from app.pcf_parser import PcfParser, parse_blocks
|
||||
|
||||
|
||||
def test_sample_pcf_weld_count():
|
||||
config = AppConfig.load()
|
||||
parser = PcfParser(config)
|
||||
files = list(Path("PCF文件样本(1)").glob("*.pcf"))
|
||||
records, candidates, messages = parser.parse_files(files)
|
||||
|
||||
assert len(files) == 20
|
||||
assert len(records) == 169
|
||||
assert not messages
|
||||
assert records[0].line_no
|
||||
assert records[0].pipeline_reference
|
||||
assert records[0].weld_area in {"安装", "预制"}
|
||||
|
||||
|
||||
def test_parse_nonstandard_field_candidate():
|
||||
text = """
|
||||
PIPELINE-REFERENCE L-001
|
||||
PIPING-SPEC C10C
|
||||
WELD
|
||||
COMPONENT-IDENTIFIER 1
|
||||
LINE-NO 01ABC
|
||||
WELD-DIA 150
|
||||
WELD-ATTRIBUTE3 Shop weld
|
||||
""".strip()
|
||||
blocks = parse_blocks(text)
|
||||
assert blocks[0].block_type == "PIPELINE-REFERENCE"
|
||||
assert blocks[1].block_type == "WELD"
|
||||
|
||||
tmp = Path("tests/_tmp_nonstandard.pcf")
|
||||
tmp.write_text(text, encoding="utf-8")
|
||||
try:
|
||||
config = AppConfig.load()
|
||||
parser = PcfParser(config)
|
||||
_records, candidates, _messages = parser.parse_files([tmp])
|
||||
fields = {c.source_field for c in candidates}
|
||||
assert "LINE-NO" in fields
|
||||
assert "WELD-DIA" in fields
|
||||
finally:
|
||||
tmp.unlink(missing_ok=True)
|
||||
@@ -0,0 +1,104 @@
|
||||
from pathlib import Path
|
||||
import os
|
||||
import threading
|
||||
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.main import app
|
||||
|
||||
|
||||
def test_web_upload_sample_files():
|
||||
client = TestClient(app)
|
||||
opened = []
|
||||
try:
|
||||
files = []
|
||||
for path in sorted(Path("PCF文件样本(1)").glob("*.pcf")):
|
||||
handle = path.open("rb")
|
||||
opened.append(handle)
|
||||
files.append(("files", (path.name, handle, "text/plain")))
|
||||
|
||||
response = client.post("/api/jobs", files=files)
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["input_files"] == 20
|
||||
assert payload["weld_count"] == 169
|
||||
assert len(payload["tables"]["import_rows"]) == 169
|
||||
assert {"key": "unit_code", "label": "单位代码"} in payload["tables"]["import_columns"]
|
||||
assert "unit_code" in payload["tables"]["import_rows"][0]
|
||||
assert "line_no" in payload["tables"]["import_rows"][0]
|
||||
assert "单位代码" not in payload["tables"]["import_rows"][0]
|
||||
assert payload["tables"]["issue_rows"]
|
||||
assert "单位代码" in payload["tables"]["import_headers"]
|
||||
|
||||
xls = client.get(payload["downloads"]["xls"])
|
||||
assert xls.status_code == 200
|
||||
assert xls.content[:8] == b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1"
|
||||
finally:
|
||||
for handle in opened:
|
||||
handle.close()
|
||||
|
||||
|
||||
def test_parse_url_api_returns_json_payload(tmp_path):
|
||||
sample = sorted(Path("PCF文件样本(1)").glob("*.pcf"))[0]
|
||||
hosted = tmp_path / sample.name
|
||||
hosted.write_bytes(sample.read_bytes())
|
||||
|
||||
class QuietHandler(SimpleHTTPRequestHandler):
|
||||
def log_message(self, format, *args):
|
||||
return
|
||||
|
||||
old_cwd = os.getcwd()
|
||||
os.chdir(tmp_path)
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), QuietHandler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
url = f"http://127.0.0.1:{server.server_port}/{hosted.name}"
|
||||
client = TestClient(app)
|
||||
response = client.post(
|
||||
"/api/AnalysisPcf",
|
||||
json={"attachment_url": url},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["input_files"] == 1
|
||||
assert payload["weld_count"] > 0
|
||||
assert payload["tables"]["import_rows"]
|
||||
assert "line_no" in payload["tables"]["import_rows"][0]
|
||||
assert "管线代号" not in payload["tables"]["import_rows"][0]
|
||||
assert "files" not in payload
|
||||
assert "downloads" not in payload
|
||||
assert payload["report"]["summary"]["weld_count"] == payload["weld_count"]
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
os.chdir(old_cwd)
|
||||
|
||||
|
||||
def test_parse_url_api_accepts_local_file_path():
|
||||
sample = sorted(Path("PCF文件样本(1)").glob("*.pcf"))[0].resolve()
|
||||
client = TestClient(app)
|
||||
response = client.post(
|
||||
"/api/AnalysisPcf",
|
||||
json={"attachment_url": str(sample)},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["input_files"] == 1
|
||||
assert payload["weld_count"] > 0
|
||||
assert payload["tables"]["import_rows"]
|
||||
|
||||
|
||||
def test_parse_url_api_accepts_file_uri():
|
||||
sample = sorted(Path("PCF文件样本(1)").glob("*.pcf"))[0].resolve()
|
||||
client = TestClient(app)
|
||||
response = client.post(
|
||||
"/api/AnalysisPcf",
|
||||
json={"attachment_url": sample.as_uri()},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["input_files"] == 1
|
||||
assert payload["weld_count"] > 0
|
||||
assert "downloads" not in payload
|
||||
Reference in New Issue
Block a user