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.
- contractforge_core/__init__.py +135 -0
- contractforge_core/adapters/__init__.py +15 -0
- contractforge_core/adapters/base/__init__.py +10 -0
- contractforge_core/adapters/base/generic.py +62 -0
- contractforge_core/adapters/base/protocol.py +29 -0
- contractforge_core/capabilities/__init__.py +4 -0
- contractforge_core/capabilities/models.py +29 -0
- contractforge_core/capabilities/native.py +53 -0
- contractforge_core/cli.py +96 -0
- contractforge_core/cli_connectors.py +29 -0
- contractforge_core/cli_contracts.py +178 -0
- contractforge_core/cli_init.py +117 -0
- contractforge_core/cli_io.py +22 -0
- contractforge_core/config.py +164 -0
- contractforge_core/connectors/__init__.py +140 -0
- contractforge_core/connectors/api/__init__.py +17 -0
- contractforge_core/connectors/api/rest/__init__.py +51 -0
- contractforge_core/connectors/api/rest/auth.py +104 -0
- contractforge_core/connectors/api/rest/pagination.py +92 -0
- contractforge_core/connectors/api/rest/reader.py +211 -0
- contractforge_core/connectors/api/rest/retry.py +21 -0
- contractforge_core/connectors/api/rest/safety.py +78 -0
- contractforge_core/connectors/api/rest/source.py +43 -0
- contractforge_core/connectors/api/rest/transport.py +15 -0
- contractforge_core/connectors/catalog/__init__.py +27 -0
- contractforge_core/connectors/catalog/catalog/__init__.py +29 -0
- contractforge_core/connectors/catalog/catalog/source.py +39 -0
- contractforge_core/connectors/catalog/catalog/table_refs.py +83 -0
- contractforge_core/connectors/databases/__init__.py +21 -0
- contractforge_core/connectors/databases/jdbc/__init__.py +23 -0
- contractforge_core/connectors/databases/jdbc/rds_iam.py +98 -0
- contractforge_core/connectors/databases/jdbc/source.py +161 -0
- contractforge_core/connectors/files/__init__.py +21 -0
- contractforge_core/connectors/files/files/__init__.py +21 -0
- contractforge_core/connectors/files/files/source.py +52 -0
- contractforge_core/connectors/http_files/__init__.py +27 -0
- contractforge_core/connectors/http_files/http_file/__init__.py +29 -0
- contractforge_core/connectors/http_files/http_file/reader.py +104 -0
- contractforge_core/connectors/http_files/http_file/retry.py +22 -0
- contractforge_core/connectors/http_files/http_file/safety.py +70 -0
- contractforge_core/connectors/http_files/http_file/source.py +82 -0
- contractforge_core/connectors/metadata.py +216 -0
- contractforge_core/connectors/native_passthrough/__init__.py +13 -0
- contractforge_core/connectors/native_passthrough/native_passthrough/__init__.py +13 -0
- contractforge_core/connectors/native_passthrough/native_passthrough/source.py +35 -0
- contractforge_core/connectors/registry.py +69 -0
- contractforge_core/connectors/sharing/__init__.py +8 -0
- contractforge_core/connectors/sharing/delta_share/__init__.py +8 -0
- contractforge_core/connectors/sharing/delta_share/source.py +22 -0
- contractforge_core/connectors/streams/__init__.py +25 -0
- contractforge_core/connectors/streams/eventhubs/__init__.py +17 -0
- contractforge_core/connectors/streams/eventhubs/source.py +44 -0
- contractforge_core/connectors/streams/kafka/__init__.py +17 -0
- contractforge_core/connectors/streams/kafka/source.py +53 -0
- contractforge_core/connectors/streams/source.py +40 -0
- contractforge_core/contracts/__init__.py +131 -0
- contractforge_core/contracts/access.py +161 -0
- contractforge_core/contracts/annotations.py +162 -0
- contractforge_core/contracts/base.py +85 -0
- contractforge_core/contracts/bundle.py +327 -0
- contractforge_core/contracts/environment.py +80 -0
- contractforge_core/contracts/execution.py +65 -0
- contractforge_core/contracts/governance.py +47 -0
- contractforge_core/contracts/governance_common.py +17 -0
- contractforge_core/contracts/naming.py +40 -0
- contractforge_core/contracts/normalize.py +129 -0
- contractforge_core/contracts/operations.py +80 -0
- contractforge_core/contracts/plan_validation.py +25 -0
- contractforge_core/contracts/quality.py +113 -0
- contractforge_core/contracts/root.py +179 -0
- contractforge_core/contracts/schema.py +65 -0
- contractforge_core/contracts/shape_validation.py +127 -0
- contractforge_core/contracts/source.py +61 -0
- contractforge_core/contracts/source_connector.py +115 -0
- contractforge_core/contracts/source_generic.py +122 -0
- contractforge_core/contracts/source_portability.py +88 -0
- contractforge_core/contracts/source_validation.py +230 -0
- contractforge_core/contracts/targeting.py +31 -0
- contractforge_core/contracts/transform.py +174 -0
- contractforge_core/diagnostics/__init__.py +5 -0
- contractforge_core/diagnostics/models.py +15 -0
- contractforge_core/errors.py +62 -0
- contractforge_core/evidence/__init__.py +43 -0
- contractforge_core/evidence/control_tables.py +147 -0
- contractforge_core/evidence/models.py +42 -0
- contractforge_core/evidence/records.py +101 -0
- contractforge_core/execution/__init__.py +27 -0
- contractforge_core/execution/results.py +18 -0
- contractforge_core/execution/strategy.py +27 -0
- contractforge_core/execution/windows.py +117 -0
- contractforge_core/execution/write_modes.py +32 -0
- contractforge_core/metrics/__init__.py +5 -0
- contractforge_core/metrics/write.py +25 -0
- contractforge_core/naming.py +144 -0
- contractforge_core/normalization/__init__.py +19 -0
- contractforge_core/normalization/common.py +26 -0
- contractforge_core/normalization/intents.py +111 -0
- contractforge_core/normalization/quality.py +100 -0
- contractforge_core/parity/__init__.py +5 -0
- contractforge_core/parity/models.py +53 -0
- contractforge_core/partitioning/__init__.py +5 -0
- contractforge_core/partitioning/predicates.py +15 -0
- contractforge_core/planner/__init__.py +18 -0
- contractforge_core/planner/governance_checks.py +50 -0
- contractforge_core/planner/matcher.py +60 -0
- contractforge_core/planner/plan_builder.py +29 -0
- contractforge_core/planner/result.py +47 -0
- contractforge_core/planner/semantic_checks.py +56 -0
- contractforge_core/planner/write_checks.py +156 -0
- contractforge_core/portability.py +53 -0
- contractforge_core/preparation/__init__.py +27 -0
- contractforge_core/preparation/staging.py +129 -0
- contractforge_core/project.py +101 -0
- contractforge_core/quality/__init__.py +24 -0
- contractforge_core/quality/results.py +73 -0
- contractforge_core/quality/rules.py +12 -0
- contractforge_core/reporting/__init__.py +5 -0
- contractforge_core/reporting/models.py +13 -0
- contractforge_core/results.py +27 -0
- contractforge_core/runtime/__init__.py +5 -0
- contractforge_core/runtime/models.py +38 -0
- contractforge_core/schema/__init__.py +13 -0
- contractforge_core/schema/diff.py +122 -0
- contractforge_core/schema/policy.py +25 -0
- contractforge_core/security/__init__.py +7 -0
- contractforge_core/security/redaction.py +73 -0
- contractforge_core/semantic/__init__.py +29 -0
- contractforge_core/semantic/models.py +108 -0
- contractforge_core/watermark.py +65 -0
- contractforge_core-0.1.0.dist-info/METADATA +374 -0
- contractforge_core-0.1.0.dist-info/RECORD +134 -0
- contractforge_core-0.1.0.dist-info/WHEEL +4 -0
- contractforge_core-0.1.0.dist-info/entry_points.txt +2 -0
- 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)
|