WIP
This commit is contained in:
parent
492c30c0bc
commit
e8f38399fa
13 changed files with 949 additions and 106 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -16,3 +16,6 @@ generated_number
|
||||||
target
|
target
|
||||||
logs/databuild/
|
logs/databuild/
|
||||||
**/logs/databuild/
|
**/logs/databuild/
|
||||||
|
|
||||||
|
# DSL generated code
|
||||||
|
**/generated/
|
||||||
|
|
|
||||||
|
|
@ -34,13 +34,8 @@ async fn run_analysis(
|
||||||
.map_err(|_| CliError::Environment("DATABUILD_GRAPH_LABEL not set".to_string()))?;
|
.map_err(|_| CliError::Environment("DATABUILD_GRAPH_LABEL not set".to_string()))?;
|
||||||
|
|
||||||
// Find analyze binary using runfiles
|
// Find analyze binary using runfiles
|
||||||
let analyze_path = env::var("RUNFILES_DIR")
|
let analyze_path = env::var("DATABUILD_ANALYZE_BINARY")
|
||||||
.map(|runfiles_dir| format!("{}/databuild+/databuild/graph/analyze", runfiles_dir))
|
.map_err(|_| CliError::Environment("DATABUILD_ANALYZE_BINARY not set".to_string()))?;
|
||||||
.or_else(|_| {
|
|
||||||
// Fallback for direct execution
|
|
||||||
Ok("./databuild/graph/analyze".to_string())
|
|
||||||
})
|
|
||||||
.map_err(|e: std::env::VarError| CliError::Environment(format!("Failed to locate analyze binary: {}", e)))?;
|
|
||||||
|
|
||||||
// Build analyze command
|
// Build analyze command
|
||||||
let cmd = Command::new(analyze_path)
|
let cmd = Command::new(analyze_path)
|
||||||
|
|
@ -88,13 +83,8 @@ async fn run_execution(
|
||||||
let build_event_log_uri = env::var("DATABUILD_BUILD_EVENT_LOG").unwrap_or_else(|_| "stdout".to_string());
|
let build_event_log_uri = env::var("DATABUILD_BUILD_EVENT_LOG").unwrap_or_else(|_| "stdout".to_string());
|
||||||
|
|
||||||
// Find execute binary using runfiles
|
// Find execute binary using runfiles
|
||||||
let execute_path = env::var("RUNFILES_DIR")
|
let execute_path = env::var("DATABUILD_EXECUTE_BINARY")
|
||||||
.map(|runfiles_dir| format!("{}/databuild+/databuild/graph/execute", runfiles_dir))
|
.map_err(|_| CliError::Environment("DATABUILD_EXECUTE_BINARY not set".to_string()))?;
|
||||||
.or_else(|_| {
|
|
||||||
// Fallback for direct execution
|
|
||||||
Ok("./databuild/graph/execute".to_string())
|
|
||||||
})
|
|
||||||
.map_err(|e: std::env::VarError| CliError::Environment(format!("Failed to locate execute binary: {}", e)))?;
|
|
||||||
|
|
||||||
// Build execute command
|
// Build execute command
|
||||||
let mut cmd = Command::new(execute_path)
|
let mut cmd = Command::new(execute_path)
|
||||||
|
|
|
||||||
|
|
@ -6,3 +6,17 @@ py_library(
|
||||||
"//databuild:py_proto",
|
"//databuild:py_proto",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
py_binary(
|
||||||
|
name = "generator",
|
||||||
|
srcs = ["generator.py"],
|
||||||
|
main = "generator.py",
|
||||||
|
data = ["dsl_job_wrapper.py"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
":dsl",
|
||||||
|
"//databuild:py_proto",
|
||||||
|
# Include test app modules for testing
|
||||||
|
"//databuild/test/app/dsl:dsl_src",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,291 @@ class DataBuildGraph:
|
||||||
"""Generates a complete databuild application, packaging up referenced jobs and this graph via bazel targets"""
|
"""Generates a complete databuild application, packaging up referenced jobs and this graph via bazel targets"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def generate_bazel_package(self, name: str, output_dir: str) -> None:
|
||||||
|
"""Generate BUILD.bazel and binaries into a generated/ subdirectory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Base name for the generated graph (without .generate suffix)
|
||||||
|
output_dir: Directory to write generated files to (will create generated/ subdir)
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
# Create generated/ subdirectory
|
||||||
|
generated_dir = os.path.join(output_dir, "generated")
|
||||||
|
os.makedirs(generated_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Generate BUILD.bazel with job and graph targets
|
||||||
|
self._generate_build_bazel(generated_dir, name)
|
||||||
|
|
||||||
|
# Generate individual job scripts (instead of shared wrapper)
|
||||||
|
self._generate_job_scripts(generated_dir)
|
||||||
|
|
||||||
|
# Generate job lookup binary
|
||||||
|
self._generate_job_lookup(generated_dir, name)
|
||||||
|
|
||||||
|
package_name = self._get_package_name()
|
||||||
|
print(f"Generated DataBuild package '{name}' in {generated_dir}")
|
||||||
|
if package_name != "UNKNOWN_PACKAGE":
|
||||||
|
print(f"Run 'bazel build //{package_name}/generated:{name}_graph.analyze' to use the generated graph")
|
||||||
|
else:
|
||||||
|
print(f"Run 'bazel build generated:{name}_graph.analyze' to use the generated graph")
|
||||||
|
|
||||||
|
def _generate_build_bazel(self, output_dir: str, name: str) -> None:
|
||||||
|
"""Generate BUILD.bazel with databuild_job and databuild_graph targets."""
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Get job classes from the lookup table
|
||||||
|
job_classes = list(set(self.lookup.values()))
|
||||||
|
|
||||||
|
# Get parent package for dependencies
|
||||||
|
parent_package = self._get_package_name()
|
||||||
|
|
||||||
|
# Generate py_binary targets for each job
|
||||||
|
job_binaries = []
|
||||||
|
job_targets = []
|
||||||
|
|
||||||
|
for job_class in job_classes:
|
||||||
|
job_name = self._snake_case(job_class.__name__)
|
||||||
|
binary_name = f"{job_name}_binary"
|
||||||
|
job_targets.append(f'"{job_name}"')
|
||||||
|
|
||||||
|
job_script_name = f"{job_name}.py"
|
||||||
|
job_binaries.append(f'''py_binary(
|
||||||
|
name = "{binary_name}",
|
||||||
|
srcs = ["{job_script_name}"],
|
||||||
|
main = "{job_script_name}",
|
||||||
|
deps = ["//{parent_package}:dsl_src"],
|
||||||
|
)
|
||||||
|
|
||||||
|
databuild_job(
|
||||||
|
name = "{job_name}",
|
||||||
|
binary = ":{binary_name}",
|
||||||
|
)''')
|
||||||
|
|
||||||
|
# Generate the complete BUILD.bazel content
|
||||||
|
build_content = f'''load("@databuild//databuild:rules.bzl", "databuild_job", "databuild_graph")
|
||||||
|
|
||||||
|
# Generated by DataBuild DSL - do not edit manually
|
||||||
|
# This file is generated in a subdirectory to avoid overwriting the original BUILD.bazel
|
||||||
|
|
||||||
|
{chr(10).join(job_binaries)}
|
||||||
|
|
||||||
|
py_binary(
|
||||||
|
name = "{name}_job_lookup",
|
||||||
|
srcs = ["{name}_job_lookup.py"],
|
||||||
|
deps = ["//{parent_package}:dsl_src"],
|
||||||
|
)
|
||||||
|
|
||||||
|
databuild_graph(
|
||||||
|
name = "{name}_graph",
|
||||||
|
jobs = [{", ".join(job_targets)}],
|
||||||
|
lookup = ":{name}_job_lookup",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
|
'''
|
||||||
|
|
||||||
|
with open(os.path.join(output_dir, "BUILD.bazel"), "w") as f:
|
||||||
|
f.write(build_content)
|
||||||
|
|
||||||
|
def _generate_job_scripts(self, output_dir: str) -> None:
|
||||||
|
"""Generate individual Python scripts for each job class."""
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Get job classes and generate a script for each one
|
||||||
|
job_classes = list(set(self.lookup.values()))
|
||||||
|
graph_module_path = self._get_graph_module_path()
|
||||||
|
|
||||||
|
for job_class in job_classes:
|
||||||
|
job_name = self._snake_case(job_class.__name__)
|
||||||
|
script_name = f"{job_name}.py"
|
||||||
|
|
||||||
|
script_content = f'''#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Generated job script for {job_class.__name__}.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
from {graph_module_path} import {job_class.__name__}
|
||||||
|
from databuild.proto import PartitionRef, JobConfigureResponse, to_dict
|
||||||
|
|
||||||
|
|
||||||
|
def parse_outputs_from_args(args: list[str]) -> list:
|
||||||
|
"""Parse partition output references from command line arguments."""
|
||||||
|
outputs = []
|
||||||
|
for arg in args:
|
||||||
|
# Find which output type can deserialize this partition reference
|
||||||
|
for output_type in {job_class.__name__}.output_types:
|
||||||
|
try:
|
||||||
|
partition = output_type.deserialize(arg)
|
||||||
|
outputs.append(partition)
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise ValueError(f"No output type in {job_class.__name__} can deserialize partition ref: {{arg}}")
|
||||||
|
|
||||||
|
return outputs
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
raise Exception(f"Invalid command usage")
|
||||||
|
|
||||||
|
command = sys.argv[1]
|
||||||
|
job_instance = {job_class.__name__}()
|
||||||
|
|
||||||
|
if command == "config":
|
||||||
|
# Parse output partition references as PartitionRef objects (for Rust wrapper)
|
||||||
|
output_refs = [PartitionRef(str=raw_ref) for raw_ref in sys.argv[2:]]
|
||||||
|
|
||||||
|
# Also parse them into DSL partition objects (for DSL job.config())
|
||||||
|
outputs = parse_outputs_from_args(sys.argv[2:])
|
||||||
|
|
||||||
|
# Call job's config method - returns list[JobConfig]
|
||||||
|
configs = job_instance.config(outputs)
|
||||||
|
|
||||||
|
# Wrap in JobConfigureResponse and serialize using to_dict()
|
||||||
|
response = JobConfigureResponse(configs=configs)
|
||||||
|
print(json.dumps(to_dict(response)))
|
||||||
|
|
||||||
|
elif command == "exec":
|
||||||
|
# The exec method expects a JobConfig but the Rust wrapper passes args
|
||||||
|
# For now, let the DSL job handle the args directly
|
||||||
|
# TODO: This needs to be refined based on actual Rust wrapper interface
|
||||||
|
job_instance.exec(sys.argv[2:])
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception(f"Invalid command `{{sys.argv[1]}}`")
|
||||||
|
'''
|
||||||
|
|
||||||
|
script_path = os.path.join(output_dir, script_name)
|
||||||
|
with open(script_path, "w") as f:
|
||||||
|
f.write(script_content)
|
||||||
|
|
||||||
|
# Make it executable
|
||||||
|
os.chmod(script_path, 0o755)
|
||||||
|
|
||||||
|
def _generate_job_lookup(self, output_dir: str, name: str) -> None:
|
||||||
|
"""Generate job lookup binary that maps partition patterns to job targets."""
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Build the job lookup mappings with full package paths
|
||||||
|
package_name = self._get_package_name()
|
||||||
|
lookup_mappings = []
|
||||||
|
for partition_type, job_class in self.lookup.items():
|
||||||
|
job_name = self._snake_case(job_class.__name__)
|
||||||
|
pattern = partition_type._raw_pattern
|
||||||
|
full_target = f"//{package_name}/generated:{job_name}"
|
||||||
|
lookup_mappings.append(f' r"{pattern}": "{full_target}",')
|
||||||
|
|
||||||
|
lookup_content = f'''#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Generated job lookup for DataBuild DSL graph.
|
||||||
|
Maps partition patterns to job targets.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
|
||||||
|
# Mapping from partition patterns to job targets
|
||||||
|
JOB_MAPPINGS = {{
|
||||||
|
{chr(10).join(lookup_mappings)}
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
def lookup_job_for_partition(partition_ref: str) -> str:
|
||||||
|
"""Look up which job can build the given partition reference."""
|
||||||
|
for pattern, job_target in JOB_MAPPINGS.items():
|
||||||
|
if re.match(pattern, partition_ref):
|
||||||
|
return job_target
|
||||||
|
|
||||||
|
raise ValueError(f"No job found for partition: {{partition_ref}}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: job_lookup.py <partition_ref> [partition_ref...]", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
results = defaultdict(list)
|
||||||
|
try:
|
||||||
|
for partition_ref in sys.argv[1:]:
|
||||||
|
job_target = lookup_job_for_partition(partition_ref)
|
||||||
|
results[job_target].append(partition_ref)
|
||||||
|
|
||||||
|
# Output the results as JSON (matching existing lookup format)
|
||||||
|
print(json.dumps(dict(results)))
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"ERROR: {{e}}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
'''
|
||||||
|
|
||||||
|
lookup_file = os.path.join(output_dir, f"{name}_job_lookup.py")
|
||||||
|
with open(lookup_file, "w") as f:
|
||||||
|
f.write(lookup_content)
|
||||||
|
|
||||||
|
# Make it executable
|
||||||
|
os.chmod(lookup_file, 0o755)
|
||||||
|
|
||||||
|
def _snake_case(self, name: str) -> str:
|
||||||
|
"""Convert CamelCase to snake_case."""
|
||||||
|
import re
|
||||||
|
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
||||||
|
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
||||||
|
|
||||||
|
def _get_graph_module_path(self) -> str:
|
||||||
|
"""Get the module path for the graph containing this instance."""
|
||||||
|
# Try to find the module by looking at where the graph object is defined
|
||||||
|
import inspect
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Look through all loaded modules to find where this graph instance is defined
|
||||||
|
for module_name, module in sys.modules.items():
|
||||||
|
if hasattr(module, 'graph') and getattr(module, 'graph') is self:
|
||||||
|
if module_name != '__main__':
|
||||||
|
return module_name
|
||||||
|
|
||||||
|
# Look through the call stack to find the module that imported us
|
||||||
|
for frame_info in inspect.stack():
|
||||||
|
frame_globals = frame_info.frame.f_globals
|
||||||
|
module_name = frame_globals.get('__name__')
|
||||||
|
if module_name and module_name != '__main__' and 'graph' in frame_globals:
|
||||||
|
# Check if this frame has our graph
|
||||||
|
if frame_globals.get('graph') is self:
|
||||||
|
return module_name
|
||||||
|
|
||||||
|
# Last resort fallback - this will need to be manually configured
|
||||||
|
return "UNKNOWN_MODULE"
|
||||||
|
|
||||||
|
def _get_package_name(self) -> str:
|
||||||
|
"""Get the Bazel package name where the DSL source files are located."""
|
||||||
|
# Extract package from the graph label if available
|
||||||
|
if hasattr(self, 'label') and self.label.startswith('//'):
|
||||||
|
# Extract package from label like "//databuild/test/app:dsl_graph"
|
||||||
|
package_part = self.label.split(':')[0]
|
||||||
|
return package_part[2:] # Remove "//" prefix
|
||||||
|
|
||||||
|
# Fallback to trying to infer from module path
|
||||||
|
module_path = self._get_graph_module_path()
|
||||||
|
if module_path != "UNKNOWN_MODULE":
|
||||||
|
# Convert module path to package path
|
||||||
|
# e.g., "databuild.test.app.dsl.graph" -> "databuild/test/app/dsl"
|
||||||
|
parts = module_path.split('.')
|
||||||
|
if parts[-1] in ['graph', 'main']:
|
||||||
|
parts = parts[:-1]
|
||||||
|
return '/'.join(parts)
|
||||||
|
|
||||||
|
return "UNKNOWN_PACKAGE"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class JobConfigBuilder:
|
class JobConfigBuilder:
|
||||||
|
|
|
||||||
142
databuild/dsl/python/dsl_job_wrapper.py
Normal file
142
databuild/dsl/python/dsl_job_wrapper.py
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Shared DSL job wrapper that can execute any DataBuildJob defined in a DSL graph.
|
||||||
|
Configured via environment variables:
|
||||||
|
- DATABUILD_DSL_GRAPH_MODULE: Python module path containing the graph (e.g., 'databuild.test.app.dsl.graph')
|
||||||
|
- DATABUILD_JOB_CLASS: Job class name to execute (e.g., 'IngestColorVotes')
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import importlib
|
||||||
|
from typing import List, Any
|
||||||
|
from databuild.proto import JobConfig
|
||||||
|
|
||||||
|
|
||||||
|
def parse_outputs_from_args(args: List[str], job_class: Any) -> List[Any]:
|
||||||
|
"""Parse partition output references from command line arguments into partition objects."""
|
||||||
|
outputs = []
|
||||||
|
for arg in args:
|
||||||
|
# Find which output type can deserialize this partition reference
|
||||||
|
for output_type in job_class.output_types:
|
||||||
|
try:
|
||||||
|
partition = output_type.deserialize(arg)
|
||||||
|
outputs.append(partition)
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise ValueError(f"No output type in {job_class.__name__} can deserialize partition ref: {arg}")
|
||||||
|
|
||||||
|
return outputs
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: dsl_job_wrapper.py <config|exec> [args...]", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
command = sys.argv[1]
|
||||||
|
|
||||||
|
# Read configuration from environment
|
||||||
|
graph_module_path = os.environ.get('DATABUILD_DSL_GRAPH_MODULE')
|
||||||
|
job_class_name = os.environ.get('DATABUILD_JOB_CLASS')
|
||||||
|
|
||||||
|
if not graph_module_path:
|
||||||
|
print("ERROR: DATABUILD_DSL_GRAPH_MODULE environment variable not set", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not job_class_name:
|
||||||
|
print("ERROR: DATABUILD_JOB_CLASS environment variable not set", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Import the graph module
|
||||||
|
module = importlib.import_module(graph_module_path)
|
||||||
|
graph = getattr(module, 'graph')
|
||||||
|
|
||||||
|
# Get the job class
|
||||||
|
job_class = getattr(module, job_class_name)
|
||||||
|
|
||||||
|
# Create job instance
|
||||||
|
job_instance = job_class()
|
||||||
|
|
||||||
|
except (ImportError, AttributeError) as e:
|
||||||
|
print(f"ERROR: Failed to load job {job_class_name} from {graph_module_path}: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if command == "config":
|
||||||
|
try:
|
||||||
|
# Parse output partition references from remaining args
|
||||||
|
output_refs = sys.argv[2:]
|
||||||
|
if not output_refs:
|
||||||
|
print("ERROR: No output partition references provided", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
outputs = parse_outputs_from_args(output_refs, job_class)
|
||||||
|
|
||||||
|
# Call job's config method
|
||||||
|
configs = job_instance.config(outputs)
|
||||||
|
|
||||||
|
# Output each config as JSON (one per line for multiple configs)
|
||||||
|
for config in configs:
|
||||||
|
# Convert JobConfig to dict for JSON serialization
|
||||||
|
config_dict = {
|
||||||
|
'outputs': [{'str': ref.str} for ref in config.outputs],
|
||||||
|
'inputs': [
|
||||||
|
{
|
||||||
|
'dep_type_code': dep.dep_type_code,
|
||||||
|
'dep_type_name': dep.dep_type_name,
|
||||||
|
'partition_ref': {'str': dep.partition_ref.str}
|
||||||
|
} for dep in config.inputs
|
||||||
|
],
|
||||||
|
'args': config.args,
|
||||||
|
'env': config.env,
|
||||||
|
}
|
||||||
|
print(json.dumps(config_dict))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Config failed: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
elif command == "exec":
|
||||||
|
try:
|
||||||
|
# Read config from stdin
|
||||||
|
config_json = sys.stdin.read().strip()
|
||||||
|
if not config_json:
|
||||||
|
print("ERROR: No config provided on stdin", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
config_dict = json.loads(config_json)
|
||||||
|
|
||||||
|
# Convert dict back to JobConfig
|
||||||
|
from databuild.proto import PartitionRef, DataDep, DepType
|
||||||
|
|
||||||
|
config = JobConfig(
|
||||||
|
outputs=[PartitionRef(str=ref['str']) for ref in config_dict['outputs']],
|
||||||
|
inputs=[
|
||||||
|
DataDep(
|
||||||
|
dep_type_code=dep['dep_type_code'],
|
||||||
|
dep_type_name=dep['dep_type_name'],
|
||||||
|
partition_ref=PartitionRef(str=dep['partition_ref']['str'])
|
||||||
|
) for dep in config_dict['inputs']
|
||||||
|
],
|
||||||
|
args=config_dict['args'],
|
||||||
|
env=config_dict['env'],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Call job's exec method
|
||||||
|
job_instance.exec(config)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Execution failed: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"ERROR: Unknown command '{command}'. Use 'config' or 'exec'.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
47
databuild/dsl/python/generator.py
Normal file
47
databuild/dsl/python/generator.py
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
DSL code generator that can be run as a py_binary with proper dependencies.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) != 4:
|
||||||
|
print("Usage: generator.py <module_path> <graph_attr> <output_dir>", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
module_path = sys.argv[1]
|
||||||
|
graph_attr = sys.argv[2]
|
||||||
|
output_dir = sys.argv[3]
|
||||||
|
|
||||||
|
# Extract the base name from the output directory for naming
|
||||||
|
name = os.path.basename(output_dir.rstrip('/')) or "graph"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Import the graph module
|
||||||
|
module = importlib.import_module(module_path)
|
||||||
|
graph = getattr(module, graph_attr)
|
||||||
|
|
||||||
|
# Generate the bazel package
|
||||||
|
graph.generate_bazel_package(name, output_dir)
|
||||||
|
|
||||||
|
print(f"Generated DataBuild DSL package in {output_dir}")
|
||||||
|
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"ERROR: Failed to import {graph_attr} from {module_path}: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
except AttributeError as e:
|
||||||
|
print(f"ERROR: Module {module_path} does not have attribute {graph_attr}: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Generation failed: {e}", file=sys.stderr)
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -37,6 +37,14 @@ pub struct SqliteBuildEventLog {
|
||||||
|
|
||||||
impl SqliteBuildEventLog {
|
impl SqliteBuildEventLog {
|
||||||
pub async fn new(path: &str) -> Result<Self> {
|
pub async fn new(path: &str) -> Result<Self> {
|
||||||
|
// Create parent directory if it doesn't exist
|
||||||
|
if let Some(parent) = std::path::Path::new(path).parent() {
|
||||||
|
std::fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| BuildEventLogError::ConnectionError(
|
||||||
|
format!("Failed to create directory {}: {}", parent.display(), e)
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
|
||||||
let conn = Connection::open(path)
|
let conn = Connection::open(path)
|
||||||
.map_err(|e| BuildEventLogError::ConnectionError(e.to_string()))?;
|
.map_err(|e| BuildEventLogError::ConnectionError(e.to_string()))?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ use std::io::{BufReader, Read, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
use std::{env, thread};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
// Command line parsing removed - using environment variables
|
// Command line parsing removed - using environment variables
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
@ -52,48 +52,6 @@ fn get_task_key(task: &Task) -> String {
|
||||||
key_parts.join("|")
|
key_parts.join("|")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolves the executable path from runfiles.
|
|
||||||
// Mirrors the Go implementation's resolveExecutableFromRunfiles.
|
|
||||||
fn resolve_executable_from_runfiles(job_label: &str) -> PathBuf {
|
|
||||||
let mut target_name = job_label.to_string();
|
|
||||||
if let Some(colon_index) = job_label.rfind(':') {
|
|
||||||
target_name = job_label[colon_index + 1..].to_string();
|
|
||||||
} else if let Some(name) = Path::new(job_label).file_name().and_then(|n| n.to_str()) {
|
|
||||||
target_name = name.to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
let exec_name = format!("{}.exec", target_name);
|
|
||||||
|
|
||||||
if let Ok(runfiles_dir_str) = std::env::var("RUNFILES_DIR") {
|
|
||||||
let path = PathBuf::from(runfiles_dir_str).join("_main").join(&exec_name);
|
|
||||||
debug!("Resolved executable path (RUNFILES_DIR): {}", path.display());
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(current_exe) = std::env::current_exe() {
|
|
||||||
let mut runfiles_dir_path = PathBuf::from(format!("{}.runfiles", current_exe.display()));
|
|
||||||
if !runfiles_dir_path.is_dir() { // Bazel often puts it next to the binary
|
|
||||||
if let Some(parent) = current_exe.parent() {
|
|
||||||
runfiles_dir_path = parent.join(format!("{}.runfiles", current_exe.file_name().unwrap_or_default().to_string_lossy()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if runfiles_dir_path.is_dir() {
|
|
||||||
let path = runfiles_dir_path.join("_main").join(&exec_name);
|
|
||||||
debug!("Resolved executable path (derived RUNFILES_DIR): {}", path.display());
|
|
||||||
return path;
|
|
||||||
} else {
|
|
||||||
warn!("Warning: RUNFILES_DIR not found or invalid, and derived path {} is not a directory.", runfiles_dir_path.display());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
warn!("Warning: Could not determine current executable path.");
|
|
||||||
}
|
|
||||||
|
|
||||||
let fallback_path = PathBuf::from(format!("{}.exec", job_label));
|
|
||||||
warn!("Falling back to direct executable path: {}", fallback_path.display());
|
|
||||||
fallback_path
|
|
||||||
}
|
|
||||||
|
|
||||||
fn worker(
|
fn worker(
|
||||||
task_rx: Receiver<Arc<Task>>,
|
task_rx: Receiver<Arc<Task>>,
|
||||||
result_tx: Sender<TaskExecutionResult>,
|
result_tx: Sender<TaskExecutionResult>,
|
||||||
|
|
@ -105,7 +63,8 @@ fn worker(
|
||||||
info!("[Worker {}] Starting job: {} (Key: {})", worker_id, task.job.as_ref().unwrap().label, task_key);
|
info!("[Worker {}] Starting job: {} (Key: {})", worker_id, task.job.as_ref().unwrap().label, task_key);
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
|
|
||||||
let exec_path = resolve_executable_from_runfiles(&task.job.as_ref().unwrap().label);
|
let exec_path = env::var("DATABUILD_EXECUTE_BINARY")
|
||||||
|
.map_err(|e| Error::from(format!("Env var DATABUILD_EXECUTE_BINARY not set: {}", e)));
|
||||||
|
|
||||||
let config_json = match serde_json::to_string(&task.config.as_ref().unwrap()) {
|
let config_json = match serde_json::to_string(&task.config.as_ref().unwrap()) {
|
||||||
Ok(json) => json,
|
Ok(json) => json,
|
||||||
|
|
|
||||||
|
|
@ -651,21 +651,52 @@ def _databuild_graph_build_impl(ctx):
|
||||||
|
|
||||||
script_content = RUNFILES_PREFIX + """
|
script_content = RUNFILES_PREFIX + """
|
||||||
# Build DATABUILD_CANDIDATE_JOBS dynamically with proper rlocation resolution
|
# Build DATABUILD_CANDIDATE_JOBS dynamically with proper rlocation resolution
|
||||||
%s
|
{candidate_jobs_script}
|
||||||
|
|
||||||
export DATABUILD_CANDIDATE_JOBS="$CANDIDATE_JOBS_JSON"
|
export DATABUILD_CANDIDATE_JOBS="$CANDIDATE_JOBS_JSON"
|
||||||
export DATABUILD_JOB_LOOKUP_PATH="$(rlocation _main/%s)"
|
|
||||||
export DATABUILD_GRAPH_LABEL="%s"
|
# Resolve binary paths with error checking
|
||||||
|
DATABUILD_JOB_LOOKUP_PATH="$(rlocation _main/{lookup_path})"
|
||||||
|
if [[ -z "$DATABUILD_JOB_LOOKUP_PATH" || ! -f "$DATABUILD_JOB_LOOKUP_PATH" ]]; then
|
||||||
|
echo "ERROR: Could not find job lookup binary at _main/{lookup_path}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
export DATABUILD_JOB_LOOKUP_PATH
|
||||||
|
|
||||||
|
DATABUILD_ANALYZE_BINARY="$(rlocation _main/{analyze_path})"
|
||||||
|
if [[ -z "$DATABUILD_ANALYZE_BINARY" || ! -f "$DATABUILD_ANALYZE_BINARY" ]]; then
|
||||||
|
echo "ERROR: Could not find analyze binary at _main/{analyze_path}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
export DATABUILD_ANALYZE_BINARY
|
||||||
|
|
||||||
|
DATABUILD_EXECUTE_BINARY="$(rlocation _main/{execute_path})"
|
||||||
|
if [[ -z "$DATABUILD_EXECUTE_BINARY" || ! -f "$DATABUILD_EXECUTE_BINARY" ]]; then
|
||||||
|
echo "ERROR: Could not find execute binary at _main/{execute_path}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
export DATABUILD_EXECUTE_BINARY
|
||||||
|
|
||||||
|
export DATABUILD_GRAPH_LABEL="{graph_label}"
|
||||||
|
|
||||||
# Generate a single build request ID for the entire CLI operation
|
# Generate a single build request ID for the entire CLI operation
|
||||||
export DATABUILD_BUILD_REQUEST_ID=$(python3 -c "import uuid; print(uuid.uuid4())")
|
export DATABUILD_BUILD_REQUEST_ID=$(python3 -c "import uuid; print(uuid.uuid4())")
|
||||||
|
|
||||||
# Run unified DataBuild CLI wrapper
|
# Run unified DataBuild CLI wrapper using the provided cli_wrapper attribute
|
||||||
"$(rlocation databuild+/databuild/cli/databuild_cli)" "$@"
|
CLI_BINARY="$(rlocation _main/{cli_path})"
|
||||||
""" % (
|
if [[ -z "$CLI_BINARY" || ! -f "$CLI_BINARY" ]]; then
|
||||||
candidate_jobs_script,
|
echo "ERROR: Could not find CLI binary at _main/{cli_path}" >&2
|
||||||
ctx.attr.lookup.files_to_run.executable.short_path,
|
exit 1
|
||||||
ctx.attr.graph_label,
|
fi
|
||||||
|
|
||||||
|
"$CLI_BINARY" "$@"
|
||||||
|
""".format(
|
||||||
|
candidate_jobs_script = candidate_jobs_script,
|
||||||
|
lookup_path = ctx.attr.lookup.files_to_run.executable.short_path,
|
||||||
|
analyze_path = ctx.attr._analyze.files_to_run.executable.short_path,
|
||||||
|
execute_path = ctx.attr._execute.files_to_run.executable.short_path,
|
||||||
|
graph_label = ctx.attr.graph_label,
|
||||||
|
cli_path = ctx.attr.cli_wrapper.files_to_run.executable.short_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
ctx.actions.write(
|
ctx.actions.write(
|
||||||
|
|
@ -674,58 +705,29 @@ export DATABUILD_BUILD_REQUEST_ID=$(python3 -c "import uuid; print(uuid.uuid4())
|
||||||
content = script_content,
|
content = script_content,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Gather the configure and execute executables
|
# Gather the configure executables
|
||||||
configure_executables = [
|
configure_executables = [
|
||||||
job[DataBuildJobInfo].configure.files_to_run.executable
|
job[DataBuildJobInfo].configure.files_to_run.executable
|
||||||
for job in ctx.attr.jobs
|
for job in ctx.attr.jobs
|
||||||
]
|
]
|
||||||
|
|
||||||
# Get the execute targets - these are the .exec files that need to be in runfiles
|
# Create runfiles including the CLI binary, analyze/execute binaries and all dependencies
|
||||||
execute_executables = []
|
|
||||||
for job in ctx.attr.jobs:
|
|
||||||
# The job target itself contains references to both configure and execute
|
|
||||||
# We need to find the .exec target for each job
|
|
||||||
job_name = job.label.name
|
|
||||||
exec_target_name = job_name + ".exec"
|
|
||||||
|
|
||||||
# Find the .exec target in the same package
|
|
||||||
for attr_name in dir(job):
|
|
||||||
if attr_name.endswith("_exec") or exec_target_name in attr_name:
|
|
||||||
exec_target = getattr(job, attr_name, None)
|
|
||||||
if exec_target and hasattr(exec_target, "files_to_run"):
|
|
||||||
execute_executables.append(exec_target.files_to_run.executable)
|
|
||||||
break
|
|
||||||
|
|
||||||
# Also check if we can access exec targets directly from job dependencies
|
|
||||||
all_job_files = []
|
|
||||||
for job in ctx.attr.jobs:
|
|
||||||
if hasattr(job, "default_runfiles") and job.default_runfiles:
|
|
||||||
all_job_files.extend(job.default_runfiles.files.to_list())
|
|
||||||
|
|
||||||
runfiles = ctx.runfiles(
|
runfiles = ctx.runfiles(
|
||||||
files = [ctx.executable.cli_wrapper, ctx.executable.lookup] + configure_executables + execute_executables + all_job_files,
|
files = [ctx.executable.cli_wrapper, ctx.executable.lookup, ctx.executable._analyze, ctx.executable._execute] + configure_executables,
|
||||||
).merge(ctx.attr.cli_wrapper.default_runfiles).merge(ctx.attr.lookup.default_runfiles).merge(
|
).merge(ctx.attr.cli_wrapper.default_runfiles).merge(ctx.attr.lookup.default_runfiles).merge(
|
||||||
ctx.attr._bash_runfiles.default_runfiles,
|
ctx.attr._analyze.default_runfiles,
|
||||||
)
|
).merge(ctx.attr._execute.default_runfiles).merge(ctx.attr._bash_runfiles.default_runfiles).merge_all([job.default_runfiles for job in ctx.attr.jobs])
|
||||||
|
|
||||||
# Merge runfiles from all configure targets and job targets
|
# Merge runfiles from all configure targets
|
||||||
for job in ctx.attr.jobs:
|
for job in ctx.attr.jobs:
|
||||||
configure_target = job[DataBuildJobInfo].configure
|
configure_target = job[DataBuildJobInfo].configure
|
||||||
runfiles = runfiles.merge(configure_target.default_runfiles)
|
runfiles = runfiles.merge(configure_target.default_runfiles)
|
||||||
|
|
||||||
# Also merge the job's own runfiles which should include the .exec target
|
|
||||||
runfiles = runfiles.merge(job.default_runfiles)
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
DefaultInfo(
|
DefaultInfo(
|
||||||
executable = script,
|
executable = script,
|
||||||
runfiles = runfiles,
|
runfiles = runfiles,
|
||||||
),
|
),
|
||||||
DataBuildGraphInfo(
|
|
||||||
analyze = ctx.attr.cli_wrapper,
|
|
||||||
exec = ctx.attr.cli_wrapper,
|
|
||||||
jobs = ctx.attr.jobs,
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
_databuild_graph_build = rule(
|
_databuild_graph_build = rule(
|
||||||
|
|
@ -755,6 +757,16 @@ _databuild_graph_build = rule(
|
||||||
default = Label("@bazel_tools//tools/bash/runfiles"),
|
default = Label("@bazel_tools//tools/bash/runfiles"),
|
||||||
allow_files = True,
|
allow_files = True,
|
||||||
),
|
),
|
||||||
|
"_analyze": attr.label(
|
||||||
|
default = "@databuild//databuild/graph:analyze",
|
||||||
|
executable = True,
|
||||||
|
cfg = "target",
|
||||||
|
),
|
||||||
|
"_execute": attr.label(
|
||||||
|
default = "@databuild//databuild/graph:execute",
|
||||||
|
executable = True,
|
||||||
|
cfg = "target",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
executable = True,
|
executable = True,
|
||||||
)
|
)
|
||||||
|
|
@ -902,3 +914,186 @@ _databuild_graph_service = rule(
|
||||||
},
|
},
|
||||||
executable = True,
|
executable = True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def databuild_dsl_generator(
|
||||||
|
name,
|
||||||
|
graph_file,
|
||||||
|
graph_attr = "graph",
|
||||||
|
output_package = None,
|
||||||
|
deps = [],
|
||||||
|
visibility = None):
|
||||||
|
"""Creates a DataBuild DSL code generator that can generate BUILD.bazel and job binaries.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Name of the generator target (typically ends with .generate)
|
||||||
|
graph_file: Python file containing the DSL graph definition
|
||||||
|
graph_attr: Name of the graph attribute in the module (default: "graph")
|
||||||
|
output_package: Target package for generated files (default: current package)
|
||||||
|
deps: Dependencies needed to load the graph
|
||||||
|
visibility: Visibility specification
|
||||||
|
"""
|
||||||
|
if not output_package:
|
||||||
|
output_package = "//" + native.package_name()
|
||||||
|
|
||||||
|
_databuild_dsl_generator_rule(
|
||||||
|
name = name,
|
||||||
|
graph_file = graph_file,
|
||||||
|
graph_attr = graph_attr,
|
||||||
|
output_package = output_package,
|
||||||
|
deps = deps,
|
||||||
|
visibility = visibility,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _databuild_dsl_generator_impl(ctx):
|
||||||
|
"""Implementation of the DSL generator rule."""
|
||||||
|
script = ctx.actions.declare_file(ctx.label.name + "_generator.py")
|
||||||
|
|
||||||
|
# Get the module path from the graph file
|
||||||
|
graph_file_path = ctx.file.graph_file.short_path
|
||||||
|
if graph_file_path.endswith(".py"):
|
||||||
|
graph_file_path = graph_file_path[:-3]
|
||||||
|
module_path = graph_file_path.replace("/", ".")
|
||||||
|
|
||||||
|
# Get the package path for output
|
||||||
|
package_path = ctx.attr.output_package.strip("//").replace(":", "/")
|
||||||
|
|
||||||
|
script_content = """#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Determine output directory
|
||||||
|
workspace_root = os.environ.get('BUILD_WORKSPACE_DIRECTORY')
|
||||||
|
if workspace_root:
|
||||||
|
# Running with bazel run - write to source tree
|
||||||
|
output_dir = os.path.join(workspace_root, '{package_path}')
|
||||||
|
else:
|
||||||
|
# Running with bazel build - write to current directory (bazel-bin)
|
||||||
|
output_dir = '.'
|
||||||
|
|
||||||
|
print(f"Generating DataBuild DSL code to {{output_dir}}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Setup runfiles for module resolution
|
||||||
|
try:
|
||||||
|
from python.runfiles import runfiles
|
||||||
|
r = runfiles.Create()
|
||||||
|
if r:
|
||||||
|
generator_path = r.Rlocation("databuild/databuild/dsl/python/generator")
|
||||||
|
else:
|
||||||
|
raise ImportError("runfiles not available")
|
||||||
|
except ImportError:
|
||||||
|
# Fallback - assume generator is in the same directory as this script
|
||||||
|
import subprocess
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
# Try to find the generator binary using bazel
|
||||||
|
generator_path = shutil.which("bazel")
|
||||||
|
if generator_path:
|
||||||
|
import subprocess
|
||||||
|
result = subprocess.run([
|
||||||
|
"bazel", "run", "//databuild/dsl/python:generator", "--",
|
||||||
|
"{module_path}", "{graph_attr}", output_dir
|
||||||
|
], cwd=workspace_root if workspace_root else ".", capture_output=True, text=True)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"ERROR: Generation failed:", file=sys.stderr)
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(result.stdout)
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
print("ERROR: Could not find bazel to run generator", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Run the generator with proper module resolution
|
||||||
|
import subprocess
|
||||||
|
result = subprocess.run([
|
||||||
|
generator_path, "{module_path}", "{graph_attr}", output_dir
|
||||||
|
], capture_output=True, text=True)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"ERROR: Generation failed:", file=sys.stderr)
|
||||||
|
print(result.stderr, file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(result.stdout)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Generation failed: {{e}}", file=sys.stderr)
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
|
""".format(
|
||||||
|
package_path = package_path,
|
||||||
|
module_path = module_path,
|
||||||
|
graph_attr = ctx.attr.graph_attr,
|
||||||
|
name = ctx.attr.name.replace(".generate", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx.actions.write(
|
||||||
|
output = script,
|
||||||
|
content = script_content,
|
||||||
|
is_executable = True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create runfiles with all dependencies
|
||||||
|
runfiles = ctx.runfiles(
|
||||||
|
files = [ctx.file.graph_file, ctx.executable._generator],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Merge runfiles from all dependencies
|
||||||
|
for dep in ctx.attr.deps:
|
||||||
|
if hasattr(dep, "default_runfiles"):
|
||||||
|
runfiles = runfiles.merge(dep.default_runfiles)
|
||||||
|
|
||||||
|
# Add protobuf dependencies
|
||||||
|
if hasattr(ctx.attr._py_proto, "default_runfiles"):
|
||||||
|
runfiles = runfiles.merge(ctx.attr._py_proto.default_runfiles)
|
||||||
|
|
||||||
|
# Add generator runfiles
|
||||||
|
runfiles = runfiles.merge(ctx.attr._generator.default_runfiles)
|
||||||
|
|
||||||
|
# Also add Python runfiles for proper module resolution
|
||||||
|
runfiles = runfiles.merge(ctx.attr._python_runfiles.default_runfiles)
|
||||||
|
|
||||||
|
return [DefaultInfo(
|
||||||
|
executable = script,
|
||||||
|
runfiles = runfiles,
|
||||||
|
)]
|
||||||
|
|
||||||
|
_databuild_dsl_generator_rule = rule(
|
||||||
|
implementation = _databuild_dsl_generator_impl,
|
||||||
|
attrs = {
|
||||||
|
"graph_file": attr.label(
|
||||||
|
doc = "Python file containing the DSL graph definition",
|
||||||
|
allow_single_file = [".py"],
|
||||||
|
mandatory = True,
|
||||||
|
),
|
||||||
|
"graph_attr": attr.string(
|
||||||
|
doc = "Name of the graph attribute in the module",
|
||||||
|
default = "graph",
|
||||||
|
),
|
||||||
|
"output_package": attr.string(
|
||||||
|
doc = "Target package for generated files",
|
||||||
|
mandatory = True,
|
||||||
|
),
|
||||||
|
"deps": attr.label_list(
|
||||||
|
doc = "Dependencies needed to load the graph",
|
||||||
|
allow_empty = True,
|
||||||
|
),
|
||||||
|
"_python_runfiles": attr.label(
|
||||||
|
default = "@rules_python//python/runfiles",
|
||||||
|
allow_files = True,
|
||||||
|
),
|
||||||
|
"_py_proto": attr.label(
|
||||||
|
default = "//databuild:py_proto",
|
||||||
|
),
|
||||||
|
"_generator": attr.label(
|
||||||
|
default = "//databuild/dsl/python:generator",
|
||||||
|
executable = True,
|
||||||
|
cfg = "target",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
executable = True,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
load("@databuild//databuild:rules.bzl", "databuild_dsl_generator")
|
||||||
|
|
||||||
py_library(
|
py_library(
|
||||||
name = "dsl_src",
|
name = "dsl_src",
|
||||||
srcs = glob(
|
srcs = glob(
|
||||||
|
|
@ -11,3 +13,12 @@ py_library(
|
||||||
"//databuild/test/app:job_src",
|
"//databuild/test/app:job_src",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
databuild_dsl_generator(
|
||||||
|
name = "graph.generate",
|
||||||
|
graph_file = "graph.py",
|
||||||
|
graph_attr = "graph",
|
||||||
|
output_package = "//databuild/test/app/dsl",
|
||||||
|
deps = [":dsl_src"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
|
|
|
||||||
47
databuild/test/app/dsl/dsl_job_lookup.py
Executable file
47
databuild/test/app/dsl/dsl_job_lookup.py
Executable file
|
|
@ -0,0 +1,47 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Generated job lookup for DataBuild DSL graph.
|
||||||
|
Maps partition patterns to job targets.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
# Mapping from partition patterns to job targets
|
||||||
|
JOB_MAPPINGS = {
|
||||||
|
r"daily_color_votes/(?P<data_date>\d{4}-\d{2}-\d{2})/(?P<color>[^/]+)": ":ingest_color_votes",
|
||||||
|
r"color_votes_1m/(?P<data_date>\d{4}-\d{2}-\d{2})/(?P<color>[^/]+)": ":trailing_color_votes",
|
||||||
|
r"color_votes_1w/(?P<data_date>\d{4}-\d{2}-\d{2})/(?P<color>[^/]+)": ":trailing_color_votes",
|
||||||
|
r"daily_votes/(?P<data_date>\d{4}-\d{2}-\d{2})": ":aggregate_color_votes",
|
||||||
|
r"votes_1w/(?P<data_date>\d{4}-\d{2}-\d{2})": ":aggregate_color_votes",
|
||||||
|
r"votes_1m/(?P<data_date>\d{4}-\d{2}-\d{2})": ":aggregate_color_votes",
|
||||||
|
r"color_vote_report/(?P<data_date>\d{4}-\d{2}-\d{2})/(?P<color>[^/]+)": ":color_vote_report_calc",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def lookup_job_for_partition(partition_ref: str) -> str:
|
||||||
|
"""Look up which job can build the given partition reference."""
|
||||||
|
for pattern, job_target in JOB_MAPPINGS.items():
|
||||||
|
if re.match(pattern, partition_ref):
|
||||||
|
return job_target
|
||||||
|
|
||||||
|
raise ValueError(f"No job found for partition: {partition_ref}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print("Usage: job_lookup.py <partition_ref>", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
partition_ref = sys.argv[1]
|
||||||
|
try:
|
||||||
|
job_target = lookup_job_for_partition(partition_ref)
|
||||||
|
print(job_target)
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"ERROR: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
142
databuild/test/app/dsl/dsl_job_wrapper.py
Normal file
142
databuild/test/app/dsl/dsl_job_wrapper.py
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Shared DSL job wrapper that can execute any DataBuildJob defined in a DSL graph.
|
||||||
|
Configured via environment variables:
|
||||||
|
- DATABUILD_DSL_GRAPH_MODULE: Python module path containing the graph (e.g., 'databuild.test.app.dsl.graph')
|
||||||
|
- DATABUILD_JOB_CLASS: Job class name to execute (e.g., 'IngestColorVotes')
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import importlib
|
||||||
|
from typing import List, Any
|
||||||
|
from databuild.proto import JobConfig
|
||||||
|
|
||||||
|
|
||||||
|
def parse_outputs_from_args(args: List[str], job_class: Any) -> List[Any]:
|
||||||
|
"""Parse partition output references from command line arguments into partition objects."""
|
||||||
|
outputs = []
|
||||||
|
for arg in args:
|
||||||
|
# Find which output type can deserialize this partition reference
|
||||||
|
for output_type in job_class.output_types:
|
||||||
|
try:
|
||||||
|
partition = output_type.deserialize(arg)
|
||||||
|
outputs.append(partition)
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise ValueError(f"No output type in {job_class.__name__} can deserialize partition ref: {arg}")
|
||||||
|
|
||||||
|
return outputs
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: dsl_job_wrapper.py <config|exec> [args...]", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
command = sys.argv[1]
|
||||||
|
|
||||||
|
# Read configuration from environment
|
||||||
|
graph_module_path = os.environ.get('DATABUILD_DSL_GRAPH_MODULE')
|
||||||
|
job_class_name = os.environ.get('DATABUILD_JOB_CLASS')
|
||||||
|
|
||||||
|
if not graph_module_path:
|
||||||
|
print("ERROR: DATABUILD_DSL_GRAPH_MODULE environment variable not set", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not job_class_name:
|
||||||
|
print("ERROR: DATABUILD_JOB_CLASS environment variable not set", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Import the graph module
|
||||||
|
module = importlib.import_module(graph_module_path)
|
||||||
|
graph = getattr(module, 'graph')
|
||||||
|
|
||||||
|
# Get the job class
|
||||||
|
job_class = getattr(module, job_class_name)
|
||||||
|
|
||||||
|
# Create job instance
|
||||||
|
job_instance = job_class()
|
||||||
|
|
||||||
|
except (ImportError, AttributeError) as e:
|
||||||
|
print(f"ERROR: Failed to load job {job_class_name} from {graph_module_path}: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if command == "config":
|
||||||
|
try:
|
||||||
|
# Parse output partition references from remaining args
|
||||||
|
output_refs = sys.argv[2:]
|
||||||
|
if not output_refs:
|
||||||
|
print("ERROR: No output partition references provided", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
outputs = parse_outputs_from_args(output_refs, job_class)
|
||||||
|
|
||||||
|
# Call job's config method
|
||||||
|
configs = job_instance.config(outputs)
|
||||||
|
|
||||||
|
# Output each config as JSON (one per line for multiple configs)
|
||||||
|
for config in configs:
|
||||||
|
# Convert JobConfig to dict for JSON serialization
|
||||||
|
config_dict = {
|
||||||
|
'outputs': [{'str': ref.str} for ref in config.outputs],
|
||||||
|
'inputs': [
|
||||||
|
{
|
||||||
|
'dep_type_code': dep.dep_type_code,
|
||||||
|
'dep_type_name': dep.dep_type_name,
|
||||||
|
'partition_ref': {'str': dep.partition_ref.str}
|
||||||
|
} for dep in config.inputs
|
||||||
|
],
|
||||||
|
'args': config.args,
|
||||||
|
'env': config.env,
|
||||||
|
}
|
||||||
|
print(json.dumps(config_dict))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Config failed: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
elif command == "exec":
|
||||||
|
try:
|
||||||
|
# Read config from stdin
|
||||||
|
config_json = sys.stdin.read().strip()
|
||||||
|
if not config_json:
|
||||||
|
print("ERROR: No config provided on stdin", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
config_dict = json.loads(config_json)
|
||||||
|
|
||||||
|
# Convert dict back to JobConfig
|
||||||
|
from databuild.proto import PartitionRef, DataDep, DepType
|
||||||
|
|
||||||
|
config = JobConfig(
|
||||||
|
outputs=[PartitionRef(str=ref['str']) for ref in config_dict['outputs']],
|
||||||
|
inputs=[
|
||||||
|
DataDep(
|
||||||
|
dep_type_code=dep['dep_type_code'],
|
||||||
|
dep_type_name=dep['dep_type_name'],
|
||||||
|
partition_ref=PartitionRef(str=dep['partition_ref']['str'])
|
||||||
|
) for dep in config_dict['inputs']
|
||||||
|
],
|
||||||
|
args=config_dict['args'],
|
||||||
|
env=config_dict['env'],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Call job's exec method
|
||||||
|
job_instance.exec(config)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Execution failed: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"ERROR: Unknown command '{command}'. Use 'config' or 'exec'.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -19,7 +19,7 @@ from databuild.test.app.dsl.partitions import (
|
||||||
)
|
)
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
|
|
||||||
graph = DataBuildGraph("//databuild/test/app:dsl_graph")
|
graph = DataBuildGraph("//databuild/test/app/dsl:dsl_graph")
|
||||||
|
|
||||||
|
|
||||||
@graph.job
|
@graph.job
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue