Initial import of NavSea pbf project
This commit is contained in:
237
navsea_mapping_registry.py
Normal file
237
navsea_mapping_registry.py
Normal file
@@ -0,0 +1,237 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import pymysql
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FieldValueRule:
|
||||
field_name_jp: str
|
||||
legacy_value: str
|
||||
standardized_field: str
|
||||
standardized_value: str
|
||||
geom_scope: tuple[str, ...]
|
||||
source_layer_scope: tuple[str, ...]
|
||||
canonical_object_scope: tuple[str, ...]
|
||||
|
||||
def matches(self, context: dict[str, str]) -> bool:
|
||||
if self.geom_scope and context.get("geom_type", "") not in self.geom_scope:
|
||||
return False
|
||||
if self.source_layer_scope and context.get("source_layer", "") not in self.source_layer_scope:
|
||||
return False
|
||||
if self.canonical_object_scope and context.get("canonical_object_type", "") not in self.canonical_object_scope:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RenderRule:
|
||||
rule_id: str
|
||||
priority: int
|
||||
match_expr: dict[str, Any]
|
||||
output: dict[str, Any]
|
||||
|
||||
def matches(self, context: dict[str, Any]) -> bool:
|
||||
return evaluate_match_expr(self.match_expr, context)
|
||||
|
||||
|
||||
def normalize_csv_scope(value: str | None) -> tuple[str, ...]:
|
||||
if not value:
|
||||
return ()
|
||||
return tuple(part.strip() for part in str(value).split(",") if part.strip())
|
||||
|
||||
|
||||
def evaluate_match_expr(match_expr: dict[str, Any], context: dict[str, Any]) -> bool:
|
||||
clauses = match_expr.get("all") or []
|
||||
for clause in clauses:
|
||||
field = clause.get("field")
|
||||
op = clause.get("op")
|
||||
expected = clause.get("value")
|
||||
actual = context.get(str(field))
|
||||
actual_text = "" if actual is None else str(actual)
|
||||
|
||||
if op == "eq":
|
||||
if actual_text != str(expected):
|
||||
return False
|
||||
elif op == "neq":
|
||||
if actual_text == str(expected):
|
||||
return False
|
||||
elif op == "in":
|
||||
if actual_text not in {str(item) for item in (expected or [])}:
|
||||
return False
|
||||
elif op == "contains":
|
||||
if str(expected) not in actual_text:
|
||||
return False
|
||||
elif op == "contains_any":
|
||||
values = [str(item) for item in (expected or [])]
|
||||
if not any(item in actual_text for item in values):
|
||||
return False
|
||||
elif op == "is_null":
|
||||
if actual not in (None, ""):
|
||||
return False
|
||||
elif op == "not_null":
|
||||
if actual in (None, ""):
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class NavSeaMappingRegistry:
|
||||
def __init__(
|
||||
self,
|
||||
bundle_id: str,
|
||||
bundle_version: str,
|
||||
source_layer_rules: dict[str, dict[str, str]],
|
||||
field_value_rules: dict[tuple[str, str], list[FieldValueRule]],
|
||||
render_rules: list[RenderRule],
|
||||
) -> None:
|
||||
self.bundle_id = bundle_id
|
||||
self.bundle_version = bundle_version
|
||||
self.source_layer_rules = source_layer_rules
|
||||
self.field_value_rules = field_value_rules
|
||||
self.render_rules = render_rules
|
||||
|
||||
@classmethod
|
||||
def load(
|
||||
cls,
|
||||
conn: pymysql.Connection,
|
||||
bundle_id: str | None = None,
|
||||
) -> "NavSeaMappingRegistry":
|
||||
with conn.cursor() as cur:
|
||||
if bundle_id:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT bundle_id, bundle_version
|
||||
FROM navsea_rule_bundle
|
||||
WHERE bundle_id = %s
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
(bundle_id,),
|
||||
)
|
||||
else:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT bundle_id, bundle_version
|
||||
FROM navsea_rule_bundle
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
)
|
||||
bundle = cur.fetchone()
|
||||
if not bundle:
|
||||
raise RuntimeError("navsea mapping registry is empty")
|
||||
|
||||
resolved_bundle_id = str(bundle["bundle_id"])
|
||||
resolved_bundle_version = str(bundle["bundle_version"])
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT source_layer_jp, source_layer_std
|
||||
FROM navsea_source_layer_rules
|
||||
WHERE bundle_id = %s AND bundle_version = %s
|
||||
""",
|
||||
(resolved_bundle_id, resolved_bundle_version),
|
||||
)
|
||||
source_layer_rules = {
|
||||
str(row["source_layer_jp"]): {
|
||||
"source_layer_std": str(row["source_layer_std"]),
|
||||
"source_layer_rule_id": f"SLR:{row['source_layer_jp']}",
|
||||
}
|
||||
for row in cur.fetchall()
|
||||
}
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT
|
||||
field_name_jp,
|
||||
legacy_value,
|
||||
standardized_field,
|
||||
standardized_value,
|
||||
geom_scope,
|
||||
source_layer_scope,
|
||||
canonical_object_scope
|
||||
FROM navsea_field_value_rules
|
||||
WHERE bundle_id = %s AND bundle_version = %s
|
||||
ORDER BY rule_priority ASC, rule_id ASC, rule_revision ASC
|
||||
""",
|
||||
(resolved_bundle_id, resolved_bundle_version),
|
||||
)
|
||||
field_value_rules: dict[tuple[str, str], list[FieldValueRule]] = {}
|
||||
for row in cur.fetchall():
|
||||
key = (str(row["field_name_jp"]), str(row["legacy_value"]))
|
||||
field_value_rules.setdefault(key, []).append(
|
||||
FieldValueRule(
|
||||
field_name_jp=str(row["field_name_jp"]),
|
||||
legacy_value=str(row["legacy_value"]),
|
||||
standardized_field=str(row["standardized_field"]),
|
||||
standardized_value=str(row["standardized_value"]),
|
||||
geom_scope=normalize_csv_scope(row.get("geom_scope")),
|
||||
source_layer_scope=normalize_csv_scope(row.get("source_layer_scope")),
|
||||
canonical_object_scope=normalize_csv_scope(row.get("canonical_object_scope")),
|
||||
)
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT rule_id, priority, match_expr_json, output_json
|
||||
FROM navsea_render_rules
|
||||
WHERE bundle_id = %s AND bundle_version = %s AND enabled = 1
|
||||
ORDER BY priority ASC, rule_id ASC, rule_revision ASC
|
||||
""",
|
||||
(resolved_bundle_id, resolved_bundle_version),
|
||||
)
|
||||
render_rules = []
|
||||
for row in cur.fetchall():
|
||||
render_rules.append(
|
||||
RenderRule(
|
||||
rule_id=str(row["rule_id"]),
|
||||
priority=int(row["priority"]),
|
||||
match_expr=json.loads(row["match_expr_json"]),
|
||||
output=json.loads(row["output_json"]),
|
||||
)
|
||||
)
|
||||
|
||||
return cls(
|
||||
bundle_id=resolved_bundle_id,
|
||||
bundle_version=resolved_bundle_version,
|
||||
source_layer_rules=source_layer_rules,
|
||||
field_value_rules=field_value_rules,
|
||||
render_rules=render_rules,
|
||||
)
|
||||
|
||||
def resolve_source_layer(self, source_layer_jp: str) -> tuple[str, str]:
|
||||
rule = self.source_layer_rules.get(source_layer_jp)
|
||||
if not rule:
|
||||
return source_layer_jp, f"SLR:{source_layer_jp}:identity"
|
||||
return rule["source_layer_std"], rule["source_layer_rule_id"]
|
||||
|
||||
def standardize_field_value(
|
||||
self,
|
||||
field_name_jp: str,
|
||||
legacy_value: object,
|
||||
standardized_field: str,
|
||||
*,
|
||||
context: dict[str, str] | None = None,
|
||||
) -> str | None:
|
||||
if legacy_value in (None, ""):
|
||||
return None
|
||||
key = (field_name_jp, str(legacy_value))
|
||||
rules = self.field_value_rules.get(key, [])
|
||||
match_context = context or {}
|
||||
for rule in rules:
|
||||
if rule.standardized_field != standardized_field:
|
||||
continue
|
||||
if rule.matches(match_context):
|
||||
return rule.standardized_value
|
||||
return None
|
||||
|
||||
def resolve_render_rule(self, context: dict[str, Any]) -> tuple[dict[str, Any], str | None]:
|
||||
for rule in self.render_rules:
|
||||
if rule.matches(context):
|
||||
return dict(rule.output), rule.rule_id
|
||||
return {}, None
|
||||
Reference in New Issue
Block a user