Migrate all repos into monorepo context folders
Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)
Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
"""PAT Registry module for loading, validating, and persisting the PAT registry."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from .errors import RegistryValidationError
|
||||
from .models import PatEntry
|
||||
|
||||
# Valid service names and their required renewal methods
|
||||
VALID_SERVICES: dict[str, str] = {
|
||||
"GitLab": "auto",
|
||||
"Jira": "manual",
|
||||
"Confluence": "manual",
|
||||
"OrgMyLife": "manual",
|
||||
}
|
||||
|
||||
# ISO 8601 date pattern (YYYY-MM-DD)
|
||||
_ISO_DATE_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
||||
|
||||
# Maximum token name length
|
||||
_MAX_TOKEN_NAME_LENGTH = 128
|
||||
|
||||
|
||||
class PatRegistry:
|
||||
"""Manages loading, validation, and persistence of the PAT registry.
|
||||
|
||||
The registry is a JSON file with structure:
|
||||
{"version": "1.0", "tokens": [...]}
|
||||
|
||||
Each token entry contains: service, token_name, expiry_date, renewal_method.
|
||||
"""
|
||||
|
||||
def load(self, path: Path) -> list[PatEntry]:
|
||||
"""Load and validate all entries from a PAT registry JSON file.
|
||||
|
||||
Args:
|
||||
path: Path to the registry JSON file.
|
||||
|
||||
Returns:
|
||||
List of validated PatEntry objects.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the registry file does not exist.
|
||||
json.JSONDecodeError: If the file contains invalid JSON.
|
||||
RegistryValidationError: If any entry fails validation.
|
||||
"""
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
if not isinstance(data, dict):
|
||||
raise RegistryValidationError(
|
||||
entry={},
|
||||
failed_fields=["root"],
|
||||
)
|
||||
|
||||
tokens = data.get("tokens", [])
|
||||
if not isinstance(tokens, list):
|
||||
raise RegistryValidationError(
|
||||
entry={},
|
||||
failed_fields=["tokens"],
|
||||
)
|
||||
|
||||
entries: list[PatEntry] = []
|
||||
seen: set[tuple[str, str]] = set()
|
||||
|
||||
for raw_entry in tokens:
|
||||
entry = self.validate_entry(raw_entry)
|
||||
# Check for duplicates
|
||||
key = (entry.service, entry.token_name)
|
||||
if key in seen:
|
||||
raise RegistryValidationError(
|
||||
entry=raw_entry,
|
||||
failed_fields=["service", "token_name"],
|
||||
)
|
||||
seen.add(key)
|
||||
entries.append(entry)
|
||||
|
||||
return entries
|
||||
|
||||
def validate_entry(self, entry: dict) -> PatEntry:
|
||||
"""Validate a single registry entry dictionary.
|
||||
|
||||
Checks:
|
||||
- All required fields are present (service, token_name, renewal_method)
|
||||
- service is one of the supported values
|
||||
- token_name is non-empty and max 128 characters
|
||||
- expiry_date is null or valid ISO 8601 (YYYY-MM-DD)
|
||||
- renewal_method matches the service (GitLab -> "auto", others -> "manual")
|
||||
|
||||
Args:
|
||||
entry: Raw dictionary representing a PAT entry.
|
||||
|
||||
Returns:
|
||||
A validated PatEntry instance.
|
||||
|
||||
Raises:
|
||||
RegistryValidationError: If validation fails, with specific failed fields.
|
||||
"""
|
||||
if not isinstance(entry, dict):
|
||||
raise RegistryValidationError(
|
||||
entry=entry if isinstance(entry, dict) else {"_raw": str(entry)},
|
||||
failed_fields=["entry_type"],
|
||||
)
|
||||
|
||||
failed_fields: list[str] = []
|
||||
|
||||
# Check required fields presence
|
||||
required_fields = ["service", "token_name", "renewal_method"]
|
||||
for field in required_fields:
|
||||
if field not in entry:
|
||||
failed_fields.append(field)
|
||||
|
||||
# If required fields are missing, raise immediately
|
||||
if failed_fields:
|
||||
raise RegistryValidationError(entry=entry, failed_fields=failed_fields)
|
||||
|
||||
# Validate service
|
||||
service = entry["service"]
|
||||
if service not in VALID_SERVICES:
|
||||
failed_fields.append("service")
|
||||
|
||||
# Validate token_name
|
||||
token_name = entry["token_name"]
|
||||
if (
|
||||
not isinstance(token_name, str)
|
||||
or len(token_name) == 0
|
||||
or len(token_name) > _MAX_TOKEN_NAME_LENGTH
|
||||
):
|
||||
failed_fields.append("token_name")
|
||||
|
||||
# Validate expiry_date (can be null/None or valid ISO 8601 YYYY-MM-DD)
|
||||
expiry_date = entry.get("expiry_date")
|
||||
if expiry_date is not None:
|
||||
if not isinstance(expiry_date, str) or not _is_valid_iso_date(expiry_date):
|
||||
failed_fields.append("expiry_date")
|
||||
|
||||
# Validate renewal_method matches service
|
||||
renewal_method = entry["renewal_method"]
|
||||
if service in VALID_SERVICES:
|
||||
expected_method = VALID_SERVICES[service]
|
||||
if renewal_method != expected_method:
|
||||
failed_fields.append("renewal_method")
|
||||
elif renewal_method not in ("auto", "manual"):
|
||||
# If service is invalid, still check renewal_method is at least valid
|
||||
failed_fields.append("renewal_method")
|
||||
|
||||
if failed_fields:
|
||||
raise RegistryValidationError(entry=entry, failed_fields=failed_fields)
|
||||
|
||||
return PatEntry(
|
||||
service=service,
|
||||
token_name=token_name,
|
||||
expiry_date=expiry_date,
|
||||
renewal_method=renewal_method,
|
||||
)
|
||||
|
||||
def save(self, path: Path, entries: list[PatEntry]) -> None:
|
||||
"""Persist a list of PAT entries to a registry JSON file.
|
||||
|
||||
Args:
|
||||
path: Path where the registry file should be written.
|
||||
entries: List of PatEntry objects to serialize.
|
||||
"""
|
||||
data = {
|
||||
"version": "1.0",
|
||||
"tokens": [
|
||||
{
|
||||
"service": entry.service,
|
||||
"token_name": entry.token_name,
|
||||
"expiry_date": entry.expiry_date,
|
||||
"renewal_method": entry.renewal_method,
|
||||
}
|
||||
for entry in entries
|
||||
],
|
||||
}
|
||||
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
def create_empty(self, path: Path) -> None:
|
||||
"""Create a new empty registry file with valid JSON structure.
|
||||
|
||||
Args:
|
||||
path: Path where the empty registry file should be created.
|
||||
"""
|
||||
self.save(path, [])
|
||||
|
||||
|
||||
def _is_valid_iso_date(date_str: str) -> bool:
|
||||
"""Check if a string is a valid ISO 8601 date (YYYY-MM-DD).
|
||||
|
||||
Validates both the format and that the date values are actually valid
|
||||
(e.g., rejects 2025-02-30).
|
||||
"""
|
||||
if not _ISO_DATE_PATTERN.match(date_str):
|
||||
return False
|
||||
|
||||
# Validate actual date values
|
||||
try:
|
||||
year, month, day = date_str.split("-")
|
||||
year_int, month_int, day_int = int(year), int(month), int(day)
|
||||
|
||||
# Basic range checks
|
||||
if month_int < 1 or month_int > 12:
|
||||
return False
|
||||
if day_int < 1 or day_int > 31:
|
||||
return False
|
||||
|
||||
# Use datetime for full validation (handles leap years, etc.)
|
||||
import datetime
|
||||
|
||||
datetime.date(year_int, month_int, day_int)
|
||||
return True
|
||||
except (ValueError, OverflowError):
|
||||
return False
|
||||
Reference in New Issue
Block a user