faas-gitops 0.2.1__py3-none-any.whl

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.
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Protocol, runtime_checkable
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class PushResult:
10
+ pushed: bool
11
+ url: str | None = None
12
+
13
+
14
+ @runtime_checkable
15
+ class GitWriter(Protocol):
16
+
17
+ async def clone_repo(self, repo_slug: str) -> Path:
18
+ ...
19
+
20
+ async def prepare_branch(self, repo_path: Path, branch: str, force: bool = False) -> None:
21
+ ...
22
+
23
+ async def commit_and_push(
24
+ self,
25
+ repo_path: Path,
26
+ prefix: str,
27
+ msg: str,
28
+ branch: str,
29
+ *,
30
+ force: bool = False,
31
+ create_pr: bool = False,
32
+ ) -> PushResult:
33
+ ...
34
+
35
+ async def current_branch(self, repo_path: Path) -> str:
36
+ """Return the name of the current branch."""
37
+ ...
38
+
39
+ async def checkout(self, repo_path: Path, ref: str) -> None:
40
+ """Switch to the given branch or ref."""
41
+ ...
File without changes
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from pathlib import Path
5
+
6
+ from loguru import logger
7
+
8
+ from faas_gitops.config import Settings
9
+ from faas_gitops.models import ApplyResult
10
+ from faas_gitops.protocols.faas_function import FaaSFunctionProvider
11
+ from faas_gitops.usecases.detect_function_drift import DetectFunctionDrift
12
+ from faas_gitops.usecases.sync_functions import read_function_from_dir
13
+
14
+
15
+ class ApplyFunctionChanges:
16
+
17
+ def __init__(self, function_provider: FaaSFunctionProvider, settings: Settings) -> None:
18
+ self._provider = function_provider
19
+ self._settings = settings
20
+
21
+ async def run(self, repo_path: Path, base: str = "main", dry_run: bool = False, sync_branch: str = "sync/faas-functions", repo_slug: str = "") -> ApplyResult:
22
+ result = ApplyResult()
23
+ drift = await DetectFunctionDrift(self._settings).run(repo_path, base, sync_branch)
24
+ if not drift.drifted:
25
+ return result
26
+ proc = await asyncio.create_subprocess_exec("git", "checkout", base, cwd=repo_path, stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL)
27
+ await proc.communicate()
28
+ proc = await asyncio.create_subprocess_exec("git", "pull", cwd=repo_path, stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL)
29
+ await proc.communicate()
30
+ proc = await asyncio.create_subprocess_exec(
31
+ "git", "rev-parse", "--short", f"origin/{base}",
32
+ cwd=repo_path,
33
+ stdout=asyncio.subprocess.PIPE,
34
+ stderr=asyncio.subprocess.PIPE,
35
+ )
36
+ stdout, _ = await proc.communicate()
37
+ short_hash = stdout.decode().strip()
38
+ comment = f"{repo_slug.rsplit('/', 1)[-1]}@{short_hash}" if short_hash else ""
39
+ for fid in drift.missing_in_git:
40
+ logger.warning(f" ! {fid}: not found on {base}, skipping")
41
+ for fid in drift.drifted:
42
+ func_dir = repo_path / "functions" / fid
43
+ if not func_dir.exists():
44
+ logger.warning(f" ! {fid}: not found on {base}, skipping")
45
+ result.skipped.append(fid)
46
+ continue
47
+ if dry_run:
48
+ result.skipped.append(fid)
49
+ continue
50
+ parts = await read_function_from_dir(func_dir)
51
+ if parts.definition is not None:
52
+ def_payload = parts.to_faas_definition(comment=comment)
53
+ if def_payload is not None:
54
+ await self._provider.push_definition(fid, def_payload)
55
+ await self._provider.update_function_meta(fid, parts.to_faas_meta())
56
+ result.applied.append(fid)
57
+ return result
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from pathlib import Path
5
+
6
+ from loguru import logger
7
+
8
+ from faas_gitops.models import ApplyResult
9
+ from faas_gitops.protocols.faas_template import FaaSTemplateProvider
10
+ from faas_gitops.usecases.detect_template_drift import DetectTemplateDrift
11
+ from faas_gitops.usecases.sync_templates import read_template_from_dir
12
+
13
+
14
+ class ApplyTemplateChanges:
15
+
16
+ def __init__(self, template_provider: FaaSTemplateProvider) -> None:
17
+ self._provider = template_provider
18
+
19
+ async def run(self, repo_path: Path, base: str = "main", dry_run: bool = False, sync_branch: str = "sync/faas-templates") -> ApplyResult:
20
+ result = ApplyResult()
21
+ drift = await DetectTemplateDrift().run(repo_path, base, sync_branch)
22
+ if not drift.drifted:
23
+ return result
24
+ proc = await asyncio.create_subprocess_exec("git", "checkout", base, cwd=repo_path, stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL)
25
+ await proc.communicate()
26
+ proc = await asyncio.create_subprocess_exec("git", "pull", cwd=repo_path, stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL)
27
+ await proc.communicate()
28
+ for item_id in drift.missing_in_git:
29
+ logger.warning(f" ! {item_id}: not found on {base}, skipping")
30
+ for item_id in drift.drifted:
31
+ parts = item_id.split("/")
32
+ if len(parts) != 2:
33
+ continue
34
+ org_id, tpl_name = parts
35
+ tpl_dir = repo_path / "templates" / org_id / tpl_name
36
+ if not tpl_dir.exists():
37
+ logger.warning(f" ! {item_id}: not found on {base}, skipping")
38
+ result.skipped.append(item_id)
39
+ continue
40
+ git_yaml = (await read_template_from_dir(tpl_dir)).to_faas_yaml()
41
+ faas_yaml = await self._provider.get_template_content(org_id, tpl_name)
42
+ if git_yaml == faas_yaml:
43
+ result.unchanged.append(item_id)
44
+ elif dry_run:
45
+ result.skipped.append(item_id)
46
+ else:
47
+ await self._provider.update_template(org_id, tpl_name, git_yaml)
48
+ result.applied.append(item_id)
49
+ return result
@@ -0,0 +1,76 @@
1
+ """Shared git-diff based drift detection logic."""
2
+
3
+ from __future__ import annotations
4
+ from loguru import logger
5
+ import asyncio
6
+ import re
7
+ from pathlib import Path
8
+
9
+ from faas_gitops.models import DriftResult
10
+
11
+ _DIFF_HEADER = re.compile(r"^diff --git a/.+ b/(.+)$")
12
+ _NEW_FILE = re.compile(r"^new file mode")
13
+ _DELETED_FILE = re.compile(r"^deleted file mode")
14
+
15
+
16
+ async def run_git_diff(repo_path: Path, base: str, head: str, prefix: str) -> str:
17
+ cmd = ["git", "diff", f"origin/{head}..origin/{base}", "--", prefix]
18
+ logger.info("执行指令" + " ".join(cmd))
19
+ logger.info(f'TODo 实现打印在线预览')
20
+ proc = await asyncio.create_subprocess_exec(
21
+ *cmd,
22
+ cwd=repo_path,
23
+ stdout=asyncio.subprocess.PIPE,
24
+ stderr=asyncio.subprocess.PIPE,
25
+ )
26
+ stdout, _ = await proc.communicate()
27
+ return stdout.decode()
28
+
29
+
30
+ def parse_git_diff(text: str, prefix: str, depth: int, *, exclude_suffixes: tuple[str, ...] = ()) -> DriftResult:
31
+ result = DriftResult(diff_text=text)
32
+ if not text:
33
+ return result
34
+
35
+ seen: dict[str, str] = {}
36
+ current_path: str | None = None
37
+ current_status: str | None = None
38
+ for line in text.splitlines():
39
+ m = _DIFF_HEADER.match(line)
40
+ if m:
41
+ if current_path and current_status:
42
+ seen[current_path] = current_status
43
+ current_path = m.group(1)
44
+ current_status = "modified"
45
+ continue
46
+ if current_path:
47
+ if _NEW_FILE.match(line):
48
+ current_status = "missing_in_faas"
49
+ elif _DELETED_FILE.match(line):
50
+ current_status = "missing_in_git"
51
+ if current_path and current_status:
52
+ seen[current_path] = current_status
53
+
54
+ items: dict[str, str] = {}
55
+ for path, status in seen.items():
56
+ if not path.startswith(prefix + "/"):
57
+ continue
58
+ if exclude_suffixes and any(path.endswith(s) for s in exclude_suffixes):
59
+ continue
60
+ rel = path[len(prefix) + 1:]
61
+ parts = rel.split("/")
62
+ if len(parts) <= depth:
63
+ continue
64
+ item_id = "/".join(parts[:depth])
65
+ if item_id in items:
66
+ continue
67
+ items[item_id] = status
68
+
69
+ for item_id, status in items.items():
70
+ if status == "missing_in_faas":
71
+ result.missing_in_faas.append(item_id)
72
+ elif status == "missing_in_git":
73
+ result.missing_in_git.append(item_id)
74
+ else:
75
+ result.drifted.append(item_id)
76
+ return result
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from pathlib import Path
5
+
6
+ import aiofiles
7
+ import yaml
8
+
9
+ from faas_gitops.config import Settings
10
+ from faas_gitops.filtering import match_scope
11
+ from faas_gitops.layout import FUNCTION_DIR, FUNCTION_META
12
+ from faas_gitops.models import DriftResult
13
+ from faas_gitops.usecases.detect_drift import parse_git_diff, run_git_diff
14
+
15
+
16
+ class DetectFunctionDrift:
17
+
18
+ def __init__(self, settings: Settings) -> None:
19
+ self._settings = settings
20
+
21
+ async def run(self, repo_path: Path, base: str = "main", sync_branch: str = "sync/faas-functions") -> DriftResult:
22
+ text = await run_git_diff(repo_path, base, sync_branch, FUNCTION_DIR)
23
+ result = parse_git_diff(text, FUNCTION_DIR, depth=1, exclude_suffixes=())
24
+ if not any((result.drifted, result.missing_in_git, result.missing_in_faas)):
25
+ return result
26
+ await self._apply_scope(result, repo_path)
27
+ return result
28
+
29
+ async def _apply_scope(self, result: DriftResult, repo_path: Path) -> None:
30
+ scoped = self._settings.scoped_functions
31
+ tags = self._settings.scoped_function_tags
32
+ if scoped is None and tags is None:
33
+ return
34
+ result.drifted = [fid for fid in result.drifted if await self._in_scope(fid, repo_path)]
35
+ result.missing_in_git = [fid for fid in result.missing_in_git if await self._in_scope(fid, repo_path)]
36
+ result.missing_in_faas = [fid for fid in result.missing_in_faas if await self._in_scope(fid, repo_path)]
37
+
38
+ async def _in_scope(self, fid: str, repo_path: Path) -> bool:
39
+ meta_path = repo_path / FUNCTION_DIR / fid / FUNCTION_META
40
+ if not meta_path.exists():
41
+ return True
42
+ async with aiofiles.open(meta_path) as f:
43
+ meta = yaml.safe_load(await f.read()) or {}
44
+ func_tags = meta.get("tags") or []
45
+ return match_scope(fid, func_tags, self._settings.scoped_functions, self._settings.scoped_function_tags)
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from faas_gitops.layout import TEMPLATE_DIR
6
+ from faas_gitops.models import DriftResult
7
+ from faas_gitops.usecases.detect_drift import parse_git_diff, run_git_diff
8
+
9
+
10
+ class DetectTemplateDrift:
11
+
12
+ async def run(self, repo_path: Path, base: str = "main", sync_branch: str = "sync/faas-templates") -> DriftResult:
13
+ text = await run_git_diff(repo_path, base, sync_branch, TEMPLATE_DIR)
14
+ return parse_git_diff(text, TEMPLATE_DIR, depth=2)
@@ -0,0 +1,96 @@
1
+ """Render rendered.yaml for all functions in a local repo."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ from pathlib import Path
6
+
7
+ import aiofiles
8
+ import yaml
9
+ from loguru import logger
10
+ from tqdm.asyncio import tqdm_asyncio
11
+
12
+ from faas_gitops.infra.yaml_utils import dump_faas_yaml, load_yaml
13
+ from faas_gitops.layout import FUNCTION_BASE, FUNCTION_DEFINITION, FUNCTION_DIR, FUNCTION_RENDERED, TEMPLATE_DIR
14
+ from faas_gitops.protocols.faas_template import FaaSTemplateProvider
15
+ from faas_gitops.protocols.git_gateway import GitWriter
16
+ from faas_gitops.usecases.sync_functions import read_function_from_dir
17
+ from faas_gitops.usecases.sync_templates import _quiet_logger, read_template_from_dir
18
+
19
+ _RENDERED_HEADER = "# rendered by faas-gitops, do not modify\n"
20
+
21
+
22
+ def _function_dirs(func_dir: Path) -> list[Path]:
23
+ if not func_dir.is_dir():
24
+ return []
25
+ return sorted(
26
+ d for d in func_dir.iterdir()
27
+ if d.is_dir() and (d / FUNCTION_DEFINITION).exists() and (d / FUNCTION_BASE).exists()
28
+ )
29
+
30
+
31
+ def _resolve_template_dir(repo_path: Path, base: dict) -> Path | None:
32
+ tpl_name: str = base.get("templateName", "")
33
+ if "/" not in tpl_name:
34
+ return None
35
+ org_id, tpl_id = tpl_name.split("/", 1)
36
+ return repo_path / TEMPLATE_DIR / org_id / tpl_id
37
+
38
+
39
+ async def _render_one(
40
+ func_dir: Path, repo_path: Path, provider: FaaSTemplateProvider, sem: asyncio.Semaphore,
41
+ ) -> int:
42
+ async with sem:
43
+ try:
44
+ base = load_yaml((func_dir / FUNCTION_BASE).read_text())
45
+ tpl_dir = _resolve_template_dir(repo_path, base or {})
46
+ if tpl_dir is None:
47
+ return 0
48
+ if not tpl_dir.exists():
49
+ logger.warning(f"template not found: {tpl_dir}, skipping {func_dir.name}")
50
+ return 0
51
+
52
+ tpl_data = yaml.safe_load((await read_template_from_dir(tpl_dir)).to_faas_yaml())
53
+ parts = await read_function_from_dir(func_dir)
54
+ rendered = await parts.render_draft(provider, tpl_data)
55
+
56
+ content = _RENDERED_HEADER + dump_faas_yaml(rendered)
57
+ out = func_dir / FUNCTION_RENDERED
58
+ if out.exists() and out.read_text() == content:
59
+ return 0
60
+ async with aiofiles.open(out, "w") as f:
61
+ await f.write(content)
62
+ return 1
63
+
64
+ except Exception as e:
65
+ logger.warning(f"render failed for {func_dir.name}: {e}")
66
+ return 0
67
+
68
+
69
+ async def render_definitions(
70
+ provider: FaaSTemplateProvider,
71
+ repo_path: Path,
72
+ *,
73
+ branch: str,
74
+ git: GitWriter,
75
+ max_workers: int = 16,
76
+ ) -> int:
77
+ assert branch is not None
78
+ await git.prepare_branch(repo_path=repo_path, branch=branch, force=False)
79
+ logger.info(f"render-definitions: branch={branch}")
80
+
81
+ func_dir = repo_path / FUNCTION_DIR
82
+ dirs = _function_dirs(func_dir)
83
+ sem = asyncio.Semaphore(max_workers)
84
+
85
+ with _quiet_logger():
86
+ results = await tqdm_asyncio.gather(
87
+ *[_render_one(d, repo_path, provider, sem) for d in dirs],
88
+ desc=f"render-definitions [{branch}]",
89
+ unit="func",
90
+ )
91
+ written = sum(results)
92
+ await git.commit_and_push(
93
+ repo_path, FUNCTION_DIR, f"render-definitions[{written}]",
94
+ branch=branch, force=False, create_pr=False,
95
+ )
96
+ return written
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from pathlib import Path
5
+ import yaml
6
+ from loguru import logger
7
+
8
+ import aiofiles
9
+ from tqdm.asyncio import tqdm_asyncio
10
+
11
+ from faas_gitops.config import Settings
12
+ from faas_gitops.usecases.sync_templates import _quiet_logger
13
+ from faas_gitops.filtering import match_scope
14
+ from faas_gitops.infra.yaml_utils import dump_faas_yaml, load_yaml
15
+ from faas_gitops.layout import FUNCTION_BASE, FUNCTION_DEFINITION, FUNCTION_DIR, FUNCTION_META, FUNCTION_RENDERED, TEMPLATE_DIR
16
+ from faas_gitops.models import FunctionParts
17
+ from faas_gitops.protocols.faas_function import FaaSFunctionProvider
18
+ from faas_gitops.protocols.faas_template import FaaSTemplateProvider
19
+ from faas_gitops.protocols.git_gateway import GitWriter
20
+ from faas_gitops.usecases.sync_templates import read_template_from_dir
21
+
22
+
23
+ async def _write_function(parts: FunctionParts, dest: Path) -> None:
24
+ dest.mkdir(parents=True, exist_ok=True)
25
+ async with aiofiles.open(dest / FUNCTION_META, "w") as f:
26
+ await f.write(dump_faas_yaml(parts.meta))
27
+ if parts.base is not None:
28
+ async with aiofiles.open(dest / FUNCTION_BASE, "w") as f:
29
+ await f.write(dump_faas_yaml(parts.base))
30
+ if parts.definition is not None:
31
+ async with aiofiles.open(dest / FUNCTION_DEFINITION, "w") as f:
32
+ await f.write(dump_faas_yaml(parts.definition))
33
+
34
+ async def read_function_from_dir(func_dir: Path) -> FunctionParts:
35
+ async with aiofiles.open(func_dir / FUNCTION_META) as f:
36
+ meta = load_yaml(await f.read()) or {}
37
+ base = None
38
+ base_path = func_dir / FUNCTION_BASE
39
+ if base_path.exists():
40
+ async with aiofiles.open(base_path) as f:
41
+ base = load_yaml(await f.read())
42
+ definition = None
43
+ def_path = func_dir / FUNCTION_DEFINITION
44
+ if def_path.exists():
45
+ async with aiofiles.open(def_path) as f:
46
+ definition = load_yaml(await f.read())
47
+ return FunctionParts(meta=meta, base=base, definition=definition)
48
+
49
+
50
+ class SyncFunctions:
51
+ """FaaS→Git function sync orchestration.
52
+
53
+ Fetches functions from FaaS, writes meta + runtime + definition YAML files,
54
+ force-pushes to a sync branch.
55
+
56
+ File layout per function:
57
+ functions/{func_id}/meta.yaml — function metadata (name, tags)
58
+ functions/{func_id}/base.yaml — runtime base (templateName)
59
+ functions/{func_id}/definition.yaml — spec.data
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ provider: FaaSFunctionProvider,
65
+ git_writer: GitWriter,
66
+ settings: Settings,
67
+ tpl_provider: FaaSTemplateProvider | None = None,
68
+ branch: str = "sync/faas-functions",
69
+ ) -> None:
70
+ self._provider = provider
71
+ self._git_writer = git_writer
72
+ self._settings = settings
73
+ self._tpl_provider = tpl_provider
74
+ self._branch = branch
75
+
76
+ async def run(self, repo_path: Path):
77
+ functions = [
78
+ f for f in await self._provider.list_functions()
79
+ if match_scope(f["id"], f.get("tags", []), self._settings.scoped_functions, self._settings.scoped_function_tags)
80
+ ]
81
+ await self._git_writer.prepare_branch(repo_path, self._branch, force=True)
82
+ out_dir = repo_path / FUNCTION_DIR
83
+ sem = asyncio.Semaphore(self._settings.max_workers)
84
+
85
+ async def _sync_one(func: dict) -> None:
86
+ async with sem:
87
+ fid = func["id"]
88
+ raw = await self._provider.get_latest_definition(fid)
89
+ meta = {"functionName": func.get("name", ""), "tags": func.get("tags", [])}
90
+ base = None
91
+ definition = None
92
+ if raw is not None:
93
+ base = {"templateName": raw.get("metadata", {}).get("templateName", "")}
94
+ definition = raw.get("spec", {}).get("data")
95
+ await _write_function(FunctionParts(meta=meta, base=base, definition=definition), out_dir / fid)
96
+
97
+ with _quiet_logger():
98
+ await tqdm_asyncio.gather(*[_sync_one(f) for f in functions], desc="sync-functions")
99
+ return await self._git_writer.commit_and_push(
100
+ repo_path, FUNCTION_DIR, f"sync: {len(functions)} functions", self._branch,
101
+ force=True, create_pr=True,
102
+ )
@@ -0,0 +1,106 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from contextlib import contextmanager
5
+ from pathlib import Path
6
+
7
+ import aiofiles
8
+ import yaml
9
+ from loguru import logger
10
+ from tqdm.asyncio import tqdm_asyncio
11
+
12
+ from faas_gitops.config import Settings
13
+ from faas_gitops.filtering import match_scope
14
+ from faas_gitops.protocols.faas_template import FaaSTemplateProvider
15
+ from faas_gitops.infra.yaml_utils import dump_faas_yaml, load_yaml
16
+ from faas_gitops.layout import TEMPLATE_DIR, TEMPLATE_PRIMARY
17
+ from faas_gitops.models import TemplateParts, _INFO_FIELDS
18
+ from faas_gitops.protocols.git_gateway import GitWriter
19
+
20
+
21
+ @contextmanager
22
+ def _quiet_logger():
23
+ logger.disable("bizy_deploy")
24
+ try:
25
+ yield
26
+ finally:
27
+ logger.enable("bizy_deploy")
28
+
29
+
30
+ def _split_template(content: str) -> TemplateParts:
31
+ data = yaml.safe_load(content)
32
+ meta, spec = data.get("metadata", {}), data.get("spec", {})
33
+ org_id, tpl_id = meta["name"].split("/")
34
+ info = {"subject": org_id, "id": tpl_id} | {
35
+ dst: meta[src] for src, dst in _INFO_FIELDS.items() if meta.get(src)
36
+ }
37
+ if v := meta.get("parentTemplateName"):
38
+ info["parent"] = v
39
+ return TemplateParts(info=info, jinja=spec.get("jinjaTemplate"), schema=spec.get("schema"))
40
+
41
+
42
+ async def read_template_from_dir(template_dir: Path) -> TemplateParts:
43
+ async with aiofiles.open(template_dir / TEMPLATE_PRIMARY) as f:
44
+ info = load_yaml(await f.read()) or {}
45
+ jinja = None
46
+ jinja_path = template_dir / "template.jinja"
47
+ if jinja_path.exists():
48
+ async with aiofiles.open(jinja_path) as f:
49
+ jinja = await f.read()
50
+ schema = None
51
+ schema_path = template_dir / "schema.yaml"
52
+ if schema_path.exists():
53
+ async with aiofiles.open(schema_path) as f:
54
+ schema = load_yaml(await f.read())
55
+ return TemplateParts(info=info, jinja=jinja, schema=schema)
56
+
57
+
58
+ async def _write_files(parts: TemplateParts, dest: Path) -> None:
59
+ dest.mkdir(parents=True, exist_ok=True)
60
+ async with aiofiles.open(dest / TEMPLATE_PRIMARY, "w") as f:
61
+ await f.write(dump_faas_yaml(parts.info))
62
+ if parts.jinja:
63
+ async with aiofiles.open(dest / "template.jinja", "w") as f:
64
+ await f.write(parts.jinja)
65
+ if parts.schema:
66
+ async with aiofiles.open(dest / "schema.yaml", "w") as f:
67
+ await f.write(dump_faas_yaml(parts.schema))
68
+
69
+
70
+ class SyncTemplates:
71
+
72
+ def __init__(
73
+ self,
74
+ provider: FaaSTemplateProvider,
75
+ git_writer: GitWriter,
76
+ settings: Settings,
77
+ branch: str = "sync/faas-templates",
78
+ ) -> None:
79
+ self._provider = provider
80
+ self._git = git_writer
81
+ self._settings = settings
82
+ self._branch = branch
83
+
84
+ async def run(self, repo_path: Path):
85
+ templates = [
86
+ t for t in await self._provider.list_templates()
87
+ if match_scope(t["name"], t.get("tags", []), self._settings.scoped_templates, self._settings.scoped_template_tags)
88
+ ]
89
+ await self._git.prepare_branch(repo_path, self._branch, force=True)
90
+ out_dir = repo_path / TEMPLATE_DIR
91
+ sem = asyncio.Semaphore(self._settings.max_workers)
92
+
93
+ async def _sync_one(t: dict) -> None:
94
+ async with sem:
95
+ org_id, tpl_id = t["name"].split("/")
96
+ content = await self._provider.get_template_content(org_id, tpl_id)
97
+ parts = _split_template(content)
98
+ dest = out_dir / parts.info["subject"] / parts.info["id"]
99
+ await _write_files(parts, dest)
100
+
101
+ with _quiet_logger():
102
+ await tqdm_asyncio.gather(*[_sync_one(t) for t in templates], desc="sync-templates")
103
+ return await self._git.commit_and_push(
104
+ repo_path, TEMPLATE_DIR, f"sync: {len(templates)} templates", self._branch,
105
+ force=True, create_pr=True,
106
+ )
@@ -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,33 @@
1
+ faas_gitops/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ faas_gitops/config.py,sha256=1cIzgALAfBQG7o9PTgLcTXICnnr6_rS1utCnOiAbQRs,948
3
+ faas_gitops/filtering.py,sha256=_fPG_vDVbA_mp8q-6Jx6Ovd4su350NstGrmbpwlXf4c,371
4
+ faas_gitops/layout.py,sha256=RRggSifRkiEVW8LXZMdQoVUxhu7N3LFVmGda4JMgxOY,383
5
+ faas_gitops/models.py,sha256=wp09GBqtIX3ineXgKzdXXBb5SlD7f7VGfWUGruFPZD4,3213
6
+ faas_gitops/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ faas_gitops/adapters/bizy_function_provider.py,sha256=C_cjPYYD-7GwnwLpo2dTwgPqLiOmph60GnKu3iH3TrA,1175
8
+ faas_gitops/adapters/bizy_template_provider.py,sha256=Si18FYUH08F0UQmgwFdrRQ0U6Bi0z9Mq1vx79taH6KI,1360
9
+ faas_gitops/adapters/git_cli_gateway.py,sha256=oIAsll39ox_EzoT_Zb8oVO37EFu4fNQrwA6KGeoOKlI,4255
10
+ faas_gitops/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ faas_gitops/cli/app.py,sha256=qZr7p-h1BOrFqR3mrdLMSeeA3gh8NAf-6m63sWoKmDs,4619
12
+ faas_gitops/cli/parser.py,sha256=jP7BB-ZdPuuPKgf8RGA7XZNwVI4l_ZdyEbhuGQNWp4c,1134
13
+ faas_gitops/cli/presenters.py,sha256=ky8l7fSHcR37uf74IiXTWt06fgtnpnfJ-zbUlRKWVLY,1156
14
+ faas_gitops/infra/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ faas_gitops/infra/yaml_utils.py,sha256=DzvjabEP2sKEUK4wv7XUzt0BowQroa1p-ZDeGsv5Iq8,442
16
+ faas_gitops/protocols/__init__.py,sha256=Jhmfp9QQ2K_JdKm-98hASR9vqo2SBe8ea42pj1wIhcU,219
17
+ faas_gitops/protocols/faas_function.py,sha256=Y8dMnML0UHDrOoX7Bv6_mSBc-GMHrxoTImjynYCYG9M,1468
18
+ faas_gitops/protocols/faas_template.py,sha256=OaMLRIgtYuBNMaFq0Sbas-MMbtt3p842YVHfzsSvBm8,2148
19
+ faas_gitops/protocols/git_gateway.py,sha256=ZJ2IbJd4KDWNNR8ayOKd8P5yRymW12CwULtj2YdnL7c,945
20
+ faas_gitops/usecases/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ faas_gitops/usecases/apply_function_changes.py,sha256=tj7nRrELwMfo9HxUhtAUGxnnS-PVqUp-gv9M_SO8rqA,2653
22
+ faas_gitops/usecases/apply_template_changes.py,sha256=D-TlvM0TkVs9Ua74vMOYPlNsyGBDNlJ1pnKIM3XQIDU,2234
23
+ faas_gitops/usecases/detect_drift.py,sha256=aeAtg2ovIa1KOI9egB3TOpf8tgXeSwF3y7R-KkXX7Kk,2513
24
+ faas_gitops/usecases/detect_function_drift.py,sha256=EhDxe9jMuCP1jk5_yYveECzK6sWRs9f3jVtFFaFqt7A,1969
25
+ faas_gitops/usecases/detect_template_drift.py,sha256=zT53j9MbU50RVFVDePbdP6UN8mI9YK1C2ewmqr_RLU8,510
26
+ faas_gitops/usecases/render_definitions.py,sha256=gELdUQY9KKAILhNEPNHGM303sCqT3FUOc7jc5X6HOP8,3333
27
+ faas_gitops/usecases/sync_functions.py,sha256=S-0Uqt2NlE0z1JyTTWMZkLvIr2RiNsFCo0BzvXNC_Mg,4268
28
+ faas_gitops/usecases/sync_templates.py,sha256=iF5rcPKr2sOxAk7ujL1HcIBmo_Rr-idLQoeOkhtRe7s,3857
29
+ faas_gitops-0.2.1.dist-info/METADATA,sha256=nTiS8m6oa2ajNQ95L8rMxIXsjLuyXnHo66ve_RZSOT4,353
30
+ faas_gitops-0.2.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
31
+ faas_gitops-0.2.1.dist-info/entry_points.txt,sha256=caS7fkXkKLHfS5QE4-_0C13hYKDusf22xpgZ9d1GAPM,57
32
+ faas_gitops-0.2.1.dist-info/top_level.txt,sha256=vFe6sgocS2gAPxbwBQtVR7Jm2ZaMMR-XCIDgAdVFVoU,12
33
+ faas_gitops-0.2.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ faas-gitops = faas_gitops.cli.app:main
@@ -0,0 +1 @@
1
+ faas_gitops