Legora / db /parsers /bsa /clause_parser.py
sai-Rohan's picture
combined results of all parsers and made a single pipeline
dbabef2
Raw
History Blame Contribute Delete
9.13 kB
from __future__ import annotations
import re
from typing import List
from db.parsers.bsa.legal_models import (
Clause,
SubClause,
RomanClause
)
class ClauseParser:
# =====================================================
# REGEX
# =====================================================
CLAUSE_RE = re.compile(
r"(?m)^\s*\((\d+[A-Za-z]?)\)"
)
SUBCLAUSE_RE = re.compile(
r"(?m)^\s*\(([a-z])\)"
)
ROMAN_RE = re.compile(
r"(?m)^\s*\(([ivxlcdm]+)\)"
)
ROMAN_VALUES = {
"i", "ii", "iii", "iv", "v",
"vi", "vii", "viii", "ix", "x",
"xi", "xii", "xiii", "xiv", "xv",
"xvi", "xvii", "xviii", "xix", "xx"
}
STOP_WORDS = [
"Explanation.",
"Explanation.—",
"Explanation.-",
"Illustration.",
"Illustrations.",
"Exception.",
"Exception.—",
"Provided that",
"Provided further that"
]
# =====================================================
# SPLITTER
# =====================================================
def split_sections(
self,
text: str,
pattern: re.Pattern
):
matches = list(
pattern.finditer(text)
)
if not matches:
return []
sections = []
for i, match in enumerate(matches):
start = match.start()
end = (
matches[i + 1].start()
if i + 1 < len(matches)
else len(text)
)
sections.append(
(
match.group(1),
text[start:end].strip()
)
)
return sections
# =====================================================
# CLEAN CLAUSE TEXT
# =====================================================
def clean_clause_text(
self,
text: str
) -> str:
cutoff = len(text)
for word in self.STOP_WORDS:
pos = text.find(word)
if pos != -1:
cutoff = min(
cutoff,
pos
)
return text[:cutoff]
# =====================================================
# ROMAN CLAUSES
# =====================================================
def parse_roman_clauses(
self,
text: str
) -> List[RomanClause]:
romans = []
roman_sections = self.split_sections(
text,
re.compile(
r"(?m)^\s*\((i|ii|iii|iv|v|vi|vii|viii|ix|x|xi|xii|xiii|xiv|xv|xvi|xvii|xviii|xix|xx)\)"
)
)
for roman_no, roman_text in roman_sections:
romans.append(
RomanClause(
document="bsa",
roman_no=roman_no,
text=roman_text
)
)
return romans
# =====================================================
# SUBCLAUSES
# =====================================================
def parse_subclauses(
self,
text: str
) -> List[SubClause]:
subclauses = []
sub_sections = self.split_sections(
text,
self.SUBCLAUSE_RE
)
for sub_no, sub_text in sub_sections:
if (
sub_no.lower()
in self.ROMAN_VALUES
):
continue
sub = SubClause(
document="bsa",
sub_clause_no=sub_no,
text=sub_text
)
sub.roman_clauses.extend(
self.parse_roman_clauses(
sub_text
)
)
subclauses.append(
sub
)
return subclauses
# =====================================================
# CLAUSES
# =====================================================
def parse_clauses(
self,
section_text: str
) -> List[Clause]:
section_text = re.sub(
r"([;:—])\(([a-z])\)",
r"\1\n(\2)",
section_text
)
section_text = re.sub(
r"([.;:—])\((\d+[A-Za-z]?)\)",
r"\1\n(\2)",
section_text
)
section_text = re.sub(
r"(\d+\.)\s*(\(\d+[A-Za-z]?\))",
r"\1\n\2",
section_text
)
clauses = []
# =================================================
# NUMERIC CLAUSES
# =================================================
clause_sections = self.split_sections(
section_text,
self.CLAUSE_RE
)
if clause_sections:
for clause_no, clause_text in clause_sections:
clause = Clause(
document="bsa",
clause_no=clause_no,
text=clause_text
)
clean_text = self.clean_clause_text(
clause_text
)
clause.sub_clauses.extend(
self.parse_subclauses(
clean_text
)
)
if not clause.sub_clauses:
clause.roman_clauses.extend(
self.parse_roman_clauses(
clean_text
)
)
clauses.append(
clause
)
return clauses
# =================================================
# BSA STYLE
#
# (a)
# (b)
# (c)
#
# WITHOUT (1)
# =================================================
alpha_sections = self.split_sections(
section_text,
self.SUBCLAUSE_RE
)
if alpha_sections:
synthetic_clause = Clause(
document="bsa",
clause_no="0",
text=section_text
)
for alpha_no, alpha_text in alpha_sections:
if (
alpha_no.lower()
in self.ROMAN_VALUES
):
continue
sub = SubClause(
document="bsa",
sub_clause_no=alpha_no,
text=alpha_text
)
sub.roman_clauses.extend(
self.parse_roman_clauses(
alpha_text
)
)
synthetic_clause.sub_clauses.append(
sub
)
clauses.append(
synthetic_clause
)
return clauses
return []
# =====================================================
# VALIDATION
# =====================================================
def validate_clauses(
self,
clauses: List[Clause]
) -> List[str]:
errors = []
clause_seen = set()
for clause in clauses:
if (
clause.clause_no
in clause_seen
):
errors.append(
f"Duplicate Clause "
f"{clause.clause_no}"
)
clause_seen.add(
clause.clause_no
)
# ---------------------------------
# Romans directly under clause
# ---------------------------------
roman_seen = set()
for roman in clause.roman_clauses:
if (
roman.roman_no
in roman_seen
):
errors.append(
f"Duplicate Roman "
f"{roman.roman_no}"
)
roman_seen.add(
roman.roman_no
)
# ---------------------------------
# Subclauses
# ---------------------------------
sub_seen = set()
for sub in clause.sub_clauses:
if (
sub.sub_clause_no
in sub_seen
):
errors.append(
f"Duplicate SubClause "
f"{sub.sub_clause_no}"
)
sub_seen.add(
sub.sub_clause_no
)
roman_seen = set()
for roman in sub.roman_clauses:
if (
roman.roman_no
in roman_seen
):
errors.append(
f"Duplicate Roman "
f"{roman.roman_no}"
)
roman_seen.add(
roman.roman_no
)
return errors