feat: add Python library infrastructure and extension scaffolding
- Add scripts/lib/common.py with core utilities (logging, validation, JSON, file ops, git) - Add scripts/lib/joomla_manifest.py for manifest parsing and validation - Add scripts/run/scaffold_extension.py to create extension scaffolding - Support all Joomla extension types (component, module, plugin, template, package) - Maintain CLI compatibility with existing bash scripts - Foundation for converting remaining scripts to Python Co-authored-by: jmiller-moko <230051081+jmiller-moko@users.noreply.github.com>
This commit is contained in:
BIN
scripts/lib/__pycache__/common.cpython-312.pyc
Normal file
BIN
scripts/lib/__pycache__/common.cpython-312.pyc
Normal file
Binary file not shown.
BIN
scripts/lib/__pycache__/joomla_manifest.cpython-312.pyc
Normal file
BIN
scripts/lib/__pycache__/joomla_manifest.cpython-312.pyc
Normal file
Binary file not shown.
452
scripts/lib/common.py
Executable file
452
scripts/lib/common.py
Executable file
@@ -0,0 +1,452 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Common utilities for Moko-Cassiopeia scripts.
|
||||
|
||||
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||
|
||||
This file is part of a Moko Consulting project.
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program (./LICENSE.md).
|
||||
|
||||
FILE INFORMATION
|
||||
DEFGROUP: Script.Library
|
||||
INGROUP: Common
|
||||
REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
||||
PATH: /scripts/lib/common.py
|
||||
VERSION: 01.00.00
|
||||
BRIEF: Unified shared Python utilities for all CI and local scripts
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
import subprocess
|
||||
import traceback
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Environment and Detection
|
||||
# ============================================================================
|
||||
|
||||
def is_ci() -> bool:
|
||||
"""Check if running in CI environment."""
|
||||
return os.environ.get("CI", "").lower() == "true"
|
||||
|
||||
|
||||
def require_cmd(command: str) -> None:
|
||||
"""
|
||||
Ensure a required command is available.
|
||||
|
||||
Args:
|
||||
command: Command name to check
|
||||
|
||||
Raises:
|
||||
SystemExit: If command is not found
|
||||
"""
|
||||
if not shutil.which(command):
|
||||
log_error(f"Required command not found: {command}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Logging
|
||||
# ============================================================================
|
||||
|
||||
class Colors:
|
||||
"""ANSI color codes for terminal output."""
|
||||
RED = '\033[0;31m'
|
||||
GREEN = '\033[0;32m'
|
||||
YELLOW = '\033[1;33m'
|
||||
BLUE = '\033[0;34m'
|
||||
CYAN = '\033[0;36m'
|
||||
BOLD = '\033[1m'
|
||||
NC = '\033[0m' # No Color
|
||||
|
||||
@classmethod
|
||||
def enabled(cls) -> bool:
|
||||
"""Check if colors should be enabled."""
|
||||
return sys.stdout.isatty() and os.environ.get("NO_COLOR") is None
|
||||
|
||||
|
||||
def log_info(message: str) -> None:
|
||||
"""Log informational message."""
|
||||
print(f"INFO: {message}")
|
||||
|
||||
|
||||
def log_warn(message: str) -> None:
|
||||
"""Log warning message."""
|
||||
color = Colors.YELLOW if Colors.enabled() else ""
|
||||
nc = Colors.NC if Colors.enabled() else ""
|
||||
print(f"{color}WARN: {message}{nc}", file=sys.stderr)
|
||||
|
||||
|
||||
def log_error(message: str) -> None:
|
||||
"""Log error message."""
|
||||
color = Colors.RED if Colors.enabled() else ""
|
||||
nc = Colors.NC if Colors.enabled() else ""
|
||||
print(f"{color}ERROR: {message}{nc}", file=sys.stderr)
|
||||
|
||||
|
||||
def log_success(message: str) -> None:
|
||||
"""Log success message."""
|
||||
color = Colors.GREEN if Colors.enabled() else ""
|
||||
nc = Colors.NC if Colors.enabled() else ""
|
||||
print(f"{color}✓ {message}{nc}")
|
||||
|
||||
|
||||
def log_step(message: str) -> None:
|
||||
"""Log a step in a process."""
|
||||
color = Colors.CYAN if Colors.enabled() else ""
|
||||
nc = Colors.NC if Colors.enabled() else ""
|
||||
print(f"{color}➜ {message}{nc}")
|
||||
|
||||
|
||||
def log_section(title: str) -> None:
|
||||
"""Log a section header."""
|
||||
print()
|
||||
print("=" * 60)
|
||||
print(title)
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
def log_kv(key: str, value: str) -> None:
|
||||
"""Log a key-value pair."""
|
||||
print(f" {key}: {value}")
|
||||
|
||||
|
||||
def die(message: str, exit_code: int = 1) -> None:
|
||||
"""
|
||||
Log error and exit.
|
||||
|
||||
Args:
|
||||
message: Error message
|
||||
exit_code: Exit code (default: 1)
|
||||
"""
|
||||
log_error(message)
|
||||
|
||||
if os.environ.get("VERBOSE_ERRORS", "true").lower() == "true":
|
||||
print("", file=sys.stderr)
|
||||
print("Stack trace:", file=sys.stderr)
|
||||
traceback.print_stack(file=sys.stderr)
|
||||
print("", file=sys.stderr)
|
||||
print("Environment:", file=sys.stderr)
|
||||
print(f" PWD: {os.getcwd()}", file=sys.stderr)
|
||||
print(f" USER: {os.environ.get('USER', 'unknown')}", file=sys.stderr)
|
||||
print(f" PYTHON: {sys.version}", file=sys.stderr)
|
||||
print(f" CI: {is_ci()}", file=sys.stderr)
|
||||
print("", file=sys.stderr)
|
||||
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Validation Helpers
|
||||
# ============================================================================
|
||||
|
||||
def assert_file_exists(path: Union[str, Path]) -> None:
|
||||
"""
|
||||
Assert that a file exists.
|
||||
|
||||
Args:
|
||||
path: Path to file
|
||||
|
||||
Raises:
|
||||
SystemExit: If file doesn't exist
|
||||
"""
|
||||
if not Path(path).is_file():
|
||||
die(f"Required file missing: {path}")
|
||||
|
||||
|
||||
def assert_dir_exists(path: Union[str, Path]) -> None:
|
||||
"""
|
||||
Assert that a directory exists.
|
||||
|
||||
Args:
|
||||
path: Path to directory
|
||||
|
||||
Raises:
|
||||
SystemExit: If directory doesn't exist
|
||||
"""
|
||||
if not Path(path).is_dir():
|
||||
die(f"Required directory missing: {path}")
|
||||
|
||||
|
||||
def assert_not_empty(value: Any, name: str) -> None:
|
||||
"""
|
||||
Assert that a value is not empty.
|
||||
|
||||
Args:
|
||||
value: Value to check
|
||||
name: Name of the value for error message
|
||||
|
||||
Raises:
|
||||
SystemExit: If value is empty
|
||||
"""
|
||||
if not value:
|
||||
die(f"Required value is empty: {name}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# JSON Utilities
|
||||
# ============================================================================
|
||||
|
||||
def json_escape(text: str) -> str:
|
||||
"""
|
||||
Escape text for JSON.
|
||||
|
||||
Args:
|
||||
text: Text to escape
|
||||
|
||||
Returns:
|
||||
Escaped text
|
||||
"""
|
||||
return json.dumps(text)[1:-1] # Remove surrounding quotes
|
||||
|
||||
|
||||
def json_output(data: Dict[str, Any], pretty: bool = False) -> None:
|
||||
"""
|
||||
Output data as JSON.
|
||||
|
||||
Args:
|
||||
data: Dictionary to output
|
||||
pretty: Whether to pretty-print
|
||||
"""
|
||||
if pretty:
|
||||
print(json.dumps(data, indent=2, sort_keys=True))
|
||||
else:
|
||||
print(json.dumps(data, separators=(',', ':')))
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Path Utilities
|
||||
# ============================================================================
|
||||
|
||||
def script_root() -> Path:
|
||||
"""
|
||||
Get the root scripts directory.
|
||||
|
||||
Returns:
|
||||
Path to scripts directory
|
||||
"""
|
||||
return Path(__file__).parent.parent
|
||||
|
||||
|
||||
def repo_root() -> Path:
|
||||
"""
|
||||
Get the repository root directory.
|
||||
|
||||
Returns:
|
||||
Path to repository root
|
||||
"""
|
||||
return script_root().parent
|
||||
|
||||
|
||||
def normalize_path(path: Union[str, Path]) -> str:
|
||||
"""
|
||||
Normalize a path (resolve, absolute, forward slashes).
|
||||
|
||||
Args:
|
||||
path: Path to normalize
|
||||
|
||||
Returns:
|
||||
Normalized path string
|
||||
"""
|
||||
return str(Path(path).resolve()).replace("\\", "/")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# File Operations
|
||||
# ============================================================================
|
||||
|
||||
def read_file(path: Union[str, Path], encoding: str = "utf-8") -> str:
|
||||
"""
|
||||
Read a file.
|
||||
|
||||
Args:
|
||||
path: Path to file
|
||||
encoding: File encoding
|
||||
|
||||
Returns:
|
||||
File contents
|
||||
"""
|
||||
assert_file_exists(path)
|
||||
return Path(path).read_text(encoding=encoding)
|
||||
|
||||
|
||||
def write_file(path: Union[str, Path], content: str, encoding: str = "utf-8") -> None:
|
||||
"""
|
||||
Write a file.
|
||||
|
||||
Args:
|
||||
path: Path to file
|
||||
content: Content to write
|
||||
encoding: File encoding
|
||||
"""
|
||||
Path(path).write_text(content, encoding=encoding)
|
||||
|
||||
|
||||
def ensure_dir(path: Union[str, Path]) -> None:
|
||||
"""
|
||||
Ensure a directory exists.
|
||||
|
||||
Args:
|
||||
path: Path to directory
|
||||
"""
|
||||
Path(path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Command Execution
|
||||
# ============================================================================
|
||||
|
||||
def run_command(
|
||||
cmd: List[str],
|
||||
capture_output: bool = True,
|
||||
check: bool = True,
|
||||
cwd: Optional[Union[str, Path]] = None,
|
||||
env: Optional[Dict[str, str]] = None
|
||||
) -> subprocess.CompletedProcess:
|
||||
"""
|
||||
Run a command.
|
||||
|
||||
Args:
|
||||
cmd: Command and arguments
|
||||
capture_output: Whether to capture stdout/stderr
|
||||
check: Whether to raise on non-zero exit
|
||||
cwd: Working directory
|
||||
env: Environment variables
|
||||
|
||||
Returns:
|
||||
CompletedProcess instance
|
||||
"""
|
||||
return subprocess.run(
|
||||
cmd,
|
||||
capture_output=capture_output,
|
||||
text=True,
|
||||
check=check,
|
||||
cwd=cwd,
|
||||
env=env
|
||||
)
|
||||
|
||||
|
||||
def run_shell(
|
||||
script: str,
|
||||
capture_output: bool = True,
|
||||
check: bool = True,
|
||||
cwd: Optional[Union[str, Path]] = None
|
||||
) -> subprocess.CompletedProcess:
|
||||
"""
|
||||
Run a shell script.
|
||||
|
||||
Args:
|
||||
script: Shell script
|
||||
capture_output: Whether to capture stdout/stderr
|
||||
check: Whether to raise on non-zero exit
|
||||
cwd: Working directory
|
||||
|
||||
Returns:
|
||||
CompletedProcess instance
|
||||
"""
|
||||
return subprocess.run(
|
||||
script,
|
||||
shell=True,
|
||||
capture_output=capture_output,
|
||||
text=True,
|
||||
check=check,
|
||||
cwd=cwd
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Git Utilities
|
||||
# ============================================================================
|
||||
|
||||
def git_root() -> Path:
|
||||
"""
|
||||
Get git repository root.
|
||||
|
||||
Returns:
|
||||
Path to git root
|
||||
"""
|
||||
result = run_command(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
check=True
|
||||
)
|
||||
return Path(result.stdout.strip())
|
||||
|
||||
|
||||
def git_status(porcelain: bool = True) -> str:
|
||||
"""
|
||||
Get git status.
|
||||
|
||||
Args:
|
||||
porcelain: Use porcelain format
|
||||
|
||||
Returns:
|
||||
Git status output
|
||||
"""
|
||||
cmd = ["git", "status"]
|
||||
if porcelain:
|
||||
cmd.append("--porcelain")
|
||||
|
||||
result = run_command(cmd, capture_output=True, check=True)
|
||||
return result.stdout
|
||||
|
||||
|
||||
def git_branch() -> str:
|
||||
"""
|
||||
Get current git branch.
|
||||
|
||||
Returns:
|
||||
Branch name
|
||||
"""
|
||||
result = run_command(
|
||||
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
capture_output=True,
|
||||
check=True
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Main Entry Point (for testing)
|
||||
# ============================================================================
|
||||
|
||||
def main() -> None:
|
||||
"""Test the common utilities."""
|
||||
log_section("Testing Common Utilities")
|
||||
|
||||
log_info("This is an info message")
|
||||
log_warn("This is a warning message")
|
||||
log_success("This is a success message")
|
||||
log_step("This is a step message")
|
||||
|
||||
log_section("Environment")
|
||||
log_kv("CI", str(is_ci()))
|
||||
log_kv("Script Root", str(script_root()))
|
||||
log_kv("Repo Root", str(repo_root()))
|
||||
log_kv("Git Root", str(git_root()))
|
||||
log_kv("Git Branch", git_branch())
|
||||
|
||||
log_section("Tests Passed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
430
scripts/lib/joomla_manifest.py
Executable file
430
scripts/lib/joomla_manifest.py
Executable file
@@ -0,0 +1,430 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Joomla manifest parsing and validation utilities.
|
||||
|
||||
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||
|
||||
This file is part of a Moko Consulting project.
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program (./LICENSE.md).
|
||||
|
||||
FILE INFORMATION
|
||||
DEFGROUP: Script.Library
|
||||
INGROUP: Joomla.Manifest
|
||||
REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia
|
||||
PATH: /scripts/lib/joomla_manifest.py
|
||||
VERSION: 01.00.00
|
||||
BRIEF: Joomla manifest parsing and validation utilities
|
||||
"""
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from dataclasses import dataclass
|
||||
|
||||
try:
|
||||
from . import common
|
||||
except ImportError:
|
||||
import common
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Joomla Extension Types
|
||||
# ============================================================================
|
||||
|
||||
class ExtensionType:
|
||||
"""Joomla extension types."""
|
||||
COMPONENT = "component"
|
||||
MODULE = "module"
|
||||
PLUGIN = "plugin"
|
||||
TEMPLATE = "template"
|
||||
LIBRARY = "library"
|
||||
PACKAGE = "package"
|
||||
FILE = "file"
|
||||
LANGUAGE = "language"
|
||||
|
||||
ALL_TYPES = [
|
||||
COMPONENT, MODULE, PLUGIN, TEMPLATE,
|
||||
LIBRARY, PACKAGE, FILE, LANGUAGE
|
||||
]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Manifest Data Class
|
||||
# ============================================================================
|
||||
|
||||
@dataclass
|
||||
class ManifestInfo:
|
||||
"""Information extracted from a Joomla manifest."""
|
||||
path: Path
|
||||
extension_type: str
|
||||
name: str
|
||||
version: str
|
||||
description: Optional[str] = None
|
||||
author: Optional[str] = None
|
||||
author_email: Optional[str] = None
|
||||
author_url: Optional[str] = None
|
||||
copyright: Optional[str] = None
|
||||
license: Optional[str] = None
|
||||
creation_date: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"path": str(self.path),
|
||||
"ext_type": self.extension_type,
|
||||
"name": self.name,
|
||||
"version": self.version,
|
||||
"description": self.description or "",
|
||||
"author": self.author or "",
|
||||
"author_email": self.author_email or "",
|
||||
"author_url": self.author_url or "",
|
||||
"copyright": self.copyright or "",
|
||||
"license": self.license or "",
|
||||
"creation_date": self.creation_date or ""
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Manifest Discovery
|
||||
# ============================================================================
|
||||
|
||||
def find_manifest(src_dir: str = "src") -> Path:
|
||||
"""
|
||||
Find the primary Joomla manifest in the given directory.
|
||||
|
||||
Args:
|
||||
src_dir: Source directory to search
|
||||
|
||||
Returns:
|
||||
Path to manifest file
|
||||
|
||||
Raises:
|
||||
SystemExit: If no manifest found
|
||||
"""
|
||||
src_path = Path(src_dir)
|
||||
|
||||
if not src_path.is_dir():
|
||||
common.die(f"Source directory missing: {src_dir}")
|
||||
|
||||
# Template manifest (templateDetails.xml)
|
||||
template_manifest = src_path / "templateDetails.xml"
|
||||
if template_manifest.is_file():
|
||||
return template_manifest
|
||||
|
||||
# Also check in templates subdirectory
|
||||
templates_dir = src_path / "templates"
|
||||
if templates_dir.is_dir():
|
||||
for template_file in templates_dir.glob("templateDetails.xml"):
|
||||
return template_file
|
||||
|
||||
# Package manifest (pkg_*.xml)
|
||||
pkg_manifests = list(src_path.rglob("pkg_*.xml"))
|
||||
if pkg_manifests:
|
||||
return pkg_manifests[0]
|
||||
|
||||
# Component manifest (com_*.xml)
|
||||
com_manifests = list(src_path.rglob("com_*.xml"))
|
||||
if com_manifests:
|
||||
return com_manifests[0]
|
||||
|
||||
# Module manifest (mod_*.xml)
|
||||
mod_manifests = list(src_path.rglob("mod_*.xml"))
|
||||
if mod_manifests:
|
||||
return mod_manifests[0]
|
||||
|
||||
# Plugin manifest (plg_*.xml)
|
||||
plg_manifests = list(src_path.rglob("plg_*.xml"))
|
||||
if plg_manifests:
|
||||
return plg_manifests[0]
|
||||
|
||||
# Fallback: any XML with <extension
|
||||
for xml_file in src_path.rglob("*.xml"):
|
||||
try:
|
||||
content = xml_file.read_text(encoding="utf-8")
|
||||
if "<extension" in content:
|
||||
return xml_file
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
common.die(f"No Joomla manifest XML found under {src_dir}")
|
||||
|
||||
|
||||
def find_all_manifests(src_dir: str = "src") -> List[Path]:
|
||||
"""
|
||||
Find all Joomla manifests in the given directory.
|
||||
|
||||
Args:
|
||||
src_dir: Source directory to search
|
||||
|
||||
Returns:
|
||||
List of manifest paths
|
||||
"""
|
||||
src_path = Path(src_dir)
|
||||
|
||||
if not src_path.is_dir():
|
||||
return []
|
||||
|
||||
manifests = []
|
||||
|
||||
# Template manifests
|
||||
manifests.extend(src_path.rglob("templateDetails.xml"))
|
||||
|
||||
# Package manifests
|
||||
manifests.extend(src_path.rglob("pkg_*.xml"))
|
||||
|
||||
# Component manifests
|
||||
manifests.extend(src_path.rglob("com_*.xml"))
|
||||
|
||||
# Module manifests
|
||||
manifests.extend(src_path.rglob("mod_*.xml"))
|
||||
|
||||
# Plugin manifests
|
||||
manifests.extend(src_path.rglob("plg_*.xml"))
|
||||
|
||||
return manifests
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Manifest Parsing
|
||||
# ============================================================================
|
||||
|
||||
def parse_manifest(manifest_path: Path) -> ManifestInfo:
|
||||
"""
|
||||
Parse a Joomla manifest file.
|
||||
|
||||
Args:
|
||||
manifest_path: Path to manifest file
|
||||
|
||||
Returns:
|
||||
ManifestInfo object
|
||||
|
||||
Raises:
|
||||
SystemExit: If parsing fails
|
||||
"""
|
||||
if not manifest_path.is_file():
|
||||
common.die(f"Manifest not found: {manifest_path}")
|
||||
|
||||
try:
|
||||
tree = ET.parse(manifest_path)
|
||||
root = tree.getroot()
|
||||
|
||||
# Extract extension type
|
||||
ext_type = root.attrib.get("type", "").strip().lower()
|
||||
if not ext_type:
|
||||
common.die(f"Manifest missing type attribute: {manifest_path}")
|
||||
|
||||
# Extract name
|
||||
name_elem = root.find("name")
|
||||
if name_elem is None or not name_elem.text:
|
||||
common.die(f"Manifest missing name element: {manifest_path}")
|
||||
name = name_elem.text.strip()
|
||||
|
||||
# Extract version
|
||||
version_elem = root.find("version")
|
||||
if version_elem is None or not version_elem.text:
|
||||
common.die(f"Manifest missing version element: {manifest_path}")
|
||||
version = version_elem.text.strip()
|
||||
|
||||
# Extract optional fields
|
||||
description = None
|
||||
desc_elem = root.find("description")
|
||||
if desc_elem is not None and desc_elem.text:
|
||||
description = desc_elem.text.strip()
|
||||
|
||||
author = None
|
||||
author_elem = root.find("author")
|
||||
if author_elem is not None and author_elem.text:
|
||||
author = author_elem.text.strip()
|
||||
|
||||
author_email = None
|
||||
email_elem = root.find("authorEmail")
|
||||
if email_elem is not None and email_elem.text:
|
||||
author_email = email_elem.text.strip()
|
||||
|
||||
author_url = None
|
||||
url_elem = root.find("authorUrl")
|
||||
if url_elem is not None and url_elem.text:
|
||||
author_url = url_elem.text.strip()
|
||||
|
||||
copyright_text = None
|
||||
copyright_elem = root.find("copyright")
|
||||
if copyright_elem is not None and copyright_elem.text:
|
||||
copyright_text = copyright_elem.text.strip()
|
||||
|
||||
license_text = None
|
||||
license_elem = root.find("license")
|
||||
if license_elem is not None and license_elem.text:
|
||||
license_text = license_elem.text.strip()
|
||||
|
||||
creation_date = None
|
||||
date_elem = root.find("creationDate")
|
||||
if date_elem is not None and date_elem.text:
|
||||
creation_date = date_elem.text.strip()
|
||||
|
||||
return ManifestInfo(
|
||||
path=manifest_path,
|
||||
extension_type=ext_type,
|
||||
name=name,
|
||||
version=version,
|
||||
description=description,
|
||||
author=author,
|
||||
author_email=author_email,
|
||||
author_url=author_url,
|
||||
copyright=copyright_text,
|
||||
license=license_text,
|
||||
creation_date=creation_date
|
||||
)
|
||||
|
||||
except ET.ParseError as e:
|
||||
common.die(f"Failed to parse manifest {manifest_path}: {e}")
|
||||
except Exception as e:
|
||||
common.die(f"Error reading manifest {manifest_path}: {e}")
|
||||
|
||||
|
||||
def get_manifest_version(manifest_path: Path) -> str:
|
||||
"""
|
||||
Extract version from manifest.
|
||||
|
||||
Args:
|
||||
manifest_path: Path to manifest file
|
||||
|
||||
Returns:
|
||||
Version string
|
||||
"""
|
||||
info = parse_manifest(manifest_path)
|
||||
return info.version
|
||||
|
||||
|
||||
def get_manifest_name(manifest_path: Path) -> str:
|
||||
"""
|
||||
Extract name from manifest.
|
||||
|
||||
Args:
|
||||
manifest_path: Path to manifest file
|
||||
|
||||
Returns:
|
||||
Name string
|
||||
"""
|
||||
info = parse_manifest(manifest_path)
|
||||
return info.name
|
||||
|
||||
|
||||
def get_manifest_type(manifest_path: Path) -> str:
|
||||
"""
|
||||
Extract extension type from manifest.
|
||||
|
||||
Args:
|
||||
manifest_path: Path to manifest file
|
||||
|
||||
Returns:
|
||||
Extension type string
|
||||
"""
|
||||
info = parse_manifest(manifest_path)
|
||||
return info.extension_type
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Manifest Validation
|
||||
# ============================================================================
|
||||
|
||||
def validate_manifest(manifest_path: Path) -> Tuple[bool, List[str]]:
|
||||
"""
|
||||
Validate a Joomla manifest.
|
||||
|
||||
Args:
|
||||
manifest_path: Path to manifest file
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, list_of_warnings)
|
||||
"""
|
||||
warnings = []
|
||||
|
||||
try:
|
||||
info = parse_manifest(manifest_path)
|
||||
|
||||
# Check for recommended fields
|
||||
if not info.description:
|
||||
warnings.append("Missing description element")
|
||||
|
||||
if not info.author:
|
||||
warnings.append("Missing author element")
|
||||
|
||||
if not info.copyright:
|
||||
warnings.append("Missing copyright element")
|
||||
|
||||
if not info.license:
|
||||
warnings.append("Missing license element")
|
||||
|
||||
if not info.creation_date:
|
||||
warnings.append("Missing creationDate element")
|
||||
|
||||
# Validate extension type
|
||||
if info.extension_type not in ExtensionType.ALL_TYPES:
|
||||
warnings.append(f"Unknown extension type: {info.extension_type}")
|
||||
|
||||
return (True, warnings)
|
||||
|
||||
except SystemExit:
|
||||
return (False, ["Failed to parse manifest"])
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Main Entry Point (for testing)
|
||||
# ============================================================================
|
||||
|
||||
def main() -> None:
|
||||
"""Test the manifest utilities."""
|
||||
import sys
|
||||
|
||||
common.log_section("Testing Joomla Manifest Utilities")
|
||||
|
||||
src_dir = sys.argv[1] if len(sys.argv) > 1 else "src"
|
||||
|
||||
try:
|
||||
manifest = find_manifest(src_dir)
|
||||
common.log_success(f"Found manifest: {manifest}")
|
||||
|
||||
info = parse_manifest(manifest)
|
||||
|
||||
common.log_section("Manifest Information")
|
||||
common.log_kv("Type", info.extension_type)
|
||||
common.log_kv("Name", info.name)
|
||||
common.log_kv("Version", info.version)
|
||||
|
||||
if info.description:
|
||||
common.log_kv("Description", info.description[:60] + "..." if len(info.description) > 60 else info.description)
|
||||
|
||||
if info.author:
|
||||
common.log_kv("Author", info.author)
|
||||
|
||||
is_valid, warnings = validate_manifest(manifest)
|
||||
|
||||
if is_valid:
|
||||
common.log_success("Manifest is valid")
|
||||
if warnings:
|
||||
common.log_warn(f"Warnings: {len(warnings)}")
|
||||
for warning in warnings:
|
||||
print(f" - {warning}")
|
||||
else:
|
||||
common.log_error("Manifest validation failed")
|
||||
|
||||
except SystemExit as e:
|
||||
sys.exit(e.code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user