contractforge-core 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.
Files changed (134) hide show
  1. contractforge_core/__init__.py +135 -0
  2. contractforge_core/adapters/__init__.py +15 -0
  3. contractforge_core/adapters/base/__init__.py +10 -0
  4. contractforge_core/adapters/base/generic.py +62 -0
  5. contractforge_core/adapters/base/protocol.py +29 -0
  6. contractforge_core/capabilities/__init__.py +4 -0
  7. contractforge_core/capabilities/models.py +29 -0
  8. contractforge_core/capabilities/native.py +53 -0
  9. contractforge_core/cli.py +96 -0
  10. contractforge_core/cli_connectors.py +29 -0
  11. contractforge_core/cli_contracts.py +178 -0
  12. contractforge_core/cli_init.py +117 -0
  13. contractforge_core/cli_io.py +22 -0
  14. contractforge_core/config.py +164 -0
  15. contractforge_core/connectors/__init__.py +140 -0
  16. contractforge_core/connectors/api/__init__.py +17 -0
  17. contractforge_core/connectors/api/rest/__init__.py +51 -0
  18. contractforge_core/connectors/api/rest/auth.py +104 -0
  19. contractforge_core/connectors/api/rest/pagination.py +92 -0
  20. contractforge_core/connectors/api/rest/reader.py +211 -0
  21. contractforge_core/connectors/api/rest/retry.py +21 -0
  22. contractforge_core/connectors/api/rest/safety.py +78 -0
  23. contractforge_core/connectors/api/rest/source.py +43 -0
  24. contractforge_core/connectors/api/rest/transport.py +15 -0
  25. contractforge_core/connectors/catalog/__init__.py +27 -0
  26. contractforge_core/connectors/catalog/catalog/__init__.py +29 -0
  27. contractforge_core/connectors/catalog/catalog/source.py +39 -0
  28. contractforge_core/connectors/catalog/catalog/table_refs.py +83 -0
  29. contractforge_core/connectors/databases/__init__.py +21 -0
  30. contractforge_core/connectors/databases/jdbc/__init__.py +23 -0
  31. contractforge_core/connectors/databases/jdbc/rds_iam.py +98 -0
  32. contractforge_core/connectors/databases/jdbc/source.py +161 -0
  33. contractforge_core/connectors/files/__init__.py +21 -0
  34. contractforge_core/connectors/files/files/__init__.py +21 -0
  35. contractforge_core/connectors/files/files/source.py +52 -0
  36. contractforge_core/connectors/http_files/__init__.py +27 -0
  37. contractforge_core/connectors/http_files/http_file/__init__.py +29 -0
  38. contractforge_core/connectors/http_files/http_file/reader.py +104 -0
  39. contractforge_core/connectors/http_files/http_file/retry.py +22 -0
  40. contractforge_core/connectors/http_files/http_file/safety.py +70 -0
  41. contractforge_core/connectors/http_files/http_file/source.py +82 -0
  42. contractforge_core/connectors/metadata.py +216 -0
  43. contractforge_core/connectors/native_passthrough/__init__.py +13 -0
  44. contractforge_core/connectors/native_passthrough/native_passthrough/__init__.py +13 -0
  45. contractforge_core/connectors/native_passthrough/native_passthrough/source.py +35 -0
  46. contractforge_core/connectors/registry.py +69 -0
  47. contractforge_core/connectors/sharing/__init__.py +8 -0
  48. contractforge_core/connectors/sharing/delta_share/__init__.py +8 -0
  49. contractforge_core/connectors/sharing/delta_share/source.py +22 -0
  50. contractforge_core/connectors/streams/__init__.py +25 -0
  51. contractforge_core/connectors/streams/eventhubs/__init__.py +17 -0
  52. contractforge_core/connectors/streams/eventhubs/source.py +44 -0
  53. contractforge_core/connectors/streams/kafka/__init__.py +17 -0
  54. contractforge_core/connectors/streams/kafka/source.py +53 -0
  55. contractforge_core/connectors/streams/source.py +40 -0
  56. contractforge_core/contracts/__init__.py +131 -0
  57. contractforge_core/contracts/access.py +161 -0
  58. contractforge_core/contracts/annotations.py +162 -0
  59. contractforge_core/contracts/base.py +85 -0
  60. contractforge_core/contracts/bundle.py +327 -0
  61. contractforge_core/contracts/environment.py +80 -0
  62. contractforge_core/contracts/execution.py +65 -0
  63. contractforge_core/contracts/governance.py +47 -0
  64. contractforge_core/contracts/governance_common.py +17 -0
  65. contractforge_core/contracts/naming.py +40 -0
  66. contractforge_core/contracts/normalize.py +129 -0
  67. contractforge_core/contracts/operations.py +80 -0
  68. contractforge_core/contracts/plan_validation.py +25 -0
  69. contractforge_core/contracts/quality.py +113 -0
  70. contractforge_core/contracts/root.py +179 -0
  71. contractforge_core/contracts/schema.py +65 -0
  72. contractforge_core/contracts/shape_validation.py +127 -0
  73. contractforge_core/contracts/source.py +61 -0
  74. contractforge_core/contracts/source_connector.py +115 -0
  75. contractforge_core/contracts/source_generic.py +122 -0
  76. contractforge_core/contracts/source_portability.py +88 -0
  77. contractforge_core/contracts/source_validation.py +230 -0
  78. contractforge_core/contracts/targeting.py +31 -0
  79. contractforge_core/contracts/transform.py +174 -0
  80. contractforge_core/diagnostics/__init__.py +5 -0
  81. contractforge_core/diagnostics/models.py +15 -0
  82. contractforge_core/errors.py +62 -0
  83. contractforge_core/evidence/__init__.py +43 -0
  84. contractforge_core/evidence/control_tables.py +147 -0
  85. contractforge_core/evidence/models.py +42 -0
  86. contractforge_core/evidence/records.py +101 -0
  87. contractforge_core/execution/__init__.py +27 -0
  88. contractforge_core/execution/results.py +18 -0
  89. contractforge_core/execution/strategy.py +27 -0
  90. contractforge_core/execution/windows.py +117 -0
  91. contractforge_core/execution/write_modes.py +32 -0
  92. contractforge_core/metrics/__init__.py +5 -0
  93. contractforge_core/metrics/write.py +25 -0
  94. contractforge_core/naming.py +144 -0
  95. contractforge_core/normalization/__init__.py +19 -0
  96. contractforge_core/normalization/common.py +26 -0
  97. contractforge_core/normalization/intents.py +111 -0
  98. contractforge_core/normalization/quality.py +100 -0
  99. contractforge_core/parity/__init__.py +5 -0
  100. contractforge_core/parity/models.py +53 -0
  101. contractforge_core/partitioning/__init__.py +5 -0
  102. contractforge_core/partitioning/predicates.py +15 -0
  103. contractforge_core/planner/__init__.py +18 -0
  104. contractforge_core/planner/governance_checks.py +50 -0
  105. contractforge_core/planner/matcher.py +60 -0
  106. contractforge_core/planner/plan_builder.py +29 -0
  107. contractforge_core/planner/result.py +47 -0
  108. contractforge_core/planner/semantic_checks.py +56 -0
  109. contractforge_core/planner/write_checks.py +156 -0
  110. contractforge_core/portability.py +53 -0
  111. contractforge_core/preparation/__init__.py +27 -0
  112. contractforge_core/preparation/staging.py +129 -0
  113. contractforge_core/project.py +101 -0
  114. contractforge_core/quality/__init__.py +24 -0
  115. contractforge_core/quality/results.py +73 -0
  116. contractforge_core/quality/rules.py +12 -0
  117. contractforge_core/reporting/__init__.py +5 -0
  118. contractforge_core/reporting/models.py +13 -0
  119. contractforge_core/results.py +27 -0
  120. contractforge_core/runtime/__init__.py +5 -0
  121. contractforge_core/runtime/models.py +38 -0
  122. contractforge_core/schema/__init__.py +13 -0
  123. contractforge_core/schema/diff.py +122 -0
  124. contractforge_core/schema/policy.py +25 -0
  125. contractforge_core/security/__init__.py +7 -0
  126. contractforge_core/security/redaction.py +73 -0
  127. contractforge_core/semantic/__init__.py +29 -0
  128. contractforge_core/semantic/models.py +108 -0
  129. contractforge_core/watermark.py +65 -0
  130. contractforge_core-0.1.0.dist-info/METADATA +374 -0
  131. contractforge_core-0.1.0.dist-info/RECORD +134 -0
  132. contractforge_core-0.1.0.dist-info/WHEEL +4 -0
  133. contractforge_core-0.1.0.dist-info/entry_points.txt +2 -0
  134. contractforge_core-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,135 @@
1
+ """Platform-neutral semantic core for contract-first ingestion."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version as _pkg_version
4
+
5
+ try:
6
+ __version__ = _pkg_version("contractforge-core")
7
+ except PackageNotFoundError: # pragma: no cover - editable installs without metadata
8
+ __version__ = "0.0.0+unknown"
9
+
10
+ from contractforge_core.capabilities import CapabilityEvidence, NativeCapability, PlatformCapabilities, capability
11
+ from contractforge_core.connectors import (
12
+ diagnose_source_connectors,
13
+ list_source_connector_details,
14
+ source_connector_details,
15
+ )
16
+ from contractforge_core.contracts import (
17
+ AccessContractModel,
18
+ AccessGrantContractModel,
19
+ AnnotationsContractModel,
20
+ ColumnAnnotationsContractModel,
21
+ ColumnMaskContractModel,
22
+ ContractBundle,
23
+ ConnectorSourceContract,
24
+ DeduplicateContractModel,
25
+ DeprecatedContractModel,
26
+ ExecutionCatchupContractModel,
27
+ ExecutionContractModel,
28
+ ExecutionWindowContractModel,
29
+ GenericSourceContract,
30
+ OperationsContractModel,
31
+ PiiContractModel,
32
+ QualityExpressionContractModel,
33
+ QualityRulesContractModel,
34
+ RowFilterContractModel,
35
+ ShapeArrayContractModel,
36
+ ShapeColumnContractModel,
37
+ ShapeContractModel,
38
+ ShapeFlattenContractModel,
39
+ ShapeJsonContractModel,
40
+ ShapeZipArraysContractModel,
41
+ StandardizeColumnContractModel,
42
+ TableAnnotationsContractModel,
43
+ TransformContractModel,
44
+ contract_model_schemas,
45
+ load_contract_bundle,
46
+ semantic_contract_from_mapping,
47
+ target_full_table_name,
48
+ target_schema_name,
49
+ validate_plan_shape,
50
+ yaml_schema,
51
+ )
52
+ from contractforge_core.errors import ContractForgeExecutionError, raise_for_failure_result
53
+ from contractforge_core.execution import ExecutionWindow
54
+ from contractforge_core.naming import DerivedNames, NamingConfig, derive_names, normalize_identifier, normalize_slug
55
+ from contractforge_core.planner.result import PlanningResult, PlanningStatus
56
+ from contractforge_core.portability import classify_write_mode
57
+ from contractforge_core.project import (
58
+ ProjectScheduleIntent,
59
+ StandardCron,
60
+ adapter_scheduling,
61
+ parse_standard_cron,
62
+ project_schedule_intent,
63
+ quartz_cron_expression,
64
+ )
65
+ from contractforge_core.security import redact_secrets, redact_text, redact_value
66
+ from contractforge_core.semantic.models import SemanticContract
67
+ from contractforge_core.watermark import decode_watermark_value, encode_watermark_values, extract_watermark_field_value
68
+
69
+ __all__ = [
70
+ "AccessContractModel",
71
+ "AccessGrantContractModel",
72
+ "AnnotationsContractModel",
73
+ "PlatformCapabilities",
74
+ "PlanningResult",
75
+ "PlanningStatus",
76
+ "ProjectScheduleIntent",
77
+ "StandardCron",
78
+ "SemanticContract",
79
+ "ContractForgeExecutionError",
80
+ "ContractBundle",
81
+ "CapabilityEvidence",
82
+ "ColumnAnnotationsContractModel",
83
+ "ColumnMaskContractModel",
84
+ "ConnectorSourceContract",
85
+ "DerivedNames",
86
+ "DeduplicateContractModel",
87
+ "DeprecatedContractModel",
88
+ "ExecutionCatchupContractModel",
89
+ "ExecutionContractModel",
90
+ "ExecutionWindow",
91
+ "ExecutionWindowContractModel",
92
+ "GenericSourceContract",
93
+ "NamingConfig",
94
+ "NativeCapability",
95
+ "OperationsContractModel",
96
+ "PiiContractModel",
97
+ "QualityExpressionContractModel",
98
+ "QualityRulesContractModel",
99
+ "RowFilterContractModel",
100
+ "ShapeArrayContractModel",
101
+ "ShapeColumnContractModel",
102
+ "ShapeContractModel",
103
+ "ShapeFlattenContractModel",
104
+ "ShapeJsonContractModel",
105
+ "ShapeZipArraysContractModel",
106
+ "StandardizeColumnContractModel",
107
+ "TableAnnotationsContractModel",
108
+ "TransformContractModel",
109
+ "capability",
110
+ "adapter_scheduling",
111
+ "classify_write_mode",
112
+ "contract_model_schemas",
113
+ "diagnose_source_connectors",
114
+ "derive_names",
115
+ "list_source_connector_details",
116
+ "load_contract_bundle",
117
+ "normalize_identifier",
118
+ "normalize_slug",
119
+ "project_schedule_intent",
120
+ "parse_standard_cron",
121
+ "quartz_cron_expression",
122
+ "redact_text",
123
+ "redact_secrets",
124
+ "redact_value",
125
+ "decode_watermark_value",
126
+ "encode_watermark_values",
127
+ "extract_watermark_field_value",
128
+ "raise_for_failure_result",
129
+ "semantic_contract_from_mapping",
130
+ "source_connector_details",
131
+ "target_full_table_name",
132
+ "target_schema_name",
133
+ "validate_plan_shape",
134
+ "yaml_schema",
135
+ ]
@@ -0,0 +1,15 @@
1
+ from contractforge_core.adapters.base import (
2
+ CapabilitiesAdapter,
3
+ PlatformAdapter,
4
+ RenderedArtifacts,
5
+ append_only_adapter,
6
+ full_feature_adapter,
7
+ )
8
+
9
+ __all__ = [
10
+ "CapabilitiesAdapter",
11
+ "PlatformAdapter",
12
+ "RenderedArtifacts",
13
+ "append_only_adapter",
14
+ "full_feature_adapter",
15
+ ]
@@ -0,0 +1,10 @@
1
+ from contractforge_core.adapters.base.generic import CapabilitiesAdapter, append_only_adapter, full_feature_adapter
2
+ from contractforge_core.adapters.base.protocol import PlatformAdapter, RenderedArtifacts
3
+
4
+ __all__ = [
5
+ "CapabilitiesAdapter",
6
+ "PlatformAdapter",
7
+ "RenderedArtifacts",
8
+ "append_only_adapter",
9
+ "full_feature_adapter",
10
+ ]
@@ -0,0 +1,62 @@
1
+ """Generic in-memory adapters useful for tests and dry planning."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from contractforge_core.adapters.base.protocol import RenderedArtifacts
8
+ from contractforge_core.capabilities.models import PlatformCapabilities
9
+ from contractforge_core.planner import ExecutionPlan, PlanningResult, plan_contract
10
+ from contractforge_core.semantic.models import SemanticContract
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class CapabilitiesAdapter:
15
+ """Small adapter backed only by a capability declaration."""
16
+
17
+ name: str
18
+ declared_capabilities: PlatformCapabilities
19
+
20
+ def capabilities(self) -> PlatformCapabilities:
21
+ return self.declared_capabilities
22
+
23
+ def plan(self, contract: SemanticContract) -> PlanningResult:
24
+ return plan_contract(contract, self.declared_capabilities)
25
+
26
+ def render(self, plan: ExecutionPlan) -> RenderedArtifacts:
27
+ lines = [
28
+ f"# Execution plan for {plan.platform}",
29
+ "",
30
+ "| Step | Intent |",
31
+ "| --- | --- |",
32
+ ]
33
+ lines.extend(f"| {step.name} | {step.intent} |" for step in plan.steps)
34
+ return RenderedArtifacts(artifacts={"review.md": "\n".join(lines) + "\n"})
35
+
36
+
37
+ def full_feature_adapter() -> CapabilitiesAdapter:
38
+ capabilities = PlatformCapabilities(
39
+ platform="full-feature-generic",
40
+ supports_append=True,
41
+ supports_overwrite=True,
42
+ supports_merge=True,
43
+ supports_hash_diff=True,
44
+ supports_scd2=True,
45
+ supports_snapshot_soft_delete=True,
46
+ supports_schema_evolution=True,
47
+ supports_row_filters=True,
48
+ supports_column_masks=True,
49
+ supports_available_now_streaming=True,
50
+ evidence_stores=("audit_tables",),
51
+ )
52
+ return CapabilitiesAdapter(name=capabilities.platform, declared_capabilities=capabilities)
53
+
54
+
55
+ def append_only_adapter() -> CapabilitiesAdapter:
56
+ capabilities = PlatformCapabilities(
57
+ platform="append-only-generic",
58
+ supports_append=True,
59
+ evidence_stores=("audit_files",),
60
+ )
61
+ return CapabilitiesAdapter(name=capabilities.platform, declared_capabilities=capabilities)
62
+
@@ -0,0 +1,29 @@
1
+ """Base adapter protocol for platform-specific implementations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Protocol
7
+
8
+ from contractforge_core.capabilities.models import PlatformCapabilities
9
+ from contractforge_core.planner.result import ExecutionPlan, PlanningResult
10
+ from contractforge_core.semantic.models import SemanticContract
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class RenderedArtifacts:
15
+ artifacts: dict[str, str]
16
+
17
+
18
+ class PlatformAdapter(Protocol):
19
+ name: str
20
+
21
+ def capabilities(self) -> PlatformCapabilities:
22
+ ...
23
+
24
+ def plan(self, contract: SemanticContract) -> PlanningResult:
25
+ ...
26
+
27
+ def render(self, plan: ExecutionPlan) -> RenderedArtifacts:
28
+ ...
29
+
@@ -0,0 +1,4 @@
1
+ from contractforge_core.capabilities.models import PlatformCapabilities
2
+ from contractforge_core.capabilities.native import CapabilityEvidence, CapabilityStatus, NativeCapability, capability
3
+
4
+ __all__ = ["CapabilityEvidence", "CapabilityStatus", "NativeCapability", "PlatformCapabilities", "capability"]
@@ -0,0 +1,29 @@
1
+ """Platform capability declarations consumed by the semantic planner."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class PlatformCapabilities:
10
+ platform: str
11
+ supports_append: bool = False
12
+ supports_overwrite: bool = False
13
+ supports_merge: bool = False
14
+ supports_hash_diff: bool = False
15
+ supports_scd2: bool = False
16
+ supports_snapshot_soft_delete: bool = False
17
+ supports_schema_evolution: bool = False
18
+ supports_row_filters: bool = False
19
+ supports_column_masks: bool = False
20
+ supports_available_now_streaming: bool = False
21
+ supports_required_columns_quality: bool = True
22
+ supports_unique_key_quality: bool = True
23
+ supports_max_null_ratio_quality: bool = True
24
+ supports_expression_quality: bool = False
25
+ supports_shape: bool = False
26
+ supports_transform: bool = False
27
+ evidence_stores: tuple[str, ...] = ()
28
+ review_required_semantics: tuple[str, ...] = ()
29
+ supported_custom_write_modes: tuple[str, ...] = ()
@@ -0,0 +1,53 @@
1
+ """Platform-neutral native capability evidence models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Literal
7
+
8
+ CapabilityStatus = Literal["supported", "unsupported", "unknown"]
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class CapabilityEvidence:
13
+ source: str
14
+ message: str
15
+ value: str | None = None
16
+
17
+ def as_dict(self) -> dict[str, str]:
18
+ payload = {"source": self.source, "message": self.message, "value": self.value}
19
+ return {key: value for key, value in payload.items() if value is not None}
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class NativeCapability:
24
+ name: str
25
+ status: CapabilityStatus
26
+ reason: str
27
+ evidence: tuple[CapabilityEvidence, ...] = ()
28
+ requires: tuple[str, ...] = ()
29
+
30
+ @property
31
+ def supported(self) -> bool:
32
+ return self.status == "supported"
33
+
34
+ def as_dict(self) -> dict[str, Any]:
35
+ return {
36
+ "name": self.name,
37
+ "status": self.status,
38
+ "supported": self.supported,
39
+ "reason": self.reason,
40
+ "requires": list(self.requires),
41
+ "evidence": [item.as_dict() for item in self.evidence],
42
+ }
43
+
44
+
45
+ def capability(
46
+ name: str,
47
+ status: CapabilityStatus,
48
+ reason: str,
49
+ *,
50
+ evidence: tuple[CapabilityEvidence, ...] = (),
51
+ requires: tuple[str, ...] = (),
52
+ ) -> NativeCapability:
53
+ return NativeCapability(name=name, status=status, reason=reason, evidence=evidence, requires=requires)
@@ -0,0 +1,96 @@
1
+ """Core ContractForge CLI for platform-neutral contract utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from contractforge_core.cli_connectors import handle_connector_command
11
+ from contractforge_core.cli_contracts import handle_contract_command
12
+ from contractforge_core.config import VALID_SCHEMA_POLICIES, VALID_WRITE_MODES
13
+
14
+
15
+ def build_parser() -> argparse.ArgumentParser:
16
+ parser = argparse.ArgumentParser(prog="contractforge")
17
+ sub = parser.add_subparsers(dest="command", required=True)
18
+ _add_contract_parsers(sub)
19
+ _add_connector_parser(sub)
20
+ return parser
21
+
22
+
23
+ def main(argv: list[str] | None = None) -> int:
24
+ parser = build_parser()
25
+ args = parser.parse_args(argv)
26
+ try:
27
+ return _dispatch(args)
28
+ except Exception as exc:
29
+ print(f"ERROR: {exc}", file=sys.stderr)
30
+ return 1
31
+
32
+
33
+ def _dispatch(args: argparse.Namespace) -> int:
34
+ contract_result = handle_contract_command(args)
35
+ if contract_result is not None:
36
+ return contract_result
37
+ connector_result = handle_connector_command(args)
38
+ if connector_result is not None:
39
+ return connector_result
40
+ raise ValueError(f"unsupported command: {args.command}")
41
+
42
+
43
+ def _add_contract_parsers(subparsers: Any) -> None:
44
+ validate = subparsers.add_parser("validate", help="Validate contract files without executing a platform")
45
+ validate.add_argument("paths", nargs="+", type=Path)
46
+ validate.add_argument("--indent", type=int, default=2)
47
+ validate_bundle = subparsers.add_parser("validate-bundle", help="Validate split contract bundles")
48
+ validate_bundle.add_argument("paths", nargs="+", type=Path)
49
+ validate_bundle.add_argument("--indent", type=int, default=2)
50
+ validate_project = subparsers.add_parser("validate-project", help="Discover and validate contracts recursively")
51
+ validate_project.add_argument("paths", nargs="+", type=Path)
52
+ validate_project.add_argument("--indent", type=int, default=2)
53
+ schema = subparsers.add_parser("schema", help="Print generated core contract JSON Schemas")
54
+ schema.add_argument("--indent", type=int, default=2)
55
+ init = subparsers.add_parser("init", help="Generate a starter split ContractForge contract")
56
+ init.add_argument("--output", required=True, type=Path)
57
+ init.add_argument("--source", required=True)
58
+ init.add_argument("--target-table", required=True)
59
+ init.add_argument("--catalog", default="main")
60
+ init.add_argument("--layer", default="bronze")
61
+ init.add_argument("--target-schema")
62
+ init.add_argument("--adapter", default="generic")
63
+ init.add_argument("--mode", default="scd0_append", choices=sorted(VALID_WRITE_MODES))
64
+ init.add_argument("--schema-policy", default="additive_only", choices=sorted(VALID_SCHEMA_POLICIES))
65
+ init.add_argument("--merge-keys")
66
+ init.add_argument("--hash-keys")
67
+ init.add_argument("--watermark-columns")
68
+ init.add_argument("--description")
69
+ init.add_argument("--domain")
70
+ init.add_argument("--owner")
71
+ init.add_argument("--technical-owner")
72
+ init.add_argument("--support-group")
73
+ init.add_argument("--criticality", default="medium")
74
+ init.add_argument("--expected-frequency", default="daily")
75
+ init.add_argument("--freshness-sla-minutes", type=int, default=1440)
76
+ init.add_argument("--runbook-url")
77
+ init.add_argument("--access-principal")
78
+ init.add_argument("--force", action="store_true")
79
+ init.add_argument("--indent", type=int, default=2)
80
+
81
+
82
+ def _add_connector_parser(subparsers: Any) -> None:
83
+ parser = subparsers.add_parser("connectors", help="List or inspect source connector portability")
84
+ sub = parser.add_subparsers(dest="connector_command", required=True)
85
+ list_parser = sub.add_parser("list", help="List known portable source types")
86
+ list_parser.add_argument("--indent", type=int, default=2)
87
+ show = sub.add_parser("show", help="Show connector details")
88
+ show.add_argument("names", nargs="+")
89
+ show.add_argument("--indent", type=int, default=2)
90
+ doctor = sub.add_parser("doctor", help="Diagnose connector portability without opening external connections")
91
+ doctor.add_argument("names", nargs="*")
92
+ doctor.add_argument("--indent", type=int, default=2)
93
+
94
+
95
+ if __name__ == "__main__": # pragma: no cover
96
+ raise SystemExit(main())
@@ -0,0 +1,29 @@
1
+ """Connector catalog commands for the core ContractForge CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ from contractforge_core.connectors import (
9
+ diagnose_source_connectors,
10
+ list_source_connector_details,
11
+ source_connector_details,
12
+ )
13
+
14
+
15
+ def handle_connector_command(args: Any) -> int | None:
16
+ if args.command != "connectors":
17
+ return None
18
+ if args.connector_command == "list":
19
+ return _print(list_source_connector_details(), args.indent)
20
+ if args.connector_command == "show":
21
+ return _print([source_connector_details(name) for name in args.names], args.indent)
22
+ if args.connector_command == "doctor":
23
+ return _print({"status": "SUCCESS", "items": diagnose_source_connectors(args.names)}, args.indent)
24
+ raise ValueError(f"unsupported connectors command: {args.connector_command}")
25
+
26
+
27
+ def _print(payload: object, indent: int) -> int:
28
+ print(json.dumps(payload, indent=indent, sort_keys=True, default=str))
29
+ return 0
@@ -0,0 +1,178 @@
1
+ """Contract-oriented core CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from contractforge_core.cli_init import init_contract
10
+ from contractforge_core.cli_io import yaml_load
11
+ from contractforge_core.contracts import (
12
+ contract_model_schemas,
13
+ load_contract_bundle,
14
+ semantic_contract_from_mapping,
15
+ validate_source_semantics,
16
+ )
17
+
18
+ CONTRACT_SUFFIXES = {".json", ".yaml", ".yml"}
19
+ SPLIT_MARKERS = (".annotations.", ".operations.", ".access.", ".environment.")
20
+
21
+
22
+ def handle_contract_command(args: Any) -> int | None:
23
+ if args.command == "validate":
24
+ return validate_contracts(args.paths, indent=args.indent)
25
+ if args.command == "validate-bundle":
26
+ return validate_bundles(args.paths, indent=args.indent)
27
+ if args.command == "validate-project":
28
+ return validate_project(args.paths, indent=args.indent)
29
+ if args.command == "schema":
30
+ return _print(contract_model_schemas(), args.indent)
31
+ if args.command == "init":
32
+ return init_contract(args)
33
+ return None
34
+
35
+
36
+ def validate_contracts(paths: list[Path], *, indent: int) -> int:
37
+ items = []
38
+ for path in paths:
39
+ try:
40
+ contract = _load_mapping(path)
41
+ semantic = semantic_contract_from_mapping(contract)
42
+ _validate_source_if_mapping(contract.get("source"))
43
+ items.append({"path": str(path), "kind": "contract", "status": "SUCCESS", "target": semantic.target.name, "mode": semantic.write.mode})
44
+ except Exception as exc:
45
+ items.append({"path": str(path), "kind": "contract", "status": "FAILED", "error": str(exc)})
46
+ return _report(items, indent)
47
+
48
+
49
+ def validate_bundles(paths: list[Path], *, indent: int) -> int:
50
+ items = []
51
+ for path in paths:
52
+ try:
53
+ bundle = _load_bundle(path)
54
+ _validate_source_if_mapping(bundle.contract.get("source"))
55
+ items.append(
56
+ {
57
+ "path": str(path),
58
+ "kind": "bundle",
59
+ "status": "SUCCESS",
60
+ "target": bundle.semantic.target.name,
61
+ "mode": bundle.semantic.write.mode,
62
+ "split_files": dict(bundle.metadata.get("paths", {})),
63
+ }
64
+ )
65
+ except Exception as exc:
66
+ items.append({"path": str(path), "kind": "bundle", "status": "FAILED", "error": str(exc)})
67
+ return _report(items, indent)
68
+
69
+
70
+ def validate_project(paths: list[Path], *, indent: int) -> int:
71
+ items = []
72
+ for root in paths:
73
+ discovered = _discover_contracts(root)
74
+ if not discovered:
75
+ items.append({"path": str(root), "kind": "project", "status": "FAILED", "error": "no contracts found"})
76
+ continue
77
+ for kind, path in discovered:
78
+ items.extend(_validate_bundle_for_project(path) if kind == "bundle" else _validate_single_for_project(path))
79
+ return _report(items, indent)
80
+
81
+
82
+ def _validate_bundle_for_project(path: Path) -> list[dict[str, Any]]:
83
+ try:
84
+ bundle = _load_bundle(path)
85
+ _validate_source_if_mapping(bundle.contract.get("source"))
86
+ return [{"path": str(path), "kind": "bundle", "status": "SUCCESS", "target": bundle.semantic.target.name, "mode": bundle.semantic.write.mode}]
87
+ except Exception as exc:
88
+ return [{"path": str(path), "kind": "bundle", "status": "FAILED", "error": str(exc)}]
89
+
90
+
91
+ def _validate_single_for_project(path: Path) -> list[dict[str, Any]]:
92
+ try:
93
+ contract = _load_mapping(path)
94
+ semantic = semantic_contract_from_mapping(contract)
95
+ _validate_source_if_mapping(contract.get("source"))
96
+ return [{"path": str(path), "kind": "contract", "status": "SUCCESS", "target": semantic.target.name}]
97
+ except Exception as exc:
98
+ return [{"path": str(path), "kind": "contract", "status": "FAILED", "error": str(exc)}]
99
+
100
+
101
+ def _load_bundle(path: Path):
102
+ return load_contract_bundle(_bundle_base(path))
103
+
104
+
105
+ def _discover_contracts(root: Path) -> list[tuple[str, Path]]:
106
+ if root.is_file():
107
+ if _is_split_ingestion(root):
108
+ return [("bundle", root)]
109
+ return [("contract", root)] if _is_standalone_contract(root) else []
110
+ found = []
111
+ for path in sorted(root.rglob("*")):
112
+ if _is_discovery_ignored(path):
113
+ continue
114
+ if _is_split_ingestion(path):
115
+ found.append(("bundle", path))
116
+ elif _is_standalone_contract(path):
117
+ found.append(("contract", path))
118
+ return found
119
+
120
+
121
+ def _report(items: list[dict[str, Any]], indent: int) -> int:
122
+ failed = [item for item in items if item["status"] == "FAILED"]
123
+ payload = {"status": "FAILED" if failed else "SUCCESS", "total": len(items), "succeeded": len(items) - len(failed), "failed": len(failed), "items": items}
124
+ _print(payload, indent)
125
+ return 1 if failed else 0
126
+
127
+
128
+ def _load_mapping(path: Path) -> dict[str, Any]:
129
+ text = path.read_text(encoding="utf-8")
130
+ payload = json.loads(text) if path.suffix.lower() == ".json" else yaml_load(text)
131
+ if not isinstance(payload, dict):
132
+ raise ValueError(f"{path} must contain an object")
133
+ return payload
134
+
135
+
136
+ def _validate_source_if_mapping(source: object) -> None:
137
+ if isinstance(source, dict):
138
+ validate_source_semantics(source)
139
+
140
+
141
+ def _print(payload: object, indent: int) -> int:
142
+ print(json.dumps(payload, indent=indent, sort_keys=True, default=str))
143
+ return 0
144
+
145
+
146
+ def _base_output(path: Path) -> Path:
147
+ name = path.name
148
+ for suffix in (".ingestion.yaml", ".ingestion.yml", ".ingestion.json"):
149
+ if name.endswith(suffix):
150
+ return path.with_name(name[: -len(suffix)])
151
+ return path
152
+
153
+
154
+ def _bundle_base(path: Path) -> Path:
155
+ return _base_output(path) if _is_split_ingestion(path) else path
156
+
157
+
158
+ def _is_contract_file(path: Path) -> bool:
159
+ return path.is_file() and path.suffix.lower() in CONTRACT_SUFFIXES
160
+
161
+
162
+ def _is_split_ingestion(path: Path) -> bool:
163
+ return _is_contract_file(path) and ".ingestion." in path.name
164
+
165
+
166
+ def _is_standalone_contract(path: Path) -> bool:
167
+ if not _is_contract_file(path) or ".ingestion." in path.name or any(marker in path.name for marker in SPLIT_MARKERS):
168
+ return False
169
+ try:
170
+ payload = _load_mapping(path)
171
+ except Exception:
172
+ return False
173
+ return isinstance(payload.get("source"), dict) and isinstance(payload.get("target"), dict)
174
+
175
+
176
+ def _is_discovery_ignored(path: Path) -> bool:
177
+ ignored_dirs = {".databricks", ".git", ".venv", "__pycache__", "node_modules", "build", "dist"}
178
+ return any(part in ignored_dirs for part in path.parts)