datacommons-admin 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ """CLI package for administering Data Commons instances in GCP."""
@@ -0,0 +1,553 @@
1
+ # Copyright 2026 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from pathlib import Path
16
+ import re
17
+ import sys
18
+ import urllib.request
19
+ from typing import Any, Tuple
20
+
21
+ import click
22
+ from google.api_core import exceptions
23
+ from google.cloud import storage
24
+
25
+ from datacommons_admin.infra_templates import (
26
+ BACKEND_TF_TEMPLATE,
27
+ README_TEMPLATE,
28
+ REMOTE_STATE_TEMPLATE,
29
+ )
30
+
31
+
32
+ DEFAULT_BUCKET_LOCATION = "US"
33
+ GITHUB_RAW_BASE_URL = "https://raw.githubusercontent.com/datacommonsorg/datacommons"
34
+ GITHUB_REPO_URL = "https://github.com/datacommonsorg/datacommons.git"
35
+
36
+
37
+ def _get_default_bucket_name(namespace: str, project_id: str) -> str:
38
+ """Returns the default Google Cloud Storage bucket name for Terraform state."""
39
+ return f"tf-state-{namespace}-{project_id}"
40
+
41
+
42
+ def _get_default_state_prefix(namespace: str) -> str:
43
+ """Returns the default Google Cloud Storage object prefix for Terraform state."""
44
+ return f"terraform/state/{namespace}"
45
+
46
+
47
+ def _log_resolved_value(label: str, value: str, is_default: bool, indent: int = 2):
48
+ """Logs a value with a bullet if default, or a green checkmark if from flag."""
49
+ prefix = " " * indent
50
+ padded_label = label.ljust(12)
51
+ if is_default:
52
+ click.echo(f"{prefix}- {padded_label}: {value} (Default)")
53
+ else:
54
+ click.secho(f"{prefix}✔", fg="green", nl=False)
55
+ click.echo(f" {padded_label}: {value} (from flag)")
56
+
57
+
58
+ def _prompt(text: str, indent: int = 2, **kwargs):
59
+ """Prints the cyan [?] prompt symbol and calls click.prompt."""
60
+ click.secho(" " * indent + "[?]", fg="cyan", bold=True, nl=False)
61
+ prompt_text = text if text.startswith(" ") else f" {text}"
62
+ return click.prompt(prompt_text, **kwargs)
63
+
64
+
65
+ def _confirm(text: str, indent: int = 2, **kwargs):
66
+ """Prints the cyan [?] prompt symbol and calls click.confirm."""
67
+ click.secho(" " * indent + "[?]", fg="cyan", bold=True, nl=False)
68
+ prompt_text = text if text.startswith(" ") else f" {text}"
69
+ return click.confirm(prompt_text, **kwargs)
70
+
71
+
72
+ def _create_and_configure_bucket(
73
+ storage_client,
74
+ bucket_name: str,
75
+ project_id: str,
76
+ location: str = DEFAULT_BUCKET_LOCATION,
77
+ ) -> bool:
78
+ """Prompts and creates a Google Cloud Storage bucket, enables versioning, and sets IAM policy.
79
+
80
+ Returns True if created, False if cancelled.
81
+ """
82
+ click.echo(f" - {'Status'.ljust(12)}: Not found")
83
+ click.echo(f" - {'Project'.ljust(12)}: {project_id}")
84
+ _log_resolved_value("Location", location, location == DEFAULT_BUCKET_LOCATION)
85
+
86
+ if not _confirm("Create this bucket?", default=True):
87
+ return False
88
+
89
+ click.secho(
90
+ f" Creating bucket gs://{bucket_name} in project {project_id} with location {location}...",
91
+ fg="bright_black",
92
+ )
93
+ new_bucket = storage_client.create_bucket(bucket_name, location=location)
94
+ new_bucket.iam_configuration.uniform_bucket_level_access_enabled = True
95
+ new_bucket.versioning_enabled = True
96
+ new_bucket.patch()
97
+ click.secho(f" Enabling versioning...", fg="bright_black")
98
+ click.secho(" Configuring bucket IAM policy...", fg="bright_black")
99
+ policy = new_bucket.get_iam_policy(requested_policy_version=3)
100
+ policy["roles/storage.objectAdmin"].add(f"projectEditor:{project_id}")
101
+ policy["roles/storage.objectAdmin"].add(f"projectOwner:{project_id}")
102
+ new_bucket.set_iam_policy(policy)
103
+ return True
104
+
105
+
106
+ def _abort_bucket_setup(is_default: bool):
107
+ click.secho(
108
+ " No bucket available for remote Terraform state storage. Cancelling setup.",
109
+ fg="red",
110
+ )
111
+ if is_default:
112
+ click.secho(
113
+ " Hint: Use --no-tf-remote-state to keep Terraform state local, or --tf-state-bucket to override the bucket name. See --help for more.",
114
+ fg="bright_black",
115
+ )
116
+ # Use sys.exit(1) instead of click.Abort() to avoid being caught by broad
117
+ # except Exception blocks in the caller and to avoid Click's "Aborted!" message.
118
+ sys.exit(1)
119
+
120
+
121
+ def _ensure_bucket_ready(
122
+ storage_client,
123
+ bucket_name: str,
124
+ project_id: str,
125
+ location: str = DEFAULT_BUCKET_LOCATION,
126
+ is_default: bool = False,
127
+ ) -> bool:
128
+ """Checks if bucket exists and is ready to use, or creates it if missing.
129
+
130
+ Returns True if the bucket is ready to use, False if the user wants to try another name.
131
+ """
132
+ try:
133
+ bucket = storage_client.get_bucket(bucket_name)
134
+ click.echo(f" - {'Status'.ljust(12)}: Found")
135
+ # Only prompt to reuse if it was the default bucket
136
+ if is_default:
137
+ if not _confirm("Use this bucket?", default=True):
138
+ _abort_bucket_setup(is_default)
139
+ else:
140
+ click.echo(" Proceeding...")
141
+ return True
142
+ except exceptions.NotFound:
143
+ if _create_and_configure_bucket(
144
+ storage_client, bucket_name, project_id, location
145
+ ):
146
+ return True
147
+ else:
148
+ _abort_bucket_setup(is_default)
149
+
150
+
151
+ def _configure_remote_state(
152
+ project_id: str,
153
+ namespace: str,
154
+ bucket_name: str = "",
155
+ location: str = DEFAULT_BUCKET_LOCATION,
156
+ ) -> str:
157
+ """Handles Google Cloud Storage state bucket verification, creation, and IAM setup."""
158
+ try:
159
+ storage_client = storage.Client(project=project_id)
160
+ except Exception as e:
161
+ raise click.ClickException(
162
+ f"Failed to initialize Google Cloud Storage client for project '{project_id}': {e}. "
163
+ "Ensure you are authenticated via 'gcloud auth application-default login'."
164
+ )
165
+
166
+ is_default = False
167
+ if not bucket_name:
168
+ bucket_name = _get_default_bucket_name(namespace, project_id)
169
+ is_default = True
170
+
171
+ click.echo(
172
+ "Setting up Google Cloud Storage bucket for storing terraform state remotely:"
173
+ )
174
+ _log_resolved_value("Name", bucket_name, is_default)
175
+
176
+ try:
177
+ ready = _ensure_bucket_ready(
178
+ storage_client, bucket_name, project_id, location, is_default
179
+ )
180
+ except exceptions.Unauthorized as e:
181
+ raise click.ClickException(
182
+ f"Authentication failed: {e}\n"
183
+ "Please ensure you are authenticated. Run 'gcloud auth application-default login' and try again."
184
+ )
185
+ except exceptions.Forbidden as e:
186
+ raise click.ClickException(
187
+ f"Permission denied: {e}\n"
188
+ f"Please ensure your account has 'Storage Admin' or 'Project Editor' permissions in project '{project_id}'."
189
+ )
190
+ except click.Abort:
191
+ click.echo("")
192
+ sys.exit(1)
193
+ except Exception as e:
194
+ click.secho(
195
+ f" Error: Failed to access or create bucket gs://{bucket_name}.",
196
+ fg="red",
197
+ bold=True,
198
+ )
199
+ click.secho(f" {e}", fg="red")
200
+ raise click.ClickException("Setup cancelled.")
201
+
202
+ if not ready:
203
+ raise click.ClickException("Setup cancelled.")
204
+ return bucket_name
205
+
206
+
207
+ def _get_github_templates(ref: str) -> tuple[str, str, str, str]:
208
+ """Fetches variables.tf, main.tf, outputs.tf, and terraform.tfvars.template from GitHub for the given ref."""
209
+ base_url = f"{GITHUB_RAW_BASE_URL}/{ref}/infra/dcp"
210
+
211
+ def fetch(filename: str) -> str:
212
+ url = f"{base_url}/{filename}"
213
+
214
+ req = urllib.request.Request(url, headers={"User-Agent": "DataCommons-CLI"})
215
+ with urllib.request.urlopen(req, timeout=10) as response:
216
+ return response.read().decode("utf-8")
217
+
218
+ return (
219
+ fetch("variables.tf"),
220
+ fetch("main.tf"),
221
+ fetch("outputs.tf"),
222
+ fetch("terraform.tfvars.template"),
223
+ )
224
+
225
+
226
+ @click.group()
227
+ def admin() -> None:
228
+ """Manage a Data Commons Platform instance in Google Cloud"""
229
+
230
+
231
+ def _resolve_project_config(
232
+ project_id: str, namespace: str, force: bool
233
+ ) -> Tuple[str, str, Path]:
234
+ """Resolves project ID and namespace, and determines target directory."""
235
+ if project_id:
236
+ _log_resolved_value("Project ID", project_id, is_default=False)
237
+ if namespace:
238
+ _log_resolved_value("Namespace", namespace, is_default=False)
239
+
240
+ resolved_project_id = project_id.strip()
241
+ if not resolved_project_id:
242
+ resolved_project_id = _prompt(
243
+ "Google Cloud Platform project ID", type=str
244
+ ).strip()
245
+ if not resolved_project_id:
246
+ raise click.ClickException("GCP project ID must not be empty.")
247
+
248
+ resolved_namespace = namespace.strip()
249
+ while True:
250
+ if not resolved_namespace:
251
+ resolved_namespace = _prompt("Namespace", type=str).strip()
252
+ if not resolved_namespace:
253
+ click.secho("Error: Namespace must not be empty.", fg="red")
254
+ continue
255
+
256
+ target_dir = Path.cwd() / resolved_namespace
257
+ if target_dir.exists() and not force:
258
+ click.secho(
259
+ f"Error: Folder '{resolved_namespace}' already exists locally. "
260
+ "Please specify a different namespace, or use --force to overwrite.",
261
+ fg="yellow",
262
+ )
263
+ resolved_namespace = ""
264
+ continue
265
+
266
+ break
267
+
268
+ return resolved_project_id, resolved_namespace, target_dir
269
+
270
+
271
+ def _check_existing_files(target_dir: Path, use_remote_state: bool, force: bool):
272
+ """Checks if target files already exist and raises error if they do (and not force)."""
273
+ main_tf_path = target_dir / "main.tf"
274
+ tfvars_path = target_dir / "terraform.tfvars"
275
+ readme_path = target_dir / "README.md"
276
+ backend_tf_path = target_dir / "backend.tf"
277
+
278
+ paths_to_check = [main_tf_path, tfvars_path, readme_path]
279
+ if use_remote_state:
280
+ paths_to_check.append(backend_tf_path)
281
+
282
+ existing_paths = [path for path in paths_to_check if path.exists()]
283
+ if existing_paths and not force:
284
+ existing_labels = ", ".join(str(path) for path in existing_paths)
285
+ raise click.ClickException(
286
+ f"Refusing to overwrite existing file(s): {existing_labels}. "
287
+ "Use --force to overwrite."
288
+ )
289
+
290
+
291
+ def _setup_dcp_config_dir(
292
+ target_dir: Path,
293
+ project_id: str,
294
+ namespace: str,
295
+ bucket_name: str,
296
+ tf_state_prefix: str,
297
+ dc_api_key: str,
298
+ ref: str,
299
+ use_remote_state: bool,
300
+ ):
301
+ """Downloads and populates Terraform templates."""
302
+
303
+ api_key = dc_api_key.strip()
304
+ if not api_key:
305
+ api_key = _prompt(
306
+ "Data Commons API key (from apikeys.datacommons.org)",
307
+ type=str,
308
+ default="",
309
+ show_default=False,
310
+ ).strip()
311
+
312
+ if not api_key:
313
+ click.secho(
314
+ " [!] Warning: Data Commons API key was skipped. You must add it to terraform.tfvars before running terraform apply.",
315
+ fg="yellow",
316
+ bold=True,
317
+ )
318
+
319
+ target_dir.mkdir(parents=True, exist_ok=True)
320
+ click.secho(f"Creating directory: {target_dir}", fg="bright_black")
321
+
322
+ try:
323
+ variables_content, main_content, outputs_content, tfvars_example = (
324
+ _get_github_templates(ref)
325
+ )
326
+
327
+ # Update the stack module source to point to GitHub
328
+ resolved_source = f"git::{GITHUB_REPO_URL}//infra/dcp/modules/stack?ref={ref}"
329
+ main_content = re.sub(
330
+ r'source\s*=\s*["\']\./modules/stack["\']',
331
+ f'source = "{resolved_source}"',
332
+ main_content,
333
+ )
334
+
335
+ # Write the files
336
+ (target_dir / "variables.tf").write_text(variables_content, encoding="utf-8")
337
+ (target_dir / "main.tf").write_text(main_content, encoding="utf-8")
338
+ (target_dir / "outputs.tf").write_text(outputs_content, encoding="utf-8")
339
+
340
+ # Modify tfvars_example with actual values
341
+ tfvars_content = tfvars_example
342
+ tfvars_content = tfvars_content.replace('"$$PROJECT_ID$$"', f'"{project_id}"')
343
+ tfvars_content = tfvars_content.replace('"$$NAMESPACE$$"', f'"{namespace}"')
344
+ if api_key:
345
+ tfvars_content = tfvars_content.replace('"$$DC_API_KEY$$"', f'"{api_key}"')
346
+
347
+ (target_dir / "terraform.tfvars").write_text(tfvars_content, encoding="utf-8")
348
+
349
+ except Exception as e:
350
+ raise click.ClickException(f"Failed to initialize Terraform templates: {e}")
351
+
352
+ remote_state_info = ""
353
+ if use_remote_state and bucket_name:
354
+ remote_state_info = REMOTE_STATE_TEMPLATE.format(
355
+ bucket_name=bucket_name, prefix=tf_state_prefix
356
+ )
357
+
358
+ (target_dir / "README.md").write_text(
359
+ README_TEMPLATE.format(remote_state_section=remote_state_info), encoding="utf-8"
360
+ )
361
+ if use_remote_state and bucket_name:
362
+ (target_dir / "backend.tf").write_text(
363
+ BACKEND_TF_TEMPLATE.format(
364
+ bucket_name=bucket_name,
365
+ prefix=tf_state_prefix,
366
+ ),
367
+ encoding="utf-8",
368
+ )
369
+
370
+ click.secho("Downloaded and populated Terraform templates.", fg="bright_black")
371
+
372
+
373
+ @admin.command()
374
+ @click.option(
375
+ "--project-id",
376
+ default="",
377
+ help="Google Cloud Platform project ID used for all resources related to your Data Commons instance.",
378
+ )
379
+ @click.option(
380
+ "--namespace", default="", help="Namespace prefix for provisioned resources."
381
+ )
382
+ @click.option("--dc-api-key", default="", help="Data Commons API key.")
383
+ @click.option(
384
+ "--ref", default="main", show_default=True, help="Git ref for module source."
385
+ )
386
+ @click.option(
387
+ "--force", is_flag=True, help="Overwrite existing generated files if present."
388
+ )
389
+ @click.option(
390
+ "--tf-remote-state/--no-tf-remote-state",
391
+ default=True,
392
+ help="Enable or disable Terraform remote state management in Google Cloud Storage. Disabling ignores other --tf-state-* flags.",
393
+ )
394
+ @click.option(
395
+ "--tf-state-bucket",
396
+ default="",
397
+ help="Google Cloud Storage bucket for Terraform remote state. Generates a default name if omitted. Prompts to create the bucket if it is missing.",
398
+ )
399
+ @click.option(
400
+ "--tf-state-bucket-location",
401
+ default=DEFAULT_BUCKET_LOCATION,
402
+ show_default=True,
403
+ help="Google Cloud Storage bucket location if a new bucket needs to be created.",
404
+ )
405
+ @click.option(
406
+ "--tf-state-prefix",
407
+ default="",
408
+ help="Google Cloud Storage object prefix for Terraform state file (default: terraform/state/{namespace}).",
409
+ )
410
+ def init(
411
+ project_id: str,
412
+ namespace: str,
413
+ dc_api_key: str,
414
+ ref: str,
415
+ force: bool,
416
+ tf_remote_state: bool,
417
+ tf_state_bucket: str,
418
+ tf_state_bucket_location: str,
419
+ tf_state_prefix: str,
420
+ ) -> None:
421
+ """Initialize Terraform scaffolding for Data Commons administration/infrastructure."""
422
+ click.secho("Data Commons Admin Init", fg="cyan", bold=True)
423
+
424
+ # 1. Project Configs
425
+ click.secho("\n[Project Configuration]", fg="cyan", bold=True)
426
+ click.secho("Configuring project settings...", fg="bright_black")
427
+ resolved_project_id, resolved_namespace, target_dir = _resolve_project_config(
428
+ project_id, namespace, force
429
+ )
430
+
431
+ # 2. Terraform Setup
432
+ click.secho("\n[Terraform Backend Setup]", fg="cyan", bold=True)
433
+ click.secho("Configuring backend for Terraform state...", fg="bright_black")
434
+ if not tf_remote_state:
435
+ click.echo(" Using local backend for Terraform state.")
436
+
437
+ # Refuse to overwrite existing files unless --force is specified
438
+ _check_existing_files(target_dir, tf_remote_state, force)
439
+
440
+ resolved_bucket_name = (
441
+ _configure_remote_state(
442
+ resolved_project_id,
443
+ resolved_namespace,
444
+ tf_state_bucket,
445
+ tf_state_bucket_location,
446
+ )
447
+ if tf_remote_state
448
+ else ""
449
+ )
450
+
451
+ resolved_tf_state_prefix = tf_state_prefix.strip() or _get_default_state_prefix(
452
+ resolved_namespace
453
+ )
454
+
455
+ # 3. DCP config dir setup
456
+ click.secho("\n[DCP Configuration]", fg="cyan", bold=True)
457
+ click.secho("Setting up configuration files...", fg="bright_black")
458
+ _setup_dcp_config_dir(
459
+ target_dir,
460
+ resolved_project_id,
461
+ resolved_namespace,
462
+ resolved_bucket_name,
463
+ resolved_tf_state_prefix,
464
+ dc_api_key,
465
+ ref,
466
+ tf_remote_state,
467
+ )
468
+
469
+ click.secho(
470
+ f"Customize variables in {resolved_namespace}/terraform.tfvars as needed.",
471
+ fg="green",
472
+ )
473
+ click.secho(
474
+ f"Refer to {resolved_namespace}/README.md for more info and next steps.",
475
+ fg="green",
476
+ )
477
+
478
+
479
+ def _setup_ingestion_client() -> Tuple[Any, str, str]:
480
+ click.secho(
481
+ "Fetching Ingestion Helper URI, Orchestrator Service Account, and Spanner details from Terraform outputs...",
482
+ fg="bright_black",
483
+ )
484
+
485
+ from datacommons_admin.tf_utils import (
486
+ get_dcp_ingestion_helper_uri,
487
+ get_dcp_orchestrator_service_account_email,
488
+ get_dcp_spanner_instance_id,
489
+ get_dcp_spanner_database_id,
490
+ )
491
+ from datacommons_admin.ingestion_helper_client import IngestionHelperClient
492
+
493
+ uri = get_dcp_ingestion_helper_uri()
494
+ sa_email = get_dcp_orchestrator_service_account_email()
495
+ instance_id = get_dcp_spanner_instance_id()
496
+ database_id = get_dcp_spanner_database_id()
497
+
498
+ click.secho(f"Found Ingestion Helper URI: {uri}", fg="green")
499
+ click.secho(f"Found Orchestrator Service Account: {sa_email}", fg="green")
500
+ click.secho(
501
+ f"Found Spanner Database Instance: {instance_id} / Database ID: {database_id}",
502
+ fg="green",
503
+ )
504
+
505
+ client = IngestionHelperClient(uri, service_account_email=sa_email)
506
+ return client, instance_id, database_id
507
+
508
+
509
+ def _run_seed_db(client: Any, instance_id: str, database_id: str) -> None:
510
+ click.secho(
511
+ f"Seeding Spanner database '{instance_id}/{database_id}' via the Ingestion Helper service (this may take a few moments)...",
512
+ fg="bright_black",
513
+ )
514
+ result = client.seed_database()
515
+ click.secho("Successfully seeded Spanner database!", fg="green", bold=True)
516
+ if "message" in result:
517
+ click.secho(f"Details: {result['message']}", fg="bright_black")
518
+
519
+
520
+ @admin.command(name="init-db")
521
+ @click.option(
522
+ "--init-only", is_flag=True, help="Only initialize the database without seeding."
523
+ )
524
+ def init_db(init_only: bool) -> None:
525
+ """Initialize (and by default seed) the Spanner database via the DCP Ingestion Helper service."""
526
+ click.secho("Datacommons Admin Init-DB", fg="cyan", bold=True)
527
+ client, instance_id, database_id = _setup_ingestion_client()
528
+
529
+ click.secho(
530
+ f"Initializing Spanner database '{instance_id}/{database_id}' via the Ingestion Helper service (this may take a few moments)...",
531
+ fg="bright_black",
532
+ )
533
+ result = client.initialize_database()
534
+
535
+ click.secho("Successfully initialized Spanner database!", fg="green", bold=True)
536
+ if "message" in result:
537
+ click.secho(f"Details: {result['message']}", fg="bright_black")
538
+
539
+ if not init_only:
540
+ _run_seed_db(client, instance_id, database_id)
541
+
542
+
543
+ @admin.command(name="seed-db")
544
+ def seed_db() -> None:
545
+ """Seed the Spanner database via the DCP Ingestion Helper service."""
546
+ click.secho("Datacommons Admin Seed-DB", fg="cyan", bold=True)
547
+ client, instance_id, database_id = _setup_ingestion_client()
548
+ _run_seed_db(client, instance_id, database_id)
549
+
550
+
551
+ from datacommons_admin.ingest_cli import ingest
552
+
553
+ admin.add_command(ingest)
@@ -0,0 +1,75 @@
1
+ # Copyright 2026 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ REMOTE_STATE_TEMPLATE = """
17
+ ## Remote State Management
18
+
19
+ Terraform state is configured to be stored remotely in Google Cloud Storage:
20
+ - **Bucket**: `gs://{bucket_name}`
21
+ - **Prefix**: `{prefix}`
22
+
23
+ This enables team collaboration, state locking, and automated pipeline deployments. The bucket is configured with Uniform Bucket-Level Access and object versioning.
24
+ """
25
+
26
+ README_TEMPLATE = """# Data Commons Platform Terraform Setup
27
+
28
+ This directory contains Terraform configuration for a new Data Commons Platform instance.
29
+
30
+ Data Commons is an open knowledge graph for integrating and querying structured data across domains.
31
+ The Data Commons Platform is the deployable infrastructure stack that runs Data Commons services in your GCP project.
32
+ {remote_state_section}
33
+ ## What This Terraform Creates
34
+
35
+ This setup deploys core Data Commons Platform infrastructure on GCP using the `infra/dcp` module, including Cloud Run services, Cloud Spanner resources, IAM bindings, and supporting service configuration.
36
+
37
+ ## Configure Variables
38
+
39
+ Set environment-specific values in `terraform.tfvars` (for example `project_id`, `namespace`, and `dc_api_key`), and update module arguments in `main.tf` if you want to enable or tune additional features.
40
+
41
+ ## Learn More About Variables
42
+
43
+ For the full list of supported module inputs and defaults, see:
44
+ - https://github.com/datacommonsorg/datacommons/blob/main/infra/dcp/variables.tf
45
+ - https://github.com/datacommonsorg/datacommons/blob/main/infra/dcp/outputs.tf
46
+ - https://github.com/datacommonsorg/datacommons/blob/main/infra/dcp/terraform.tfvars.template
47
+ - https://github.com/datacommonsorg/datacommons/blob/main/infra/dcp/README.md
48
+
49
+ ## Next Steps
50
+
51
+ 1. Review `terraform.tfvars` and update values if needed.
52
+ 2. Initialize Terraform:
53
+ ```bash
54
+ terraform init
55
+ ```
56
+ 3. Preview infrastructure changes:
57
+ ```bash
58
+ terraform plan
59
+ ```
60
+ 4. Deploy infrastructure:
61
+ ```bash
62
+ terraform apply
63
+ ```
64
+
65
+ Generated using the Data Commons CLI tool:
66
+ https://github.com/datacommonsorg/datacommons
67
+ """
68
+
69
+ BACKEND_TF_TEMPLATE = """terraform {{
70
+ backend "gcs" {{
71
+ bucket = "{bucket_name}"
72
+ prefix = "{prefix}"
73
+ }}
74
+ }}
75
+ """
@@ -0,0 +1,142 @@
1
+ # Copyright 2026 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import click
16
+ import re
17
+
18
+ from datacommons_admin.ingestion_job_client import IngestionJobClient
19
+ from datacommons_admin.tf_utils import (
20
+ get_cdc_data_job_name,
21
+ get_dcp_orchestrator_service_account_email,
22
+ get_dcp_project_id,
23
+ get_dcp_region,
24
+ get_dcp_workflow_name,
25
+ )
26
+
27
+
28
+ @click.group(name="ingest")
29
+ def ingest() -> None:
30
+ """Manage data ingestion jobs."""
31
+
32
+
33
+ @ingest.command(name="start")
34
+ def start() -> None:
35
+ """Start a data ingestion job execution."""
36
+ click.secho("Datacommons Admin Ingest Start", fg="cyan", bold=True)
37
+ click.secho(
38
+ "Fetching Data Job Name and Orchestrator Service Account from Terraform outputs...",
39
+ fg="bright_black",
40
+ )
41
+
42
+ job_name = get_cdc_data_job_name()
43
+ sa_email = get_dcp_orchestrator_service_account_email()
44
+ project_id = get_dcp_project_id()
45
+ region = get_dcp_region()
46
+ workflow_name = get_dcp_workflow_name()
47
+
48
+ click.secho(f"Found Data Job Name: {job_name}", fg="green")
49
+ click.secho(f"Found Orchestrator Service Account: {sa_email}", fg="green")
50
+ click.secho(f"Found GCP Project ID: {project_id}", fg="green")
51
+ click.secho(f"Found GCP Region: {region}", fg="green")
52
+ click.secho(
53
+ f"Starting Cloud Run job '{job_name}' via Admin API (this may take a few moments)...",
54
+ fg="bright_black",
55
+ )
56
+
57
+ client = IngestionJobClient(
58
+ job_name,
59
+ service_account_email=sa_email,
60
+ project_id=project_id,
61
+ location=region,
62
+ )
63
+ result = client.start_job()
64
+
65
+ click.secho("Successfully started ingestion job!", fg="green", bold=True)
66
+ res_name = result.get("name") or result.get("metadata", {}).get("name")
67
+
68
+ if res_name:
69
+ op_pattern = r"projects/([^/]+)/locations/([^/]+)/operations/([^/]+)"
70
+ op_match = re.match(op_pattern, res_name)
71
+
72
+ if op_match:
73
+ click.secho(f"Operation details: {res_name}", fg="bright_black")
74
+ resp_project_id, location, operation_id = op_match.groups()
75
+
76
+ short_job_name = job_name.split("/")[-1] if "/" in job_name else job_name
77
+ job_url = f"https://console.cloud.google.com/run/jobs/details/{location}/{short_job_name}/executions?project={resp_project_id}"
78
+
79
+ click.secho("Operation ID: ", fg="cyan", bold=True, nl=False)
80
+ click.secho(operation_id, fg="green")
81
+ click.secho("Job Console Link: ", fg="cyan", bold=True, nl=False)
82
+ click.secho(job_url, fg="blue", underline=True)
83
+ else:
84
+ click.secho(f"Resource details: {res_name}", fg="bright_black")
85
+
86
+ click.secho("\n[!] Note on Ingestion Completion", fg="yellow", bold=True)
87
+ click.secho(
88
+ "This job triggers a Cloud Workflow that runs in the background.\n"
89
+ "Check the Workflows console below to verify full completion.",
90
+ fg="yellow",
91
+ )
92
+
93
+ workflow_url = f"https://console.cloud.google.com/workflows/workflow/{region}/{workflow_name}/executions?project={project_id}"
94
+ click.secho("Workflow Console Link: ", fg="cyan", bold=True, nl=False)
95
+ click.secho(workflow_url, fg="blue", underline=True)
96
+
97
+
98
+ @ingest.command(name="show-config")
99
+ def show_config() -> None:
100
+ """Print the current ingestion job configuration (environment variables)."""
101
+ click.secho("Datacommons Admin Ingest Show-Config", fg="cyan", bold=True)
102
+ click.secho(
103
+ "Fetching Data Job Name and Orchestrator Service Account from Terraform outputs...",
104
+ fg="bright_black",
105
+ )
106
+
107
+ job_name = get_cdc_data_job_name()
108
+ sa_email = get_dcp_orchestrator_service_account_email()
109
+ project_id = get_dcp_project_id()
110
+ region = get_dcp_region()
111
+
112
+ click.secho(f"Found Data Job Name: {job_name}", fg="green")
113
+ click.secho(f"Found Orchestrator Service Account: {sa_email}", fg="green")
114
+ click.secho(f"Found GCP Project ID: {project_id}", fg="green")
115
+ click.secho(f"Found GCP Region: {region}", fg="green")
116
+ click.secho(
117
+ f"Fetching configuration for Cloud Run job '{job_name}'...",
118
+ fg="bright_black",
119
+ )
120
+
121
+ client = IngestionJobClient(
122
+ job_name,
123
+ service_account_email=sa_email,
124
+ project_id=project_id,
125
+ location=region,
126
+ )
127
+ env_vars = client.get_config()
128
+
129
+ click.secho("\nCurrent Ingestion Job Configuration:", fg="cyan", bold=True)
130
+ if not env_vars:
131
+ click.secho("No environment variables configured.", fg="yellow")
132
+ else:
133
+ for env in env_vars:
134
+ name = env.get("name", "UNKNOWN")
135
+ if "value" in env:
136
+ val = env["value"]
137
+ elif "valueSource" in env:
138
+ val = f"[SECRET: {env['valueSource']}]"
139
+ else:
140
+ val = "[UNSET]"
141
+ click.secho(f" {name}: ", fg="bright_black", nl=False)
142
+ click.secho(str(val), fg="green")
@@ -0,0 +1,108 @@
1
+ # Copyright 2026 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import click
16
+ import google.auth
17
+ from google.auth.transport.requests import AuthorizedSession, Request
18
+ from google.oauth2 import id_token
19
+
20
+
21
+ ACTION_INITIALIZE_DATABASE = "initialize_database"
22
+ ACTION_SEED_DATABASE = "seed_database"
23
+
24
+
25
+ class IngestionHelperClient:
26
+ """Client for interacting with the DCP Ingestion Helper Cloud Run service."""
27
+
28
+ def __init__(self, base_url: str, service_account_email: str = None) -> None:
29
+ self.base_url = base_url.rstrip("/")
30
+ self.auth_req = Request()
31
+ self.service_account_email = service_account_email
32
+
33
+ base_credentials, _ = google.auth.default()
34
+
35
+ if service_account_email:
36
+ from google.auth import impersonated_credentials
37
+
38
+ target_credentials = impersonated_credentials.Credentials(
39
+ source_credentials=base_credentials,
40
+ target_principal=service_account_email,
41
+ target_scopes=["https://www.googleapis.com/auth/cloud-platform"],
42
+ )
43
+ creds = impersonated_credentials.IDTokenCredentials(
44
+ target_credentials=target_credentials,
45
+ target_audience=self.base_url,
46
+ include_email=True,
47
+ )
48
+ else:
49
+ try:
50
+ token = id_token.fetch_id_token(self.auth_req, self.base_url)
51
+ from google.oauth2.credentials import Credentials
52
+
53
+ creds = Credentials(token)
54
+ except Exception as e:
55
+ raise click.ClickException(
56
+ f"Failed to fetch ID token for {self.base_url}: {e}\n"
57
+ "Please ensure you are authenticated or provide a service account to impersonate."
58
+ )
59
+
60
+ self.session = AuthorizedSession(creds)
61
+
62
+ def _call_endpoint(self, action_type: str) -> dict:
63
+ url = self.base_url
64
+ payload = {"actionType": action_type}
65
+
66
+ try:
67
+ response = self.session.post(url, json=payload, timeout=300)
68
+ except Exception as e:
69
+ msg = f"Network or authentication error connecting to Ingestion Helper service at {url}: {e}"
70
+ if self.service_account_email:
71
+ msg += f"\nFailed to impersonate {self.service_account_email}. Please ensure your GCP user account has the 'Service Account Token Creator' (roles/iam.serviceAccountTokenCreator) IAM role."
72
+ raise click.ClickException(msg)
73
+
74
+ if response.status_code == 401:
75
+ raise click.ClickException(
76
+ f"HTTP 401 Unauthorized when calling Ingestion Helper at {url}.\n"
77
+ "Your GCP credentials were rejected. Please verify that the service account has the 'Cloud Run Invoker' (roles/run.invoker) IAM role for this service.\n"
78
+ "To re-authenticate, run:\n"
79
+ " gcloud auth application-default login"
80
+ )
81
+
82
+ if not response.ok:
83
+ try:
84
+ error_data = response.json()
85
+ error_msg = (
86
+ error_data.get("message")
87
+ or error_data.get("error")
88
+ or response.text
89
+ )
90
+ except Exception:
91
+ error_msg = response.text
92
+
93
+ raise click.ClickException(
94
+ f"Ingestion Helper returned HTTP {response.status_code}: {error_msg}"
95
+ )
96
+
97
+ try:
98
+ return response.json()
99
+ except Exception:
100
+ return {"status": "success", "message": response.text}
101
+
102
+ def initialize_database(self) -> dict:
103
+ """Calls the initialize_database endpoint on the ingestion helper service."""
104
+ return self._call_endpoint(ACTION_INITIALIZE_DATABASE)
105
+
106
+ def seed_database(self) -> dict:
107
+ """Calls the seed_database endpoint on the ingestion helper service."""
108
+ return self._call_endpoint(ACTION_SEED_DATABASE)
@@ -0,0 +1,145 @@
1
+ # Copyright 2026 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import click
16
+ import google.auth
17
+ from google.auth.transport.requests import AuthorizedSession
18
+
19
+
20
+ class IngestionJobClient:
21
+ """Client for interacting with Cloud Run Admin API to manage CDC data ingestion jobs."""
22
+
23
+ def __init__(
24
+ self,
25
+ job_name: str,
26
+ service_account_email: str = None,
27
+ project_id: str = None,
28
+ location: str = None,
29
+ ) -> None:
30
+ self.service_account_email = service_account_email
31
+ base_credentials, _ = google.auth.default()
32
+
33
+ if not job_name.startswith("projects/"):
34
+ if not project_id:
35
+ raise click.ClickException(
36
+ "Project ID must be provided via Terraform outputs or as an argument when job name is not a full resource name."
37
+ )
38
+ if not location:
39
+ raise click.ClickException(
40
+ "Location must be provided via Terraform outputs or as an argument when job name is not a full resource name."
41
+ )
42
+ self.full_job_name = (
43
+ f"projects/{project_id}/locations/{location}/jobs/{job_name}"
44
+ )
45
+ else:
46
+ self.full_job_name = job_name
47
+
48
+ if service_account_email:
49
+ from google.auth import impersonated_credentials
50
+
51
+ creds = impersonated_credentials.Credentials(
52
+ source_credentials=base_credentials,
53
+ target_principal=service_account_email,
54
+ target_scopes=["https://www.googleapis.com/auth/cloud-platform"],
55
+ )
56
+ else:
57
+ creds = base_credentials
58
+
59
+ self.session = AuthorizedSession(creds)
60
+
61
+ def start_job(self) -> dict:
62
+ """Starts an execution of the Cloud Run job."""
63
+ url = f"https://run.googleapis.com/v2/{self.full_job_name}:run"
64
+ try:
65
+ response = self.session.post(url, json={}, timeout=300)
66
+ except Exception as e:
67
+ msg = f"Network or authentication error connecting to Cloud Run Admin API at {url}: {e}"
68
+ if self.service_account_email:
69
+ msg += f"\nFailed to impersonate {self.service_account_email}. Please ensure your GCP user account has the 'Service Account Token Creator' (roles/iam.serviceAccountTokenCreator) IAM role."
70
+ raise click.ClickException(msg)
71
+
72
+ if response.status_code == 401:
73
+ raise click.ClickException(
74
+ f"HTTP 401 Unauthorized when calling Cloud Run Admin API at {url}.\n"
75
+ "Your GCP credentials were rejected. Please verify your authentication.\n"
76
+ "To re-authenticate, run:\n"
77
+ " gcloud auth application-default login"
78
+ )
79
+
80
+ if not response.ok:
81
+ try:
82
+ error_data = response.json()
83
+ error_msg = (
84
+ error_data.get("message")
85
+ or error_data.get("error", {}).get("message")
86
+ or response.text
87
+ )
88
+ except Exception:
89
+ error_msg = response.text
90
+
91
+ raise click.ClickException(
92
+ f"Cloud Run Admin API returned HTTP {response.status_code}: {error_msg}"
93
+ )
94
+
95
+ try:
96
+ return response.json()
97
+ except Exception:
98
+ return {"status": "success", "message": response.text}
99
+
100
+ def get_config(self) -> list:
101
+ """Retrieves the environment variables configuration of the Cloud Run job."""
102
+ url = f"https://run.googleapis.com/v2/{self.full_job_name}"
103
+ try:
104
+ response = self.session.get(url, timeout=300)
105
+ except Exception as e:
106
+ msg = f"Network or authentication error connecting to Cloud Run Admin API at {url}: {e}"
107
+ if self.service_account_email:
108
+ msg += f"\nFailed to impersonate {self.service_account_email}. Please ensure your GCP user account has the 'Service Account Token Creator' (roles/iam.serviceAccountTokenCreator) IAM role."
109
+ raise click.ClickException(msg)
110
+
111
+ if response.status_code == 401:
112
+ raise click.ClickException(
113
+ f"HTTP 401 Unauthorized when calling Cloud Run Admin API at {url}.\n"
114
+ "Your GCP credentials were rejected. Please verify your authentication.\n"
115
+ "To re-authenticate, run:\n"
116
+ " gcloud auth application-default login"
117
+ )
118
+
119
+ if not response.ok:
120
+ try:
121
+ error_data = response.json()
122
+ error_msg = (
123
+ error_data.get("message")
124
+ or error_data.get("error", {}).get("message")
125
+ or response.text
126
+ )
127
+ except Exception:
128
+ error_msg = response.text
129
+
130
+ raise click.ClickException(
131
+ f"Cloud Run Admin API returned HTTP {response.status_code}: {error_msg}"
132
+ )
133
+
134
+ try:
135
+ job_data = response.json()
136
+ except Exception as e:
137
+ raise click.ClickException(f"Failed to parse Cloud Run job response: {e}")
138
+
139
+ containers = (
140
+ job_data.get("template", {}).get("template", {}).get("containers", [])
141
+ )
142
+ if not containers:
143
+ return []
144
+
145
+ return containers[0].get("env", [])
@@ -0,0 +1,134 @@
1
+ # Copyright 2026 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import json
16
+ import shutil
17
+ import subprocess
18
+
19
+ import click
20
+
21
+
22
+ TF_OUTPUT_INGESTION_HELPER_URI = "dcp_ingestion_helper_uri"
23
+ TF_OUTPUT_ORCHESTRATOR_SERVICE_ACCOUNT_EMAIL = "dcp_orchestrator_service_account_email"
24
+ TF_OUTPUT_SPANNER_INSTANCE_ID = "dcp_spanner_instance_id"
25
+ TF_OUTPUT_SPANNER_DATABASE_ID = "dcp_spanner_database_id"
26
+ TF_OUTPUT_CDC_DATA_JOB_NAME = "cdc_data_job_name"
27
+ TF_OUTPUT_PROJECT_ID = "project_id"
28
+ TF_OUTPUT_REGION = "region"
29
+ TF_OUTPUT_WORKFLOW_NAME = "workflow_name"
30
+
31
+
32
+ def get_terraform_output(key: str) -> str:
33
+ """Fetches a specific key from `terraform output -json` with graceful error handling."""
34
+ if not shutil.which("terraform"):
35
+ raise click.ClickException(
36
+ "Terraform CLI not found. Please ensure Terraform is installed and available in your PATH."
37
+ )
38
+
39
+ try:
40
+ result = subprocess.run(
41
+ ["terraform", "output", "-json"],
42
+ capture_output=True,
43
+ text=True,
44
+ check=True,
45
+ )
46
+ except subprocess.CalledProcessError as e:
47
+ raise click.ClickException(
48
+ f"Failed to run 'terraform output'. Are you in an initialized Terraform deployment directory?\n"
49
+ f"Error details: {e.stderr.strip() or e.stdout.strip()}"
50
+ )
51
+
52
+ try:
53
+ outputs = json.loads(result.stdout)
54
+ except json.JSONDecodeError:
55
+ raise click.ClickException(
56
+ "Failed to parse 'terraform output -json'. The output was not valid JSON."
57
+ )
58
+
59
+ if not outputs:
60
+ from pathlib import Path
61
+
62
+ cwd = Path.cwd()
63
+ has_tf_files = (
64
+ (cwd / ".terraform").exists()
65
+ or (cwd / "terraform.tfstate").exists()
66
+ or (cwd / "main.tf").exists()
67
+ )
68
+
69
+ if not has_tf_files:
70
+ raise click.ClickException(
71
+ f"No Terraform outputs found in '{cwd}'.\n"
72
+ "Please navigate to your initialized DCP Terraform directory (e.g., 'cd my-namespace') and ensure 'terraform apply' has been run."
73
+ )
74
+ else:
75
+ raise click.ClickException(
76
+ f"No Terraform outputs found in '{cwd}'.\n"
77
+ "Please ensure you have successfully run 'terraform apply' to generate the deployment state."
78
+ )
79
+
80
+ if key not in outputs:
81
+ from pathlib import Path
82
+
83
+ raise click.ClickException(
84
+ f"Terraform output key '{key}' not found in '{Path.cwd()}'.\n"
85
+ "Please verify that your Terraform configuration exports this output and that 'terraform apply' was fully completed."
86
+ )
87
+
88
+ value = outputs[key].get("value")
89
+ if not value:
90
+ raise click.ClickException(
91
+ f"Terraform output '{key}' is empty or null. Please verify your deployment state."
92
+ )
93
+
94
+ return str(value)
95
+
96
+
97
+ def get_dcp_ingestion_helper_uri() -> str:
98
+ """Convenience wrapper to fetch the dcp_ingestion_helper_uri Terraform output."""
99
+ return get_terraform_output(TF_OUTPUT_INGESTION_HELPER_URI)
100
+
101
+
102
+ def get_dcp_orchestrator_service_account_email() -> str:
103
+ """Convenience wrapper to fetch the dcp_orchestrator_service_account_email Terraform output."""
104
+ return get_terraform_output(TF_OUTPUT_ORCHESTRATOR_SERVICE_ACCOUNT_EMAIL)
105
+
106
+
107
+ def get_dcp_spanner_instance_id() -> str:
108
+ """Convenience wrapper to fetch the dcp_spanner_instance_id Terraform output."""
109
+ return get_terraform_output(TF_OUTPUT_SPANNER_INSTANCE_ID)
110
+
111
+
112
+ def get_dcp_spanner_database_id() -> str:
113
+ """Convenience wrapper to fetch the dcp_spanner_database_id Terraform output."""
114
+ return get_terraform_output(TF_OUTPUT_SPANNER_DATABASE_ID)
115
+
116
+
117
+ def get_cdc_data_job_name() -> str:
118
+ """Convenience wrapper to fetch the cdc_data_job_name Terraform output."""
119
+ return get_terraform_output(TF_OUTPUT_CDC_DATA_JOB_NAME)
120
+
121
+
122
+ def get_dcp_project_id() -> str:
123
+ """Convenience wrapper to fetch the project_id Terraform output."""
124
+ return get_terraform_output(TF_OUTPUT_PROJECT_ID)
125
+
126
+
127
+ def get_dcp_region() -> str:
128
+ """Convenience wrapper to fetch the region Terraform output."""
129
+ return get_terraform_output(TF_OUTPUT_REGION)
130
+
131
+
132
+ def get_dcp_workflow_name() -> str:
133
+ """Convenience wrapper to fetch the workflow_name Terraform output."""
134
+ return get_terraform_output(TF_OUTPUT_WORKFLOW_NAME)
@@ -0,0 +1,15 @@
1
+ # Copyright 2026 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ __version__ = "0.0.1"
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: datacommons-admin
3
+ Version: 0.0.1
4
+ Summary: Data Commons Admin CLI
5
+ License-Expression: Apache-2.0
6
+ Project-URL: Homepage, https://github.com/datacommonsorg/datacommons
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: click>=8.1.7
10
+ Requires-Dist: google-cloud-storage>=2.13.0
11
+ Requires-Dist: pyopenssl>=24.0.0
12
+
13
+ # datacommons-admin
14
+
15
+ This package provides the `admin` command group consumed by the top-level `datacommons` CLI for initializing and managing Data Commons platform instances in GCP.
@@ -0,0 +1,12 @@
1
+ datacommons_admin/__init__.py,sha256=KFwbMH40mpcWY0d5sfAty6tGgGP6p_1ISu-_iKmCC70,67
2
+ datacommons_admin/admin_cli.py,sha256=LxgiMuheQkUycsdkI24u7Qecg265_hn-HA0Eo68JzJk,19614
3
+ datacommons_admin/infra_templates.py,sha256=ClStSytsSXgt-TkYfJU7y2bx1zCu5hsHL2QBawmzkUs,2717
4
+ datacommons_admin/ingest_cli.py,sha256=KtV2GTeTq1ELmW4AS1WyEybaHWP8xaH9Y5FXGNxbxOQ,5399
5
+ datacommons_admin/ingestion_helper_client.py,sha256=nGardf_n36GdQX0DPyIgu5rnaicAIIiAhYLFiMzBoNA,4415
6
+ datacommons_admin/ingestion_job_client.py,sha256=gF1KvbphgzK7eiawM_kDxwvUJdxl0jQDCEwWCxvkEoM,5851
7
+ datacommons_admin/tf_utils.py,sha256=vG8koG1kPZe8XjlSNGgGDMbPRFIcuhYhDljteUElaTk,4840
8
+ datacommons_admin/version.py,sha256=oSC2MsGURWbdZseFaniLJiHDa6kjClfg_2shID77ZBA,598
9
+ datacommons_admin-0.0.1.dist-info/METADATA,sha256=aHzjG8KinwUzYD8fM3sJT8M-ehyqCPEYF_EmO9I2yy4,546
10
+ datacommons_admin-0.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
11
+ datacommons_admin-0.0.1.dist-info/top_level.txt,sha256=fRm2N5tIvdOXEPJ3aA7zLXOqNb9T4wOrewI2ayx2Qro,18
12
+ datacommons_admin-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ datacommons_admin