Files
google--langextract/scripts/validate_community_providers.py
wehub-resource-sync 76d991c447
Auto Update PR / update-prs (push) Has been cancelled
CI / format-check (push) Has been cancelled
CI / test (3.10) (push) Has been cancelled
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / live-api-tests (push) Has been cancelled
CI / plugin-integration-test (push) Has been cancelled
CI / ollama-integration-test (push) Has been cancelled
CI / test-fork-pr (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:37:14 +08:00

210 lines
6.3 KiB
Python
Executable File

# Copyright 2025 Google LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#!/usr/bin/env python3
"""Validation for COMMUNITY_PROVIDERS.md plugin registry table."""
import os
from pathlib import Path
import re
import re as regex_module
import sys
from typing import Dict, List, Tuple
HEADER_ANCHOR = '| Plugin Name | PyPI Package |'
END_MARKER = '<!-- ADD NEW PLUGINS ABOVE THIS LINE -->'
# GitHub username/org and repo patterns
GH_NAME = r'[-a-zA-Z0-9]+' # usernames/orgs allow hyphens
GH_REPO = r'[-a-zA-Z0-9._]+' # repos allow ., _
GH_USER_LINK = rf'\[@{GH_NAME}\]\(https://github\.com/{GH_NAME}\)'
GH_MULTI_USER = rf'^{GH_USER_LINK}(,\s*{GH_USER_LINK})*$'
# Markdown link to a GitHub repo
GH_REPO_LINK = rf'^\[[^\]]+\]\(https://github\.com/{GH_NAME}/{GH_REPO}\)$'
# Issue link must point to LangExtract repository (issues only)
LANGEXTRACT_ISSUE_LINK = (
r'^\[[^\]]+\]\(https://github\.com/google/langextract/issues/\d+\)$'
)
# PEP 503-ish normalized name (loose): lowercase letters/digits with - _ . separators
PYPI_NORMALIZED = r'`[a-z0-9]([\-_.]?[a-z0-9]+)*`'
MIN_DESC_LEN = 10
def normalize_pypi(name: str) -> str:
"""PEP 503 normalization for PyPI package names."""
return regex_module.sub(r'[-_.]+', '-', name.strip().lower())
def find_table_bounds(lines: List[str]) -> Tuple[int, int]:
start = end = -1
for i, line in enumerate(lines):
if HEADER_ANCHOR in line:
start = i
elif start >= 0 and END_MARKER in line:
end = i
break
return start, end
def parse_row(line: str) -> List[str]:
# assumes caller trimmed line
parts = [c.strip() for c in line.split('|')[1:-1]]
return parts
def validate(filepath: Path) -> bool:
errors: List[str] = []
warnings: List[str] = []
content = filepath.read_text(encoding='utf-8')
lines = content.splitlines()
start, end = find_table_bounds(lines)
if start < 0:
errors.append('Could not find plugin registry table header.')
print_report(errors, warnings)
return False
if end < 0:
errors.append(
'Could not find end marker: <!-- ADD NEW PLUGINS ABOVE THIS LINE -->.'
)
print_report(errors, warnings)
return False
rows: List[Dict] = []
seen_names = set()
seen_pkgs = set()
for i in range(start + 2, end):
raw = lines[i].strip()
if not raw:
continue
if not raw.startswith('|') or not raw.endswith('|'):
errors.append(
f"Line {i+1}: Not a valid table row (must start and end with '|')."
)
continue
cols = parse_row(raw)
if len(cols) != 6:
errors.append(f'Line {i+1}: Expected 6 columns, found {len(cols)}.')
continue
plugin, pypi, maint, repo, desc, issue_link = cols
# Basic presence checks
if not plugin:
errors.append(f'Line {i+1}: Plugin Name is required.')
if not re.fullmatch(PYPI_NORMALIZED, pypi):
errors.append(
f'Line {i+1}: PyPI package must be backticked and normalized (e.g.,'
' `langextract-provider-foo`).'
)
elif pypi and not pypi.strip('`').lower().startswith('langextract-'):
errors.append(
f'Line {i+1}: PyPI package should start with `langextract-` for'
' discoverability.'
)
if not re.fullmatch(GH_MULTI_USER, maint):
errors.append(
f'Line {i+1}: Maintainer must be one or more GitHub handles as links '
'(e.g., [@alice](https://github.com/alice) or comma-separated).'
)
if not re.fullmatch(GH_REPO_LINK, repo):
errors.append(
f'Line {i+1}: GitHub Repo must be a Markdown link to a GitHub'
' repository.'
)
if not desc or len(desc) < MIN_DESC_LEN:
errors.append(
f'Line {i+1}: Description must be at least {MIN_DESC_LEN} characters.'
)
# Issue link is required and must point to LangExtract repo
if not issue_link:
errors.append(f'Line {i+1}: Issue Link is required.')
elif not re.fullmatch(LANGEXTRACT_ISSUE_LINK, issue_link):
errors.append(
f'Line {i+1}: Issue Link must point to a LangExtract issue (e.g.,'
' [#123](https://github.com/google/langextract/issues/123)).'
)
rows.append({
'line': i + 1,
'plugin': plugin,
'pypi': pypi.strip('`').lower() if pypi else '',
})
# Duplicate checks (case-insensitive and PEP 503 normalized)
for r in rows:
pn_key = r['plugin'].strip().casefold()
pk_key = normalize_pypi(r['pypi']) if r['pypi'] else None
if pn_key in seen_names:
errors.append(f"Line {r['line']}: Duplicate Plugin Name '{r['plugin']}'.")
seen_names.add(pn_key)
if pk_key and pk_key in seen_pkgs:
errors.append(f"Line {r['line']}: Duplicate PyPI Package '{r['pypi']}'.")
if pk_key:
seen_pkgs.add(pk_key)
# Required alphabetical sorting check
sorted_by_name = sorted(rows, key=lambda r: r['plugin'].casefold())
if [r['plugin'] for r in rows] != [r['plugin'] for r in sorted_by_name]:
errors.append('Registry rows must be alphabetically sorted by Plugin Name.')
# Guardrail: discourage leaving only the example entry
if len(rows) == 1 and rows[0]['plugin'].lower().startswith('example'):
warnings.append(
'The registry currently contains only the example row. Add real'
' providers above the marker.'
)
print_report(errors, warnings)
return not errors
def print_report(errors: List[str], warnings: List[str]) -> None:
if errors:
print('❌ Validation failed:')
for e in errors:
print(f' • {e}')
if warnings:
print('⚠️ Warnings:')
for w in warnings:
print(f' • {w}')
if not errors and not warnings:
print('✅ Table format validation passed!')
if __name__ == '__main__':
path = Path('COMMUNITY_PROVIDERS.md')
if len(sys.argv) > 1:
path = Path(sys.argv[1])
if not path.exists():
print(f'❌ Error: File not found: {path}')
sys.exit(1)
ok = validate(path)
sys.exit(0 if ok else 1)