Initial commit
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
"""Export the current org configuration as an .opcpkg package."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import yaml
|
||||
|
||||
from .package_format import OPCPackage, OPCPackageManifest, PackageAuthor, PackageContents
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from opc.core.config import OPCConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PackageExporter:
|
||||
"""Exports the current organisation as a self-contained .opcpkg directory."""
|
||||
|
||||
def __init__(self, config: OPCConfig, opc_home: Path) -> None:
|
||||
self.config = config
|
||||
self.opc_home = opc_home
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public API
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def export_current(
|
||||
self,
|
||||
package_id: str,
|
||||
name: str,
|
||||
description: str = "",
|
||||
version: str = "1.0.0",
|
||||
author_name: str = "",
|
||||
author_github: str = "",
|
||||
) -> OPCPackage:
|
||||
"""Build an OPCPackage from the live org config."""
|
||||
org = self.config.org
|
||||
|
||||
roles = [r.model_dump() for r in org.roles]
|
||||
employees = [e.model_dump() for e in org.employees]
|
||||
templates_by_id = {
|
||||
str(getattr(template, "id", "") or ""): template
|
||||
for template in list(getattr(org, "talent_templates", []) or [])
|
||||
if str(getattr(template, "id", "") or "")
|
||||
}
|
||||
employee_template_ids = {
|
||||
str(employee.get("template_id", "") or "").strip()
|
||||
for employee in employees
|
||||
if str(employee.get("template_id", "") or "").strip()
|
||||
}
|
||||
try:
|
||||
from opc.layer2_organization.talent_market import TalentMarket
|
||||
|
||||
catalog = {template.id: template for template in TalentMarket(self.opc_home, self.config).list_available_templates()}
|
||||
for template_id in employee_template_ids:
|
||||
if template_id in catalog:
|
||||
templates_by_id.setdefault(template_id, catalog[template_id])
|
||||
except Exception:
|
||||
pass
|
||||
templates = [template.model_dump() for template in templates_by_id.values()]
|
||||
|
||||
# Serialize runtime policy for the current profile
|
||||
wf_policy = None
|
||||
profile = org.company_profile
|
||||
if profile in org.runtime_policies:
|
||||
wf_policy = org.runtime_policies[profile].model_dump()
|
||||
|
||||
# Collect referenced prompt files
|
||||
prompt_contents = self._collect_prompts(roles, templates, employees)
|
||||
|
||||
manifest = OPCPackageManifest(
|
||||
id=package_id,
|
||||
name=name,
|
||||
description=description,
|
||||
version=version,
|
||||
author=PackageAuthor(name=author_name, github=author_github),
|
||||
contents=PackageContents(
|
||||
roles=len(roles),
|
||||
work_item_templates=0,
|
||||
gates=0,
|
||||
prompts=len(prompt_contents),
|
||||
),
|
||||
)
|
||||
|
||||
return OPCPackage(
|
||||
manifest=manifest,
|
||||
roles=roles,
|
||||
runtime_policy=wf_policy,
|
||||
talent_templates=templates,
|
||||
employees=employees,
|
||||
prompt_contents=prompt_contents,
|
||||
readme=self._generate_readme(manifest, roles, None),
|
||||
)
|
||||
|
||||
def write_to_path(self, package: OPCPackage, out_dir: Path) -> Path:
|
||||
"""Write an OPCPackage to disk as a .opcpkg directory."""
|
||||
pkg_dir = out_dir / f"{package.manifest.id}.opcpkg"
|
||||
pkg_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# manifest.yaml
|
||||
with open(pkg_dir / "manifest.yaml", "w", encoding="utf-8") as f:
|
||||
yaml.dump(package.manifest.model_dump(), f, default_flow_style=False, allow_unicode=True)
|
||||
|
||||
# org_config.yaml
|
||||
org_data: dict = {
|
||||
"roles": package.roles,
|
||||
"talent_templates": package.talent_templates,
|
||||
"employees": package.employees,
|
||||
"work_item_templates": package.work_item_templates,
|
||||
}
|
||||
if package.runtime_policy:
|
||||
org_data["runtime_policy"] = package.runtime_policy
|
||||
with open(pkg_dir / "org_config.yaml", "w", encoding="utf-8") as f:
|
||||
yaml.dump(org_data, f, default_flow_style=False, allow_unicode=True)
|
||||
|
||||
# prompts/
|
||||
if package.prompt_contents:
|
||||
prompts_dir = pkg_dir / "prompts"
|
||||
prompts_dir.mkdir(exist_ok=True)
|
||||
for filename, content in package.prompt_contents.items():
|
||||
with open(prompts_dir / filename, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
# README.md
|
||||
with open(pkg_dir / "README.md", "w", encoding="utf-8") as f:
|
||||
f.write(package.readme)
|
||||
|
||||
logger.info("Exported package %s to %s", package.manifest.id, pkg_dir)
|
||||
return pkg_dir
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _collect_prompts(
|
||||
self,
|
||||
roles: list[dict],
|
||||
templates: list[dict],
|
||||
employees: list[dict],
|
||||
) -> dict[str, str]:
|
||||
"""Read all referenced prompt files and return {filename: content}."""
|
||||
refs: set[str] = set()
|
||||
for r in roles:
|
||||
refs.update(r.get("prompt_refs") or [])
|
||||
for t in templates:
|
||||
ref = t.get("prompt_ref", "")
|
||||
if ref:
|
||||
refs.add(ref)
|
||||
for e in employees:
|
||||
refs.update(e.get("prompt_refs") or [])
|
||||
|
||||
contents: dict[str, str] = {}
|
||||
for ref in sorted(refs):
|
||||
path = self.opc_home / ref
|
||||
if path.is_file():
|
||||
try:
|
||||
contents[path.name] = path.read_text(encoding="utf-8")
|
||||
except Exception:
|
||||
logger.debug("Failed to read prompt %s", path)
|
||||
return contents
|
||||
|
||||
def _generate_readme(
|
||||
self,
|
||||
manifest: OPCPackageManifest,
|
||||
roles: list[dict],
|
||||
work_item_templates: list[dict] | None,
|
||||
) -> str:
|
||||
"""Auto-generate a README.md for the package."""
|
||||
lines = [
|
||||
f"# {manifest.name}",
|
||||
"",
|
||||
manifest.description or "An OPC architecture package.",
|
||||
"",
|
||||
f"- **Version**: {manifest.version}",
|
||||
f"- **Category**: {manifest.category}",
|
||||
f"- **Roles**: {manifest.contents.roles}",
|
||||
f"- **Work Item Templates**: {manifest.contents.work_item_templates}",
|
||||
"",
|
||||
"## Roles",
|
||||
"",
|
||||
]
|
||||
for r in roles:
|
||||
lines.append(f"- **{r.get('name', r.get('id', '?'))}** (`{r.get('id', '?')}`): {r.get('responsibility', '')}")
|
||||
if work_item_templates:
|
||||
lines.extend(["", "## Work Item Templates", ""])
|
||||
for item in work_item_templates:
|
||||
lines.append(f"- **{item.get('title', item.get('id', '?'))}** (`{item.get('id', '?')}`)")
|
||||
lines.extend([
|
||||
"",
|
||||
"---",
|
||||
f"*Exported at {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}*",
|
||||
])
|
||||
return "\n".join(lines) + "\n"
|
||||
Reference in New Issue
Block a user