dander-platform 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 (127) hide show
  1. dander/__init__.py +12 -0
  2. dander/_bigquery_retry.py +54 -0
  3. dander/bootstrap/__init__.py +37 -0
  4. dander/bootstrap/admin.py +276 -0
  5. dander/bootstrap/project.py +327 -0
  6. dander/bootstrap/terraform.py +356 -0
  7. dander/bootstrap/verify.py +986 -0
  8. dander/catalog/README.md +48 -0
  9. dander/catalog/__init__.py +42 -0
  10. dander/catalog/dataplex.py +210 -0
  11. dander/catalog/publisher.py +21 -0
  12. dander/catalog/registry.py +31 -0
  13. dander/catalog/spine.py +252 -0
  14. dander/catalog/store.py +213 -0
  15. dander/cli/__init__.py +1 -0
  16. dander/cli/main.py +1588 -0
  17. dander/concurrency.py +78 -0
  18. dander/core/__init__.py +1 -0
  19. dander/core/config.py +22 -0
  20. dander/core/interfaces.py +32 -0
  21. dander/dev/__init__.py +1 -0
  22. dander/dev/synthetic_vendor.py +242 -0
  23. dander/evidence/__init__.py +19 -0
  24. dander/evidence/models.py +139 -0
  25. dander/executor.py +289 -0
  26. dander/ingestion/README.md +30 -0
  27. dander/ingestion/__init__.py +44 -0
  28. dander/ingestion/config.py +43 -0
  29. dander/ingestion/dlt_backed.py +304 -0
  30. dander/ingestion/enterprise.py +196 -0
  31. dander/ingestion/pagination.py +157 -0
  32. dander/ingestion/source.py +362 -0
  33. dander/pipeline/README.md +491 -0
  34. dander/pipeline/__init__.py +102 -0
  35. dander/pipeline/compiler.py +519 -0
  36. dander/pipeline/errors.py +248 -0
  37. dander/pipeline/graph.py +1027 -0
  38. dander/pipeline/graph_ops.py +413 -0
  39. dander/pipeline/node_config.py +358 -0
  40. dander/pipeline/request_spec.py +301 -0
  41. dander/project/__init__.py +26 -0
  42. dander/project/config.py +235 -0
  43. dander/project/scaffold.py +93 -0
  44. dander/py.typed +0 -0
  45. dander/runtime.py +458 -0
  46. dander/sandbox.py +265 -0
  47. dander/schema.py +36 -0
  48. dander/security/__init__.py +35 -0
  49. dander/security/api_bearer.py +20 -0
  50. dander/security/api_key.py +27 -0
  51. dander/security/base.py +32 -0
  52. dander/security/no_auth.py +21 -0
  53. dander/security/oauth.py +157 -0
  54. dander/security/oauth1.py +104 -0
  55. dander/security/oauth_jwt.py +172 -0
  56. dander/security/secret_manager.py +136 -0
  57. dander/state/__init__.py +37 -0
  58. dander/state/lease.py +406 -0
  59. dander/state/run_history.py +472 -0
  60. dander/state/watermark.py +259 -0
  61. dander/templates/project/.dockerignore +7 -0
  62. dander/templates/project/.gitignore +6 -0
  63. dander/templates/project/Dockerfile +19 -0
  64. dander/templates/project/README.md +22 -0
  65. dander/templates/project/connectors/greenhouse_job_board.yaml +56 -0
  66. dander/templates/project/dander.yaml +25 -0
  67. dander/templates/project/infra/.terraform.lock.hcl +64 -0
  68. dander/templates/project/infra/README.md +131 -0
  69. dander/templates/project/infra/__init__.py +1 -0
  70. dander/templates/project/infra/bootstrap-admin/.terraform.lock.hcl +22 -0
  71. dander/templates/project/infra/bootstrap-admin/README.md +74 -0
  72. dander/templates/project/infra/bootstrap-admin/main.tf +176 -0
  73. dander/templates/project/infra/bootstrap-admin/outputs.tf +24 -0
  74. dander/templates/project/infra/bootstrap-admin/variables.tf +75 -0
  75. dander/templates/project/infra/bootstrap-admin/versions.tf +19 -0
  76. dander/templates/project/infra/functions/__init__.py +1 -0
  77. dander/templates/project/infra/functions/stop_billing/__init__.py +1 -0
  78. dander/templates/project/infra/functions/stop_billing/handler.py +66 -0
  79. dander/templates/project/infra/functions/stop_billing/main.py +37 -0
  80. dander/templates/project/infra/functions/stop_billing/requirements.txt +3 -0
  81. dander/templates/project/infra/main.tf +133 -0
  82. dander/templates/project/infra/modules/bigquery/main.tf +13 -0
  83. dander/templates/project/infra/modules/bigquery/outputs.tf +4 -0
  84. dander/templates/project/infra/modules/bigquery/variables.tf +16 -0
  85. dander/templates/project/infra/modules/cost-guard/README.md +12 -0
  86. dander/templates/project/infra/modules/cost-guard/main.tf +206 -0
  87. dander/templates/project/infra/modules/cost-guard/outputs.tf +14 -0
  88. dander/templates/project/infra/modules/cost-guard/variables.tf +45 -0
  89. dander/templates/project/infra/modules/cost-guard/versions.tf +21 -0
  90. dander/templates/project/infra/modules/github-wif/README.md +6 -0
  91. dander/templates/project/infra/modules/github-wif/main.tf +79 -0
  92. dander/templates/project/infra/modules/github-wif/outputs.tf +9 -0
  93. dander/templates/project/infra/modules/github-wif/variables.tf +30 -0
  94. dander/templates/project/infra/modules/github-wif/versions.tf +10 -0
  95. dander/templates/project/infra/modules/scheduled-job/README.md +37 -0
  96. dander/templates/project/infra/modules/scheduled-job/main.tf +374 -0
  97. dander/templates/project/infra/modules/scheduled-job/outputs.tf +48 -0
  98. dander/templates/project/infra/modules/scheduled-job/variables.tf +141 -0
  99. dander/templates/project/infra/modules/scheduled-job/versions.tf +10 -0
  100. dander/templates/project/infra/modules/secret-manager/README.md +5 -0
  101. dander/templates/project/infra/modules/secret-manager/main.tf +45 -0
  102. dander/templates/project/infra/modules/secret-manager/outputs.tf +4 -0
  103. dander/templates/project/infra/modules/secret-manager/variables.tf +25 -0
  104. dander/templates/project/infra/modules/secret-manager/versions.tf +10 -0
  105. dander/templates/project/infra/outputs.tf +37 -0
  106. dander/templates/project/infra/ownership-cutover.tf +11 -0
  107. dander/templates/project/infra/sandbox.auto.tfvars.example +35 -0
  108. dander/templates/project/infra/variables.tf +211 -0
  109. dander/templates/project/infra/versions.tf +44 -0
  110. dander/templates/project/models/staging/stg_greenhouse__jobs.sql +29 -0
  111. dander/templates/project/models/staging/stg_greenhouse__jobs.yml +50 -0
  112. dander/transform/README.md +26 -0
  113. dander/transform/__init__.py +35 -0
  114. dander/transform/config.py +179 -0
  115. dander/transform/model.py +67 -0
  116. dander/transform/project.py +184 -0
  117. dander/transform/runner.py +335 -0
  118. dander/writer/README.md +37 -0
  119. dander/writer/__init__.py +43 -0
  120. dander/writer/base.py +76 -0
  121. dander/writer/bigquery.py +913 -0
  122. dander/writer/storage_write.py +294 -0
  123. dander_platform-0.1.0.dist-info/METADATA +627 -0
  124. dander_platform-0.1.0.dist-info/RECORD +127 -0
  125. dander_platform-0.1.0.dist-info/WHEEL +4 -0
  126. dander_platform-0.1.0.dist-info/entry_points.txt +3 -0
  127. dander_platform-0.1.0.dist-info/licenses/LICENSE +176 -0
dander/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ """Dander — an opinionated, GCP-native data platform (ingest + transform + catalog).
2
+
3
+ Keep this module import-light: importing ``dander`` must not require the heavy optional
4
+ dependencies. Import from the subpackages (``dander.ingestion``, ``dander.writer``, ...) directly.
5
+ """
6
+
7
+ from importlib.metadata import PackageNotFoundError, version
8
+
9
+ try:
10
+ __version__ = version("dander-platform")
11
+ except PackageNotFoundError: # pragma: no cover - only an unpackaged source tree
12
+ __version__ = "0+unknown"
@@ -0,0 +1,54 @@
1
+ """Narrow retry support for BigQuery transaction contention."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import random
7
+ from time import sleep
8
+ from typing import TYPE_CHECKING, Protocol
9
+
10
+ from google.api_core.exceptions import BadRequest
11
+
12
+ if TYPE_CHECKING:
13
+ from collections.abc import Callable
14
+
15
+ _LOGGER = logging.getLogger(__name__)
16
+ _CONCURRENT_UPDATE_MESSAGES = (
17
+ "transaction is aborted due to concurrent update against table",
18
+ "could not serialize access to table",
19
+ )
20
+ _MAX_ATTEMPTS = 5
21
+ _BASE_DELAY_SECONDS = 0.25
22
+
23
+
24
+ class _Job(Protocol):
25
+ def result(self) -> object:
26
+ """Wait for the submitted BigQuery job."""
27
+
28
+
29
+ def run_mutation_with_retry[JobT: _Job](submit: Callable[[], JobT]) -> JobT:
30
+ """Run a mutation, retrying only BigQuery's concurrent-update transaction abort."""
31
+ for attempt in range(1, _MAX_ATTEMPTS + 1):
32
+ try:
33
+ job = submit()
34
+ job.result()
35
+ return job
36
+ except BadRequest as error:
37
+ message = str(error).lower()
38
+ concurrent_update = any(
39
+ signature in message for signature in _CONCURRENT_UPDATE_MESSAGES
40
+ )
41
+ if not concurrent_update or attempt == _MAX_ATTEMPTS:
42
+ raise
43
+ delay = (_BASE_DELAY_SECONDS * (2 ** (attempt - 1))) + random.uniform(
44
+ 0,
45
+ _BASE_DELAY_SECONDS,
46
+ )
47
+ _LOGGER.warning(
48
+ "BigQuery transaction contention; retrying mutation in %.2fs (attempt %d/%d)",
49
+ delay,
50
+ attempt + 1,
51
+ _MAX_ATTEMPTS,
52
+ )
53
+ sleep(delay)
54
+ raise AssertionError("BigQuery mutation retry loop exited unexpectedly")
@@ -0,0 +1,37 @@
1
+ """Infrastructure bootstrap adapters."""
2
+
3
+ from dander.bootstrap.admin import AdministrativeBootstrap, AdministrativeBootstrapError
4
+ from dander.bootstrap.project import (
5
+ ProjectBootstrapError,
6
+ RuntimeImagePublisher,
7
+ StateBucketBootstrap,
8
+ active_admin_member,
9
+ wait_for_service_account_impersonation,
10
+ )
11
+ from dander.bootstrap.terraform import TerraformBootstrap, TerraformBootstrapError
12
+ from dander.bootstrap.verify import (
13
+ DeploymentSummary,
14
+ DeploymentVerificationError,
15
+ DeploymentVerifier,
16
+ VerificationCheck,
17
+ VerificationStatus,
18
+ write_summary,
19
+ )
20
+
21
+ __all__ = [
22
+ "AdministrativeBootstrap",
23
+ "AdministrativeBootstrapError",
24
+ "DeploymentSummary",
25
+ "DeploymentVerificationError",
26
+ "DeploymentVerifier",
27
+ "ProjectBootstrapError",
28
+ "RuntimeImagePublisher",
29
+ "StateBucketBootstrap",
30
+ "TerraformBootstrap",
31
+ "TerraformBootstrapError",
32
+ "VerificationCheck",
33
+ "VerificationStatus",
34
+ "write_summary",
35
+ "active_admin_member",
36
+ "wait_for_service_account_impersonation",
37
+ ]
@@ -0,0 +1,276 @@
1
+ """Stage-zero administrative Terraform bootstrap."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ import subprocess
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from pathlib import Path
12
+
13
+ _PROJECT_ID = re.compile(r"^[a-z][a-z0-9-]{4,28}[a-z0-9]$")
14
+ _BUCKET_NAME = re.compile(r"^[a-z0-9][a-z0-9._-]{1,61}[a-z0-9]$")
15
+ _REGION = re.compile(r"^[a-z][a-z0-9-]{1,62}[a-z0-9]$")
16
+ _LOCATION = re.compile(r"^[A-Za-z][A-Za-z0-9-]{0,62}$")
17
+ _BILLING_ACCOUNT = re.compile(r"^[0-9A-F]{6}-[0-9A-F]{6}-[0-9A-F]{6}$")
18
+ _PRINCIPAL = re.compile(r"^(?:user|serviceAccount|group):[^\r\n]+$")
19
+ _SERVICE_ACCOUNT_ID = re.compile(r"^[a-z][a-z0-9-]{4,28}[a-z0-9]$")
20
+ _GITHUB_REPOSITORY = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")
21
+ _GITHUB_REF = re.compile(r"^refs/(?:heads|tags)/[A-Za-z0-9._/-]+$")
22
+ _STAGE_ZERO_STATE_PREFIX = "dander/bootstrap-admin/state"
23
+
24
+
25
+ class AdministrativeBootstrapError(RuntimeError):
26
+ """Raised when stage-zero Terraform cannot safely complete."""
27
+
28
+
29
+ class AdministrativeBootstrap:
30
+ """Create only the state bucket and identity preconditions for platform Terraform."""
31
+
32
+ def __init__(self, infra_dir: Path, operator_artifact_dir: Path) -> None:
33
+ self._infra_dir = infra_dir.resolve()
34
+ self._repository_dir = self._infra_dir.parent.parent
35
+ self._operator_artifact_dir = operator_artifact_dir.expanduser().resolve()
36
+ if self._is_within(self._operator_artifact_dir, self._repository_dir):
37
+ raise AdministrativeBootstrapError(
38
+ "Operator artifact directory must be outside the repository checkout"
39
+ )
40
+ self._tf_data_dir = self._operator_artifact_dir / "terraform-data"
41
+ self._plan_path = self._operator_artifact_dir / "dander-admin-bootstrap.tfplan"
42
+
43
+ def execute(
44
+ self,
45
+ *,
46
+ project: str,
47
+ state_bucket: str,
48
+ admin_member: str,
49
+ apply: bool,
50
+ region: str = "us-central1",
51
+ state_location: str = "US",
52
+ bootstrap_service_account_id: str = "dander-bootstrap",
53
+ billing_account_id: str = "",
54
+ github_repository: str = "",
55
+ github_ref: str = "refs/heads/main",
56
+ adopt_state_bucket: bool = False,
57
+ ) -> Path:
58
+ """Plan stage zero and optionally apply that exact saved plan.
59
+
60
+ Args:
61
+ project: GCP project receiving the administrative resources.
62
+ state_bucket: Globally unique bucket name for platform Terraform state.
63
+ admin_member: Approved principal allowed to impersonate the bootstrap identity.
64
+ apply: Whether to apply the saved plan after planning.
65
+ region: Provider region.
66
+ state_location: GCS location for the state bucket.
67
+ bootstrap_service_account_id: Account id for the bootstrap service account.
68
+ billing_account_id: Optional billing account for budget-management access.
69
+ github_repository: Optional repository allowed to use proof-workflow WIF.
70
+ github_ref: Exact branch or tag ref allowed to use proof-workflow WIF.
71
+ adopt_state_bucket: Import a pre-created stage-zero backend bucket when absent from
72
+ state. Used only by the batteries-included ``dander init`` flow.
73
+
74
+ Returns:
75
+ Path to the saved stage-zero plan.
76
+ """
77
+ self._validate(
78
+ project=project,
79
+ state_bucket=state_bucket,
80
+ admin_member=admin_member,
81
+ region=region,
82
+ state_location=state_location,
83
+ bootstrap_service_account_id=bootstrap_service_account_id,
84
+ billing_account_id=billing_account_id,
85
+ github_repository=github_repository,
86
+ github_ref=github_ref,
87
+ )
88
+ self._prepare_operator_directories()
89
+ plan_path = self._plan_path
90
+ self._run(
91
+ "terraform",
92
+ "init",
93
+ "-reconfigure",
94
+ "-input=false",
95
+ f"-backend-config=bucket={state_bucket}",
96
+ f"-backend-config=prefix={_STAGE_ZERO_STATE_PREFIX}",
97
+ )
98
+ if adopt_state_bucket:
99
+ self._adopt_state_bucket(
100
+ project=project,
101
+ state_bucket=state_bucket,
102
+ admin_member=admin_member,
103
+ region=region,
104
+ state_location=state_location,
105
+ bootstrap_service_account_id=bootstrap_service_account_id,
106
+ billing_account_id=billing_account_id,
107
+ github_repository=github_repository,
108
+ github_ref=github_ref,
109
+ )
110
+ self._run(
111
+ "terraform",
112
+ "plan",
113
+ f"-var=project_id={project}",
114
+ f"-var=state_bucket={state_bucket}",
115
+ f"-var=admin_member={admin_member}",
116
+ f"-var=region={region}",
117
+ f"-var=state_location={state_location}",
118
+ f"-var=bootstrap_service_account_id={bootstrap_service_account_id}",
119
+ f"-var=billing_account_id={billing_account_id}",
120
+ f"-var=github_repository={github_repository}",
121
+ f"-var=github_ref={github_ref}",
122
+ f"-out={plan_path}",
123
+ )
124
+ try:
125
+ if not plan_path.is_file() or plan_path.is_symlink():
126
+ raise AdministrativeBootstrapError(
127
+ f"Terraform did not create a regular saved plan at {plan_path}"
128
+ )
129
+ plan_path.chmod(0o600)
130
+ except OSError as error:
131
+ raise AdministrativeBootstrapError(
132
+ f"Could not secure the saved Terraform plan at {plan_path}"
133
+ ) from error
134
+ if apply:
135
+ self._run("terraform", "apply", str(plan_path))
136
+ return plan_path
137
+
138
+ @staticmethod
139
+ def _is_within(candidate: Path, parent: Path) -> bool:
140
+ return candidate == parent or parent in candidate.parents
141
+
142
+ def _prepare_operator_directories(self) -> None:
143
+ try:
144
+ if self._tf_data_dir.is_symlink():
145
+ raise AdministrativeBootstrapError("terraform-data must not be a symlink")
146
+ if self._tf_data_dir.exists() and not self._tf_data_dir.is_dir():
147
+ raise AdministrativeBootstrapError("terraform-data must be a directory")
148
+ resolved_tf_data_dir = self._tf_data_dir.resolve()
149
+ if not self._is_within(resolved_tf_data_dir, self._operator_artifact_dir):
150
+ raise AdministrativeBootstrapError(
151
+ "Resolved terraform-data must remain inside the operator artifact directory"
152
+ )
153
+ if self._is_within(resolved_tf_data_dir, self._repository_dir):
154
+ raise AdministrativeBootstrapError(
155
+ "Resolved terraform-data must remain outside the repository checkout"
156
+ )
157
+ if self._plan_path.is_symlink():
158
+ raise AdministrativeBootstrapError("Saved Terraform plan must not be a symlink")
159
+ if self._plan_path.exists() and not self._plan_path.is_file():
160
+ raise AdministrativeBootstrapError("Saved Terraform plan must be a regular file")
161
+ self._operator_artifact_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
162
+ self._operator_artifact_dir.chmod(0o700)
163
+ self._tf_data_dir.mkdir(mode=0o700, exist_ok=True)
164
+ self._tf_data_dir.chmod(0o700)
165
+ if self._plan_path.exists():
166
+ self._plan_path.chmod(0o600)
167
+ self._plan_path.unlink()
168
+ except OSError as error:
169
+ raise AdministrativeBootstrapError(
170
+ "Could not create or secure the operator artifact directory"
171
+ ) from error
172
+
173
+ @staticmethod
174
+ def _validate(
175
+ *,
176
+ project: str,
177
+ state_bucket: str,
178
+ admin_member: str,
179
+ region: str,
180
+ state_location: str,
181
+ bootstrap_service_account_id: str,
182
+ billing_account_id: str,
183
+ github_repository: str,
184
+ github_ref: str,
185
+ ) -> None:
186
+ for label, value, pattern in (
187
+ ("project", project, _PROJECT_ID),
188
+ ("state bucket", state_bucket, _BUCKET_NAME),
189
+ ("admin member", admin_member, _PRINCIPAL),
190
+ ("region", region, _REGION),
191
+ ("state location", state_location, _LOCATION),
192
+ ("bootstrap service-account id", bootstrap_service_account_id, _SERVICE_ACCOUNT_ID),
193
+ ):
194
+ if not pattern.fullmatch(value):
195
+ raise AdministrativeBootstrapError(f"Invalid {label}: {value!r}")
196
+ if billing_account_id and not _BILLING_ACCOUNT.fullmatch(billing_account_id):
197
+ raise AdministrativeBootstrapError(
198
+ "Billing account must use XXXXXX-XXXXXX-XXXXXX format"
199
+ )
200
+ if github_repository and not _GITHUB_REPOSITORY.fullmatch(github_repository):
201
+ raise AdministrativeBootstrapError("Invalid GitHub repository")
202
+ if not _GITHUB_REF.fullmatch(github_ref):
203
+ raise AdministrativeBootstrapError("Invalid GitHub ref")
204
+
205
+ def _run(self, *args: str) -> None:
206
+ environment = os.environ.copy()
207
+ environment["TF_DATA_DIR"] = str(self._tf_data_dir)
208
+ try:
209
+ subprocess.run(
210
+ args,
211
+ cwd=self._infra_dir,
212
+ env=environment,
213
+ umask=0o077,
214
+ check=True,
215
+ )
216
+ except FileNotFoundError as error:
217
+ raise AdministrativeBootstrapError(
218
+ "Terraform is not installed or is not available on PATH"
219
+ ) from error
220
+ except subprocess.CalledProcessError as error:
221
+ raise AdministrativeBootstrapError(
222
+ f"Terraform command failed with exit code {error.returncode}"
223
+ ) from error
224
+
225
+ def _adopt_state_bucket(
226
+ self,
227
+ *,
228
+ project: str,
229
+ state_bucket: str,
230
+ admin_member: str,
231
+ region: str,
232
+ state_location: str,
233
+ bootstrap_service_account_id: str,
234
+ billing_account_id: str,
235
+ github_repository: str,
236
+ github_ref: str,
237
+ ) -> None:
238
+ if (
239
+ self._run_status("terraform", "state", "show", "google_storage_bucket.terraform_state")
240
+ == 0
241
+ ):
242
+ return
243
+ self._run(
244
+ "terraform",
245
+ "import",
246
+ f"-var=project_id={project}",
247
+ f"-var=state_bucket={state_bucket}",
248
+ f"-var=admin_member={admin_member}",
249
+ f"-var=region={region}",
250
+ f"-var=state_location={state_location}",
251
+ f"-var=bootstrap_service_account_id={bootstrap_service_account_id}",
252
+ f"-var=billing_account_id={billing_account_id}",
253
+ f"-var=github_repository={github_repository}",
254
+ f"-var=github_ref={github_ref}",
255
+ "google_storage_bucket.terraform_state",
256
+ state_bucket,
257
+ )
258
+
259
+ def _run_status(self, *args: str) -> int:
260
+ environment = os.environ.copy()
261
+ environment["TF_DATA_DIR"] = str(self._tf_data_dir)
262
+ try:
263
+ completed = subprocess.run(
264
+ args,
265
+ cwd=self._infra_dir,
266
+ env=environment,
267
+ umask=0o077,
268
+ check=False,
269
+ stdout=subprocess.DEVNULL,
270
+ stderr=subprocess.DEVNULL,
271
+ )
272
+ except FileNotFoundError as error:
273
+ raise AdministrativeBootstrapError(
274
+ "Terraform is not installed or is not available on PATH"
275
+ ) from error
276
+ return completed.returncode