faas-gitops 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. faas_gitops-0.2.1/MANIFEST.in +1 -0
  2. faas_gitops-0.2.1/PKG-INFO +12 -0
  3. faas_gitops-0.2.1/pyproject.toml +30 -0
  4. faas_gitops-0.2.1/setup.cfg +4 -0
  5. faas_gitops-0.2.1/src/faas_gitops/__init__.py +0 -0
  6. faas_gitops-0.2.1/src/faas_gitops/adapters/__init__.py +0 -0
  7. faas_gitops-0.2.1/src/faas_gitops/adapters/bizy_function_provider.py +31 -0
  8. faas_gitops-0.2.1/src/faas_gitops/adapters/bizy_template_provider.py +31 -0
  9. faas_gitops-0.2.1/src/faas_gitops/adapters/git_cli_gateway.py +107 -0
  10. faas_gitops-0.2.1/src/faas_gitops/cli/__init__.py +0 -0
  11. faas_gitops-0.2.1/src/faas_gitops/cli/app.py +112 -0
  12. faas_gitops-0.2.1/src/faas_gitops/cli/parser.py +41 -0
  13. faas_gitops-0.2.1/src/faas_gitops/cli/presenters.py +30 -0
  14. faas_gitops-0.2.1/src/faas_gitops/config.py +30 -0
  15. faas_gitops-0.2.1/src/faas_gitops/filtering.py +9 -0
  16. faas_gitops-0.2.1/src/faas_gitops/infra/__init__.py +0 -0
  17. faas_gitops-0.2.1/src/faas_gitops/infra/yaml_utils.py +17 -0
  18. faas_gitops-0.2.1/src/faas_gitops/layout.py +12 -0
  19. faas_gitops-0.2.1/src/faas_gitops/models.py +87 -0
  20. faas_gitops-0.2.1/src/faas_gitops/protocols/__init__.py +9 -0
  21. faas_gitops-0.2.1/src/faas_gitops/protocols/faas_function.py +53 -0
  22. faas_gitops-0.2.1/src/faas_gitops/protocols/faas_template.py +70 -0
  23. faas_gitops-0.2.1/src/faas_gitops/protocols/git_gateway.py +41 -0
  24. faas_gitops-0.2.1/src/faas_gitops/usecases/__init__.py +0 -0
  25. faas_gitops-0.2.1/src/faas_gitops/usecases/apply_function_changes.py +57 -0
  26. faas_gitops-0.2.1/src/faas_gitops/usecases/apply_template_changes.py +49 -0
  27. faas_gitops-0.2.1/src/faas_gitops/usecases/detect_drift.py +76 -0
  28. faas_gitops-0.2.1/src/faas_gitops/usecases/detect_function_drift.py +45 -0
  29. faas_gitops-0.2.1/src/faas_gitops/usecases/detect_template_drift.py +14 -0
  30. faas_gitops-0.2.1/src/faas_gitops/usecases/render_definitions.py +96 -0
  31. faas_gitops-0.2.1/src/faas_gitops/usecases/sync_functions.py +102 -0
  32. faas_gitops-0.2.1/src/faas_gitops/usecases/sync_templates.py +106 -0
  33. faas_gitops-0.2.1/src/faas_gitops.egg-info/PKG-INFO +12 -0
  34. faas_gitops-0.2.1/src/faas_gitops.egg-info/SOURCES.txt +44 -0
  35. faas_gitops-0.2.1/src/faas_gitops.egg-info/dependency_links.txt +1 -0
  36. faas_gitops-0.2.1/src/faas_gitops.egg-info/entry_points.txt +2 -0
  37. faas_gitops-0.2.1/src/faas_gitops.egg-info/requires.txt +8 -0
  38. faas_gitops-0.2.1/src/faas_gitops.egg-info/top_level.txt +1 -0
  39. faas_gitops-0.2.1/tests/test_apply_function_changes.py +171 -0
  40. faas_gitops-0.2.1/tests/test_apply_template_changes.py +158 -0
  41. faas_gitops-0.2.1/tests/test_detect_drift.py +154 -0
  42. faas_gitops-0.2.1/tests/test_detect_function_drift.py +53 -0
  43. faas_gitops-0.2.1/tests/test_git_cli_gateway.py +69 -0
  44. faas_gitops-0.2.1/tests/test_render_definitions.py +115 -0
  45. faas_gitops-0.2.1/tests/test_sync_functions.py +131 -0
  46. faas_gitops-0.2.1/tests/test_sync_templates.py +109 -0
@@ -0,0 +1 @@
1
+ exclude README.md
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: faas-gitops
3
+ Version: 0.2.1
4
+ Summary: Declarative GitOps for FaaS
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: aiofiles>=23.0
7
+ Requires-Dist: bizy-deploy>=0.5.4
8
+ Requires-Dist: pyyaml>=6.0
9
+ Requires-Dist: tqdm>=4.66.0
10
+ Provides-Extra: test
11
+ Requires-Dist: pytest>=7.0; extra == "test"
12
+ Requires-Dist: pytest-asyncio>=0.23; extra == "test"
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "faas-gitops"
7
+ version = "0.2.1"
8
+ description = "Declarative GitOps for FaaS"
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "aiofiles>=23.0",
12
+ "bizy-deploy>=0.5.4",
13
+ "pyyaml>=6.0",
14
+ "tqdm>=4.66.0",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ test = [
19
+ "pytest>=7.0",
20
+ "pytest-asyncio>=0.23",
21
+ ]
22
+
23
+ [project.scripts]
24
+ faas-gitops = "faas_gitops.cli.app:main"
25
+
26
+ [tool.pytest.ini_options]
27
+ asyncio_mode = "auto"
28
+
29
+ [tool.setuptools.packages.find]
30
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
File without changes
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+ from bizy_deploy import AsyncFaaSActionProvider
3
+
4
+
5
+ class BizyFunctionProvider:
6
+ """FaaS function operations via bizy-deploy SDK.
7
+
8
+ Implements FaaSFunctionProvider protocol.
9
+ Delegates to AsyncFaaSActionProvider from bizy_deploy.
10
+ """
11
+
12
+ def __init__(self, provider: AsyncFaaSActionProvider) -> None:
13
+ self._p = provider
14
+
15
+ async def list_functions(self) -> list[dict]:
16
+ result = await self._p.list_functions()
17
+ return [i.to_dict() for i in result.data.data if i is not None]
18
+
19
+ async def get_latest_definition(self, func_id: str) -> dict | None:
20
+ result = await self._p.get_latest_function_definition(function_id=func_id)
21
+ return result.to_dict() if result else None
22
+
23
+ async def push_definition(self, func_id: str, data: dict, comment: str = "") -> str:
24
+ return await self._p.create_function_definition(merged_def=data, function_id=func_id)
25
+
26
+ async def update_function_meta(self, func_id: str, meta: dict) -> None:
27
+ await self._p.update_function(
28
+ function_id=func_id,
29
+ name=meta.get("functionName"),
30
+ tags=meta.get("tags"),
31
+ )
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from bizy_deploy import AsyncFaaSActionProvider
6
+
7
+
8
+ class BizyTemplateProvider:
9
+
10
+ def __init__(self, provider: AsyncFaaSActionProvider) -> None:
11
+ self._p = provider
12
+
13
+ async def list_templates(self) -> list[dict]:
14
+ priv = await self._p.list_private_templates()
15
+ pub = await self._p.list_published_templates()
16
+ items = [i.to_dict() for i in (priv.data.data + pub.data.data) if i is not None]
17
+ return items
18
+
19
+ async def get_template(self, org_id: str, template_id: str, dest: Path) -> None:
20
+ await self._p.get_template(org_id=org_id, template_id=template_id, output=str(dest))
21
+
22
+ async def get_template_content(self, org_id: str, template_name: str) -> str:
23
+ path = await self._p.get_template(org_id=org_id, template_id=template_name)
24
+ return Path(path).read_text()
25
+
26
+ async def update_template(self, org_id: str, template_id: str, yaml_content: str) -> dict:
27
+ files = {"file": ("template.yaml", yaml_content, "application/x-yaml")}
28
+ return await self._p.update_template(name=template_id, files=files)
29
+
30
+ async def render_template_draft(self, *, file: str, use_cache: bool = False, output: str | None = None) -> dict:
31
+ return await self._p.render_template_draft_v2(file=file, use_cache=use_cache, output=output)
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from pathlib import Path
5
+ from loguru import logger
6
+ from faas_gitops.protocols.git_gateway import PushResult
7
+
8
+
9
+ async def _git(*args: str, cwd: Path | None = None, check: bool = True) -> str:
10
+ cmd = ["git", *args]
11
+ proc = await asyncio.create_subprocess_exec(
12
+ *cmd, cwd=cwd,
13
+ stdout=asyncio.subprocess.PIPE,
14
+ stderr=asyncio.subprocess.PIPE,
15
+ )
16
+ stdout, stderr = await proc.communicate()
17
+ if check and proc.returncode != 0:
18
+ raise RuntimeError(f"git {' '.join(args)} failed: {stderr.decode()}")
19
+ return stdout.decode().strip()
20
+
21
+
22
+ async def _gh(*args: str, cwd: Path | None = None, check: bool = True) -> str:
23
+ cmd = ["gh", *args]
24
+ proc = await asyncio.create_subprocess_exec(
25
+ *cmd, cwd=cwd,
26
+ stdout=asyncio.subprocess.PIPE,
27
+ stderr=asyncio.subprocess.PIPE,
28
+ )
29
+ stdout, stderr = await proc.communicate()
30
+ if check and proc.returncode != 0:
31
+ raise RuntimeError(f"gh {' '.join(args)} failed: {stderr.decode()}")
32
+ return stdout.decode().strip()
33
+
34
+
35
+ class GitCliGateway:
36
+
37
+ def __init__(self, clone_root: Path) -> None:
38
+ self._root = clone_root
39
+ self._root.mkdir(parents=True, exist_ok=True)
40
+ logger.info(f"GitCliGateway init: clone_root={self._root}")
41
+
42
+ async def clone_repo(self, repo_slug: str) -> Path:
43
+ dest = self._root / repo_slug.split("/")[-1]
44
+ if dest.exists():
45
+ logger.info(f"pull existing repo: {dest.name}")
46
+ await _git("pull", cwd=dest, check=False)
47
+ else:
48
+ logger.info(f"gh repo clone {repo_slug}")
49
+ await _gh("repo", "clone", repo_slug, str(dest))
50
+ return dest
51
+
52
+ async def prepare_branch(self, repo_path: Path, branch: str, force: bool = False) -> None:
53
+ await _git("fetch", "origin", cwd=repo_path)
54
+ remote_exists = await _git("rev-parse", "--verify", f"origin/{branch}", cwd=repo_path, check=False)
55
+ if force or not remote_exists:
56
+ await _git("checkout", "main", cwd=repo_path)
57
+ await _git("pull", cwd=repo_path, check=False)
58
+ if force:
59
+ await _git("branch", "-D", branch, cwd=repo_path, check=False)
60
+ await _git("checkout", "-B", branch, cwd=repo_path)
61
+ else:
62
+ await _git("checkout", branch, cwd=repo_path, check=False) or await _git("checkout", "-B", branch, cwd=repo_path)
63
+ await _git("reset", "--hard", f"origin/{branch}", cwd=repo_path)
64
+
65
+ async def current_branch(self, repo_path: Path) -> str:
66
+ return await _git("rev-parse", "--abbrev-ref", "HEAD", cwd=repo_path)
67
+
68
+ async def checkout(self, repo_path: Path, ref: str) -> None:
69
+ await _git("checkout", ref, cwd=repo_path)
70
+
71
+ async def commit_and_push(
72
+ self,
73
+ repo_path: Path,
74
+ prefix: str,
75
+ msg: str,
76
+ branch: str,
77
+ *,
78
+ force: bool = False,
79
+ create_pr: bool = False,
80
+ ) -> PushResult:
81
+ await _git("add", prefix, cwd=repo_path)
82
+ proc = await asyncio.create_subprocess_exec(
83
+ "git", "diff", "--cached", "--quiet",
84
+ cwd=repo_path,
85
+ stdout=asyncio.subprocess.PIPE,
86
+ stderr=asyncio.subprocess.PIPE,
87
+ )
88
+ await proc.communicate()
89
+ if proc.returncode == 0:
90
+ return PushResult(pushed=False)
91
+ await _git("commit", "-m", msg, cwd=repo_path)
92
+ if not force:
93
+ await _git("pull", "--rebase", "origin", branch, cwd=repo_path, check=False)
94
+ await _git("push", "origin", branch, *(["--force"] if force else []), cwd=repo_path)
95
+ if create_pr:
96
+ proc = await asyncio.create_subprocess_exec(
97
+ "gh", "pr", "create", "--fill", "--base", "main", "--head", branch,
98
+ cwd=repo_path,
99
+ stdout=asyncio.subprocess.PIPE,
100
+ stderr=asyncio.subprocess.PIPE,
101
+ )
102
+ stdout, stderr = await proc.communicate()
103
+ out = stdout.decode().strip()
104
+ err = stderr.decode().strip()
105
+ url = out or next((l for l in err.splitlines() if l.startswith("http")), None)
106
+ return PushResult(pushed=True, url=url)
107
+ return PushResult(pushed=True)
File without changes
@@ -0,0 +1,112 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import tempfile
5
+ from contextlib import AsyncExitStack
6
+ from pathlib import Path
7
+
8
+ from bizy_deploy import AsyncFaaSActionProvider
9
+ from loguru import logger
10
+
11
+ from faas_gitops.adapters.bizy_function_provider import BizyFunctionProvider
12
+ from faas_gitops.adapters.bizy_template_provider import BizyTemplateProvider
13
+ from faas_gitops.adapters.git_cli_gateway import GitCliGateway
14
+ from faas_gitops.cli.parser import build_parser
15
+ from faas_gitops.cli.presenters import print_apply_report, print_drift_report
16
+ from faas_gitops.config import load_settings
17
+ from faas_gitops.models import ApplyResult
18
+ from faas_gitops.usecases.apply_function_changes import ApplyFunctionChanges
19
+ from faas_gitops.usecases.apply_template_changes import ApplyTemplateChanges
20
+ from faas_gitops.usecases.detect_function_drift import DetectFunctionDrift
21
+ from faas_gitops.usecases.detect_template_drift import DetectTemplateDrift
22
+ from faas_gitops.usecases.render_definitions import render_definitions
23
+ from faas_gitops.usecases.sync_functions import SyncFunctions
24
+ from faas_gitops.usecases.sync_templates import SyncTemplates
25
+
26
+
27
+ def _make_git() -> GitCliGateway:
28
+ return GitCliGateway(Path(tempfile.mkdtemp(prefix="faas-gitops-")))
29
+
30
+
31
+ async def _make_faas(stack: AsyncExitStack) -> AsyncFaaSActionProvider:
32
+ faas = AsyncFaaSActionProvider()
33
+ stack.push_async_callback(faas.client.close)
34
+ return faas
35
+
36
+
37
+ async def async_main() -> int:
38
+ settings = load_settings()
39
+ parser = build_parser(settings)
40
+ args = parser.parse_args()
41
+ if not args.command:
42
+ parser.print_help()
43
+ return 1
44
+
45
+ repo_slug: str = args.repo
46
+
47
+ async with AsyncExitStack() as stack:
48
+ if args.command == "sync-templates":
49
+ faas = await _make_faas(stack)
50
+ provider = BizyTemplateProvider(faas)
51
+ git = _make_git()
52
+ push = await SyncTemplates(provider, git, settings).run(await git.clone_repo(repo_slug))
53
+ logger.info(push.url if push.pushed and push.url else ("branch pushed" if push.pushed else "no changes"))
54
+ return 0
55
+
56
+ if args.command == "sync-functions":
57
+ faas = await _make_faas(stack)
58
+ provider = BizyFunctionProvider(faas)
59
+ tpl_provider = BizyTemplateProvider(faas)
60
+ git = _make_git()
61
+ repo_path = await git.clone_repo(repo_slug)
62
+ push = await SyncFunctions(provider, git, settings, tpl_provider=tpl_provider, branch=args.branch).run(repo_path)
63
+ logger.info(push.url if push.pushed and push.url else ("branch pushed" if push.pushed else "no changes"))
64
+ return 0
65
+
66
+ if args.command == "render-definitions":
67
+ faas = await _make_faas(stack)
68
+ tpl_provider = BizyTemplateProvider(faas)
69
+ git = _make_git()
70
+ repo_path = await git.clone_repo(repo_slug)
71
+ n = await render_definitions(tpl_provider, repo_path, branch=args.branch, git=git, max_workers=settings.max_workers)
72
+ logger.info(f"render-definitions: {n} file(s) written")
73
+ return 0
74
+
75
+ if args.command == "detect-template-changes":
76
+ git = _make_git()
77
+ repo_path = await git.clone_repo(repo_slug)
78
+ return print_drift_report(await DetectTemplateDrift().run(repo_path))
79
+
80
+ if args.command == "detect-function-changes":
81
+ git = _make_git()
82
+ repo_path = await git.clone_repo(repo_slug)
83
+ return print_drift_report(await DetectFunctionDrift(settings).run(repo_path))
84
+
85
+ if args.command == "apply-template-changes":
86
+ faas = await _make_faas(stack)
87
+ git = _make_git()
88
+ repo_path = await git.clone_repo(repo_slug)
89
+ result = await ApplyTemplateChanges(BizyTemplateProvider(faas)).run(
90
+ repo_path, dry_run=getattr(args, "dry_run", False),
91
+ )
92
+ return print_apply_report(result, getattr(args, "dry_run", False))
93
+
94
+ if args.command == "apply-function-changes":
95
+ faas = await _make_faas(stack)
96
+ git = _make_git()
97
+ repo_path = await git.clone_repo(repo_slug)
98
+ result = await ApplyFunctionChanges(BizyFunctionProvider(faas), settings).run(
99
+ repo_path, dry_run=getattr(args, "dry_run", False), repo_slug=repo_slug,
100
+ )
101
+ return print_apply_report(result, getattr(args, "dry_run", False))
102
+
103
+ logger.warning(f"unknown command: {args.command}")
104
+ return 1
105
+
106
+
107
+ def main() -> int:
108
+ return asyncio.run(async_main())
109
+
110
+
111
+ if __name__ == "__main__":
112
+ raise SystemExit(main())
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+
5
+ from faas_gitops.config import Settings
6
+
7
+
8
+ def build_parser(settings: Settings) -> argparse.ArgumentParser:
9
+ parser = argparse.ArgumentParser(prog="faas-gitops")
10
+ sub = parser.add_subparsers(dest="command")
11
+
12
+ def _add_repo(p: argparse.ArgumentParser) -> None:
13
+ p.add_argument("--repo", default=settings.default_repo)
14
+
15
+ p = sub.add_parser("sync-templates")
16
+ p.add_argument("--branch", default="sync/faas-templates")
17
+ _add_repo(p)
18
+
19
+ p = sub.add_parser("sync-functions")
20
+ p.add_argument("--branch", default="sync/faas-functions")
21
+ _add_repo(p)
22
+
23
+ p = sub.add_parser("render-definitions")
24
+ p.add_argument("--branch", default=None)
25
+ _add_repo(p)
26
+
27
+ p = sub.add_parser("detect-template-changes")
28
+ _add_repo(p)
29
+
30
+ p = sub.add_parser("detect-function-changes")
31
+ _add_repo(p)
32
+
33
+ p = sub.add_parser("apply-template-changes")
34
+ p.add_argument("--dry-run", action="store_true")
35
+ _add_repo(p)
36
+
37
+ p = sub.add_parser("apply-function-changes")
38
+ p.add_argument("--dry-run", action="store_true")
39
+ _add_repo(p)
40
+
41
+ return parser
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from loguru import logger
4
+
5
+ from faas_gitops.models import ApplyResult, DriftResult
6
+
7
+
8
+ def print_drift_report(result: DriftResult) -> int:
9
+ has_drift = bool(result.drifted or result.missing_in_git or result.missing_in_faas)
10
+ logger.info(f"drifted={len(result.drifted)} missing_git={len(result.missing_in_git)} missing_faas={len(result.missing_in_faas)}")
11
+ for n in result.drifted:
12
+ logger.info(f" Δ {n}")
13
+ for n in result.missing_in_git:
14
+ logger.info(f" + {n}")
15
+ for n in result.missing_in_faas:
16
+ logger.info(f" - {n}")
17
+ if result.diff_text:
18
+ logger.info(result.diff_text)
19
+ return 1 if has_drift else 0
20
+
21
+
22
+ def print_apply_report(result: ApplyResult, dry_run: bool = False) -> int:
23
+ logger.info(f"applied={len(result.applied)} unchanged={len(result.unchanged)} skipped={len(result.skipped)}{' (dry-run)' if dry_run else ''}")
24
+ for n in result.applied:
25
+ logger.info(f" ✓ {n}")
26
+ for n in result.skipped:
27
+ logger.info(f" - {n}")
28
+ for name, err in result.failed:
29
+ logger.warning(f" ! {name}: {err}")
30
+ return 1 if result.failed else 0
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class Settings:
9
+ default_repo: str = "siliconflow/faas-gitops-test"
10
+ scoped_templates: set[str] | None = None
11
+ scoped_template_tags: set[str] | None = None
12
+ scoped_functions: set[str] | None = None
13
+ scoped_function_tags: set[str] | None = None
14
+ max_workers: int = 8
15
+
16
+
17
+ def _csv_set(key: str) -> set[str] | None:
18
+ val = os.getenv(key)
19
+ return {s for s in val.split(",") if s} if val else None
20
+
21
+
22
+ def load_settings() -> Settings:
23
+ return Settings(
24
+ default_repo=os.getenv("DEFAULT_REPO", Settings.default_repo),
25
+ scoped_templates=_csv_set("SCOPED_TEMPLATES"),
26
+ scoped_template_tags=_csv_set("SCOPED_TEMPLATE_TAGS"),
27
+ scoped_functions=_csv_set("SCOPED_FUNCTIONS"),
28
+ scoped_function_tags=_csv_set("SCOPED_FUNCTION_TAGS"),
29
+ max_workers=int(os.getenv("MAX_WORKERS", "128")),
30
+ )
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def match_scope(item_id: str, tags: list[str], id_set: set[str] | None, tag_set: set[str] | None) -> bool:
5
+ if id_set is None and tag_set is None:
6
+ return True
7
+ by_id = id_set is not None and item_id in id_set
8
+ by_tag = tag_set is not None and tags is not None and bool(set(tags) & tag_set)
9
+ return by_id or by_tag
File without changes
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ import yaml
4
+ from bizy_deploy.core.utils import standardize_faas_yaml
5
+
6
+
7
+ def load_yaml(text: str | None) -> dict | None:
8
+ if not text:
9
+ return None
10
+ return yaml.safe_load(text)
11
+
12
+
13
+ def dump_faas_yaml(data: dict | str) -> str:
14
+ if isinstance(data, dict):
15
+ data = yaml.dump(data, sort_keys=False, allow_unicode=True)
16
+ assert isinstance(data, str)
17
+ return standardize_faas_yaml(data)
@@ -0,0 +1,12 @@
1
+ """File layout conventions."""
2
+
3
+ TEMPLATE_DIR = "templates"
4
+ TEMPLATE_FILES = ("info.yaml", "template.jinja", "schema.yaml")
5
+ TEMPLATE_PRIMARY = "info.yaml"
6
+
7
+ FUNCTION_DIR = "functions"
8
+ FUNCTION_META = "meta.yaml"
9
+ FUNCTION_BASE = "base.yaml"
10
+ FUNCTION_DEFINITION = "definition.yaml"
11
+ FUNCTION_FILES = (FUNCTION_META, FUNCTION_BASE, FUNCTION_DEFINITION)
12
+ FUNCTION_RENDERED = "rendered.yaml"
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Dict
5
+ import tempfile
6
+
7
+ import yaml
8
+
9
+ from faas_gitops.infra.yaml_utils import dump_faas_yaml
10
+ from faas_gitops.protocols.faas_template import FaaSTemplateProvider
11
+
12
+ _INFO_FIELDS = {"displayName": "name", "description": "description", "icon": "icon", "tags": "tags"}
13
+ _INFO_FIELDS_INV = {dst: src for src, dst in _INFO_FIELDS.items()}
14
+
15
+
16
+ @dataclass
17
+ class DriftResult:
18
+ """Container for drift detection results."""
19
+
20
+ drifted: list[str] = field(default_factory=list)
21
+ missing_in_git: list[str] = field(default_factory=list)
22
+ missing_in_faas: list[str] = field(default_factory=list)
23
+ diff_text: str = ""
24
+
25
+
26
+ @dataclass(frozen=True, slots=True)
27
+ class TemplateParts:
28
+ """Decomposed template: info dict + optional jinja/schema."""
29
+
30
+ info: dict
31
+ jinja: str | None = None
32
+ schema: dict | None = None
33
+
34
+ def to_faas_yaml(self) -> str:
35
+ """Reassemble into SiliconFunctionTemplate YAML for FaaS API."""
36
+ metadata: dict = {"name": f"{self.info['subject']}/{self.info['id']}"}
37
+ for dst, src in _INFO_FIELDS_INV.items():
38
+ if dst in self.info:
39
+ metadata[src] = self.info[dst]
40
+ if "parent" in self.info:
41
+ metadata["parentTemplateName"] = self.info["parent"]
42
+ spec: dict = {}
43
+ if self.jinja is not None:
44
+ spec["jinjaTemplate"] = self.jinja
45
+ if self.schema is not None:
46
+ spec["schema"] = self.schema
47
+ return dump_faas_yaml({
48
+ "apiVersion": "function-template.6scloud.com/v1alpha1",
49
+ "kind": "SiliconFunctionTemplate",
50
+ "metadata": metadata,
51
+ "spec": spec,
52
+ })
53
+
54
+
55
+ @dataclass(frozen=True, slots=True)
56
+ class FunctionParts:
57
+ """Decomposed function: meta + optional base/definition."""
58
+
59
+ meta: dict
60
+ base: dict | None = None
61
+ definition: dict | None = None
62
+
63
+
64
+ def to_faas_meta(self) -> dict:
65
+ return {"functionName": self.meta.get("functionName", ""), "tags": self.meta.get("tags", [])}
66
+
67
+ def to_faas_definition(self, comment: str = None) -> dict | None:
68
+ if self.definition is None:
69
+ return
70
+ assert comment is not None
71
+ return {"metadata": {"templateName": self.base["templateName"]}, "spec": {"data": self.definition, "comment": comment}, "kind": "SiliconFunctionDefinition", "apiVersion": "function-form-value.6scloud.com/v1alpha1"}
72
+
73
+ async def render_draft(self, provider: FaaSTemplateProvider, tpl_data: Dict) -> dict:
74
+ tpl_data.setdefault("spec", {})["examples"] = [self.definition]
75
+ with tempfile.NamedTemporaryFile(mode='w+', suffix='.yaml', delete=False) as tmp_file:
76
+ yaml.dump(tpl_data, tmp_file, default_flow_style=False, allow_unicode=True)
77
+ out = await provider.render_template_draft(file=tmp_file.name)
78
+ return out.to_dict()
79
+
80
+ @dataclass
81
+ class ApplyResult:
82
+ """Container for apply operation results."""
83
+
84
+ applied: list[str] = field(default_factory=list)
85
+ unchanged: list[str] = field(default_factory=list)
86
+ skipped: list[str] = field(default_factory=list)
87
+ failed: list[tuple[str, str]] = field(default_factory=list)
@@ -0,0 +1,9 @@
1
+ from .faas_template import FaaSTemplateProvider
2
+ from .faas_function import FaaSFunctionProvider
3
+ from .git_gateway import GitWriter
4
+
5
+ __all__ = [
6
+ "FaaSTemplateProvider",
7
+ "FaaSFunctionProvider",
8
+ "GitWriter",
9
+ ]
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Protocol, runtime_checkable
4
+
5
+
6
+ @runtime_checkable
7
+ class FaaSFunctionProvider(Protocol):
8
+ """Interface for FaaS function CRUD operations.
9
+
10
+ Implementations wrap the bizy-deploy SDK or any other
11
+ FaaS-compatible backend. Used by sync (read) and apply
12
+ (read + write) use cases.
13
+ """
14
+
15
+ async def list_functions(self) -> list[dict]:
16
+ """List all accessible functions.
17
+
18
+ Returns:
19
+ List of function dicts with at least 'id', 'name', and 'tags' keys.
20
+ """
21
+ ...
22
+
23
+ async def get_latest_definition(self, func_id: str) -> dict | None:
24
+ """Get the latest function definition dict.
25
+
26
+ Args:
27
+ func_id: Function identifier.
28
+
29
+ Returns:
30
+ Definition dict, or None if no definitions exist.
31
+ """
32
+ ...
33
+
34
+ async def push_definition(self, func_id: str, data: dict, comment: str = "") -> str:
35
+ """Create a new function definition version.
36
+
37
+ Args:
38
+ func_id: Function identifier.
39
+ data: Definition payload dict.
40
+
41
+ Returns:
42
+ Version string of the newly created definition.
43
+ """
44
+ ...
45
+
46
+ async def update_function_meta(self, func_id: str, meta: dict) -> None:
47
+ """Update function metadata (name, tags).
48
+
49
+ Args:
50
+ func_id: Function identifier.
51
+ meta: Dict with 'functionName' and/or 'tags' keys.
52
+ """
53
+ ...