cfgit 0.1.0__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.
- cfg/__init__.py +13 -0
- cfg/adapters/__init__.py +25 -0
- cfg/adapters/base.py +127 -0
- cfg/adapters/mongo.py +570 -0
- cfg/adapters/postgres.py +756 -0
- cfg/approval/__init__.py +5 -0
- cfg/approval/base.py +29 -0
- cfg/cli/__init__.py +2 -0
- cfg/cli/main.py +665 -0
- cfg/core/__init__.py +13 -0
- cfg/core/authz.py +58 -0
- cfg/core/config.py +324 -0
- cfg/core/diff.py +43 -0
- cfg/core/engine.py +1388 -0
- cfg/core/hashing.py +102 -0
- cfg/core/identity.py +213 -0
- cfg/interfaces/__init__.py +2 -0
- cfg/interfaces/actions.py +598 -0
- cfg/mcp/__init__.py +10 -0
- cfg/mcp/server.py +452 -0
- cfg/ui/__init__.py +2 -0
- cfg/ui/server.py +1066 -0
- cfgit-0.1.0.dist-info/METADATA +744 -0
- cfgit-0.1.0.dist-info/RECORD +28 -0
- cfgit-0.1.0.dist-info/WHEEL +4 -0
- cfgit-0.1.0.dist-info/entry_points.txt +3 -0
- cfgit-0.1.0.dist-info/licenses/LICENSE +201 -0
- cfgit-0.1.0.dist-info/licenses/NOTICE +10 -0
cfg/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Copyright 2026 Mohammad Ausaf. Licensed under the Apache License, Version 2.0.
|
|
2
|
+
"""cfg — git-shaped version control for database-resident config documents.
|
|
3
|
+
|
|
4
|
+
See docs/SPEC.md for the specification. Architecture (SPEC §1):
|
|
5
|
+
cfg.core — engine; depends ONLY on StorageAdapter + ApprovalProvider.
|
|
6
|
+
cfg.adapters — StorageAdapter implementations (the DB seam).
|
|
7
|
+
cfg.approval — ApprovalProvider implementations (out-of-band human approval).
|
|
8
|
+
cfg.cli — porcelain + plumbing interfaces.
|
|
9
|
+
cfg.mcp — agent surface (uniform result envelope).
|
|
10
|
+
The optional LLM impact layer lives OUT of this package, in plugins/cfg_impact.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
__version__ = "0.0.0"
|
cfg/adapters/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Copyright 2026 Mohammad Ausaf. Licensed under the Apache License, Version 2.0.
|
|
2
|
+
"""StorageAdapter implementations (the DB seam, SPEC §2). Mongo is first."""
|
|
3
|
+
from cfg.adapters.base import (
|
|
4
|
+
AmbiguousConfig,
|
|
5
|
+
ApplyResult,
|
|
6
|
+
AtomicityReport,
|
|
7
|
+
HistoryEnvMismatch,
|
|
8
|
+
NoSuchConfig,
|
|
9
|
+
ReconcileReport,
|
|
10
|
+
StaleHead,
|
|
11
|
+
StaleLive,
|
|
12
|
+
StorageAdapter,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"StorageAdapter",
|
|
17
|
+
"ApplyResult",
|
|
18
|
+
"ReconcileReport",
|
|
19
|
+
"AtomicityReport",
|
|
20
|
+
"StaleHead",
|
|
21
|
+
"StaleLive",
|
|
22
|
+
"AmbiguousConfig",
|
|
23
|
+
"HistoryEnvMismatch",
|
|
24
|
+
"NoSuchConfig",
|
|
25
|
+
]
|
cfg/adapters/base.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Copyright 2026 Mohammad Ausaf. Licensed under the Apache License, Version 2.0.
|
|
2
|
+
"""StorageAdapter: the DB seam (SPEC §2).
|
|
3
|
+
|
|
4
|
+
The single contract every backend implements. The core engine talks ONLY to this
|
|
5
|
+
Protocol; no DB driver is imported in cfg.core (enforced by tests/test_core_purity).
|
|
6
|
+
|
|
7
|
+
The v1 surface is collection-aware: cfgit versions opaque records in live
|
|
8
|
+
collections, not only "configs." Mongo is the first concrete adapter
|
|
9
|
+
(cfg.adapters.mongo).
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import Protocol, runtime_checkable
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# --- errors the engine branches on (mapped to CLI exit codes / MCP envelope) ---
|
|
19
|
+
class StaleHead(Exception):
|
|
20
|
+
"""A concurrent commit moved HEAD since we read it. Exit 2."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class StaleLive(Exception):
|
|
24
|
+
"""A raw bypass moved the runtime record since we read it. Exit 2."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AmbiguousConfig(Exception):
|
|
28
|
+
"""More than one live record matched a collection id. Exit 6."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class HistoryEnvMismatch(AmbiguousConfig):
|
|
32
|
+
"""History for this record exists under another configured env name. Exit 6."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class NoSuchConfig(Exception):
|
|
36
|
+
"""No live record matched where one was required. Exit 5."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AtomicityUnavailable(Exception):
|
|
40
|
+
"""The backend cannot make the requested mutation atomically. Exit 3."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ApplyResult:
|
|
45
|
+
collection: str
|
|
46
|
+
record_id: str
|
|
47
|
+
seq: int
|
|
48
|
+
oid: str
|
|
49
|
+
head_oid: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class ReconcileReport:
|
|
54
|
+
rolled_forward: list[str]
|
|
55
|
+
rolled_back: list[str]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class AtomicityReport:
|
|
60
|
+
"""[SPEC V3-1] Whether runtime+history+heads can share one transaction."""
|
|
61
|
+
atomic: bool
|
|
62
|
+
runtime_cluster: str
|
|
63
|
+
history_cluster: str
|
|
64
|
+
reason: str
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def history_env_mismatch_message(
|
|
68
|
+
*,
|
|
69
|
+
collection: str,
|
|
70
|
+
record_id: str,
|
|
71
|
+
current_env: str,
|
|
72
|
+
other_envs: list[str],
|
|
73
|
+
) -> str:
|
|
74
|
+
env_list = ", ".join(other_envs)
|
|
75
|
+
return (
|
|
76
|
+
f"no history found for {collection}:{record_id} under env={current_env!r}, "
|
|
77
|
+
f"but history/head rows exist under env(s): {env_list}. "
|
|
78
|
+
"Run with the env name that originally wrote this history, or update .cfg.toml so "
|
|
79
|
+
"this database is always addressed with one stable env name."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@runtime_checkable
|
|
84
|
+
class StorageAdapter(Protocol):
|
|
85
|
+
# runtime store
|
|
86
|
+
def get_record(self, collection: str, record_id: str) -> dict | None: ...
|
|
87
|
+
def put_record(self, collection: str, record_id: str, doc: dict) -> None: ...
|
|
88
|
+
def seed_record(self, collection: str, record_id: str, doc: dict) -> None: ...
|
|
89
|
+
def list_record_ids(self, collection: str) -> list[str]: ...
|
|
90
|
+
|
|
91
|
+
# history reads
|
|
92
|
+
def get_head(self, collection: str, record_id: str) -> dict | None: ...
|
|
93
|
+
def query_history(self, *, collection: str | None = None, record_id: str | None = None,
|
|
94
|
+
ref: str | None = None,
|
|
95
|
+
as_of_recorded: datetime | None = None, as_of_valid: datetime | None = None,
|
|
96
|
+
tag: str | None = None, git_sha: str | None = None,
|
|
97
|
+
limit: int | None = None, order: str = "desc",
|
|
98
|
+
with_doc: bool = False) -> list[dict]: ...
|
|
99
|
+
def list_tags(self) -> list[dict]: ...
|
|
100
|
+
|
|
101
|
+
# optional typed sidecar refs (branches, branch commits, PRs)
|
|
102
|
+
def put_ref(self, doc: dict) -> None: ...
|
|
103
|
+
def get_ref(self, ref_type: str, ref_id: str) -> dict | None: ...
|
|
104
|
+
def list_refs(self, ref_type: str, **filters) -> list[dict]: ...
|
|
105
|
+
def delete_ref(self, ref_type: str, ref_id: str) -> None: ...
|
|
106
|
+
|
|
107
|
+
# the one atomic mutation
|
|
108
|
+
def apply(self, *, collection: str, record_id: str, new_doc: dict | None, entry: dict,
|
|
109
|
+
expected_head_oid: str | None, expected_live_oid: str | None = None,
|
|
110
|
+
make_head: bool = True, seed_missing: bool = False) -> ApplyResult: ...
|
|
111
|
+
|
|
112
|
+
# labels
|
|
113
|
+
def add_tag(self, *, collection: str, record_id: str, seq: int, tag: str) -> None: ...
|
|
114
|
+
def remove_tag(self, *, collection: str, record_id: str, seq: int, tag: str) -> None: ...
|
|
115
|
+
|
|
116
|
+
# crash recovery / non-atomic fallback
|
|
117
|
+
def list_pending(self) -> list[dict]: ...
|
|
118
|
+
def reconcile(self) -> ReconcileReport: ...
|
|
119
|
+
|
|
120
|
+
# meta
|
|
121
|
+
def ensure_schema(self) -> None: ...
|
|
122
|
+
def check_runtime_invariant(self, collection: str | None = None) -> list[str]: ...
|
|
123
|
+
def check_atomicity_scope(self) -> AtomicityReport: ...
|
|
124
|
+
def backend_name(self) -> str: ...
|
|
125
|
+
def supports_transactions(self) -> bool: ...
|
|
126
|
+
def authenticated_principal(self) -> str | None: ...
|
|
127
|
+
def now(self) -> datetime: ...
|