mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-19 04:40:32 -05:00
b4486a627f
- Webhook models: remove duplicate index definitions so db.create_all() no longer raises 'index already exists' (columns already have index=True) - ImportService: fix circular import by late-importing ClientService, ProjectService, TimeTrackingService in __init__ - reports: fix F823 by renaming unpack variable _ to _entry_count to avoid shadowing gettext _ in export_task_excel() - Code quality: add .flake8 with extend-ignore so flake8 CI passes; simplify pyproject.toml isort config (drop unsupported options) - Format: run black and isort on app/ - tests: restore minimal app fixture in test_import_export_models
244 lines
5.4 KiB
Python
244 lines
5.4 KiB
Python
"""
|
|
PEPPOL participant identifier validation (scheme + endpoint ID).
|
|
|
|
Validates and normalizes sender/recipient identifiers before submission
|
|
to avoid malformed IDs and improve validator compliance.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Optional, Tuple
|
|
|
|
# Common PEPPOL participant identifier schemes (ISO 6523)
|
|
# See PEPPOL IC and country-specific scheme lists
|
|
KNOWN_SCHEMES = frozenset(
|
|
{
|
|
"0007",
|
|
"0088",
|
|
"0060",
|
|
"0130",
|
|
"0184",
|
|
"0190",
|
|
"0191",
|
|
"0192",
|
|
"0193",
|
|
"0195",
|
|
"0196",
|
|
"0198",
|
|
"0199",
|
|
"0200",
|
|
"0201",
|
|
"0202",
|
|
"0204",
|
|
"0208",
|
|
"0209",
|
|
"0210",
|
|
"0211",
|
|
"0212",
|
|
"0213",
|
|
"0215",
|
|
"0216",
|
|
"0218",
|
|
"0219",
|
|
"0220",
|
|
"0221",
|
|
"0222",
|
|
"0223",
|
|
"0224",
|
|
"0225",
|
|
"0226",
|
|
"0227",
|
|
"0228",
|
|
"0229",
|
|
"0230",
|
|
"0231",
|
|
"0232",
|
|
"0233",
|
|
"0234",
|
|
"0235",
|
|
"0236",
|
|
"0237",
|
|
"0238",
|
|
"0239",
|
|
"0240",
|
|
"0241",
|
|
"0242",
|
|
"0243",
|
|
"0244",
|
|
"0245",
|
|
"0246",
|
|
"0247",
|
|
"0248",
|
|
"0249",
|
|
"0250",
|
|
"0251",
|
|
"0252",
|
|
"0253",
|
|
"0254",
|
|
"0255",
|
|
"0256",
|
|
"0257",
|
|
"0258",
|
|
"0259",
|
|
"0260",
|
|
"0261",
|
|
"0262",
|
|
"0263",
|
|
"0264",
|
|
"0265",
|
|
"0266",
|
|
"0267",
|
|
"0268",
|
|
"0269",
|
|
"0270",
|
|
"0271",
|
|
"0272",
|
|
"0273",
|
|
"0274",
|
|
"0275",
|
|
"0276",
|
|
"0277",
|
|
"0278",
|
|
"0279",
|
|
"0280",
|
|
"0281",
|
|
"0282",
|
|
"0283",
|
|
"0284",
|
|
"0285",
|
|
"0286",
|
|
"0287",
|
|
"0288",
|
|
"0289",
|
|
"0290",
|
|
"0291",
|
|
"0292",
|
|
"0293",
|
|
"0294",
|
|
"0295",
|
|
"0296",
|
|
"0297",
|
|
"0298",
|
|
"0299",
|
|
"0300",
|
|
"9915",
|
|
"9925",
|
|
"9933",
|
|
"9944",
|
|
"9950",
|
|
"9952",
|
|
"9954",
|
|
"9955",
|
|
"9956",
|
|
"9957",
|
|
"9958",
|
|
"9959",
|
|
"9960",
|
|
"9961",
|
|
"9962",
|
|
"9963",
|
|
"9964",
|
|
"9965",
|
|
"9966",
|
|
"9967",
|
|
"9968",
|
|
"9969",
|
|
"9970",
|
|
"9971",
|
|
"9972",
|
|
"9973",
|
|
"9974",
|
|
"9975",
|
|
"9976",
|
|
"9977",
|
|
"9978",
|
|
"9979",
|
|
"9980",
|
|
"9981",
|
|
"9982",
|
|
"9983",
|
|
"9984",
|
|
"9985",
|
|
"9986",
|
|
"9987",
|
|
"9988",
|
|
"9989",
|
|
"9990",
|
|
"9991",
|
|
"9992",
|
|
"9993",
|
|
"9994",
|
|
"9995",
|
|
"9996",
|
|
"9997",
|
|
"9998",
|
|
"9999",
|
|
}
|
|
)
|
|
|
|
# Endpoint ID: alphanumeric, some schemes allow colon/dash (e.g. 0088:1234567890123)
|
|
_ENDPOINT_ID_PATTERN = re.compile(r"^[A-Za-z0-9_\-.:]+$")
|
|
|
|
|
|
class PeppolIdentifierError(ValueError):
|
|
"""Raised when a PEPPOL participant identifier is invalid."""
|
|
|
|
def __init__(self, message: str, field: Optional[str] = None):
|
|
super().__init__(message)
|
|
self.field = field
|
|
|
|
|
|
def validate_scheme_id(scheme_id: Optional[str], field: str = "scheme_id") -> str:
|
|
"""
|
|
Validate and return normalized scheme ID.
|
|
Raises PeppolIdentifierError if invalid.
|
|
"""
|
|
if not scheme_id or not str(scheme_id).strip():
|
|
raise PeppolIdentifierError("Participant scheme ID is required", field=field)
|
|
s = str(scheme_id).strip()
|
|
if not s.isdigit() and s not in KNOWN_SCHEMES:
|
|
# Allow unknown numeric schemes (4 digits typical)
|
|
if not (len(s) <= 10 and all(c.isdigit() for c in s)):
|
|
raise PeppolIdentifierError(
|
|
f"Invalid participant scheme ID: must be numeric or known scheme (e.g. 0088, 9915)",
|
|
field=field,
|
|
)
|
|
return s
|
|
|
|
|
|
def validate_endpoint_id(endpoint_id: Optional[str], field: str = "endpoint_id") -> str:
|
|
"""
|
|
Validate and return normalized endpoint ID.
|
|
Raises PeppolIdentifierError if invalid.
|
|
"""
|
|
if not endpoint_id or not str(endpoint_id).strip():
|
|
raise PeppolIdentifierError("Participant endpoint ID is required", field=field)
|
|
e = str(endpoint_id).strip()
|
|
if len(e) > 200:
|
|
raise PeppolIdentifierError("Endpoint ID must be at most 200 characters", field=field)
|
|
if not _ENDPOINT_ID_PATTERN.match(e):
|
|
raise PeppolIdentifierError(
|
|
"Endpoint ID may only contain letters, digits, and _ - . :",
|
|
field=field,
|
|
)
|
|
return e
|
|
|
|
|
|
def validate_participant_identifiers(
|
|
sender_endpoint_id: str,
|
|
sender_scheme_id: str,
|
|
recipient_endpoint_id: str,
|
|
recipient_scheme_id: str,
|
|
) -> Tuple[Tuple[str, str], Tuple[str, str]]:
|
|
"""
|
|
Validate sender and recipient identifiers.
|
|
Returns ((sender_endpoint_id, sender_scheme_id), (recipient_endpoint_id, recipient_scheme_id)).
|
|
Raises PeppolIdentifierError if any identifier is invalid.
|
|
"""
|
|
s_ep = validate_endpoint_id(sender_endpoint_id, "sender_endpoint_id")
|
|
s_sch = validate_scheme_id(sender_scheme_id, "sender_scheme_id")
|
|
r_ep = validate_endpoint_id(recipient_endpoint_id, "recipient_endpoint_id")
|
|
r_sch = validate_scheme_id(recipient_scheme_id, "recipient_scheme_id")
|
|
return (s_ep, s_sch), (r_ep, r_sch)
|