Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3f3f58c
remove LOADED_NAMESPACES and instead run a coverage in jsonschema.py
Sankalp-Mittal Aug 27, 2026
a2b4f2f
Merge branch 'main' into sankalp-mittal/automate-pybads-resources
Sankalp-Mittal Aug 27, 2026
c23cdb1
Merge branch 'main' into sankalp-mittal/automate-pybads-resources
Sankalp-Mittal Aug 28, 2026
52594c8
Mark PyDABs generated files as auto-generated
Sankalp-Mittal Aug 27, 2026
660d03d
Use inverse glob for generated __init__.py in .gitattributes
Sankalp-Mittal Aug 27, 2026
3afdfc4
Add PyDABs support for catalogs
Sankalp-Mittal Aug 27, 2026
d05605b
Add acceptance test for PyDABs catalogs support
Sankalp-Mittal Aug 27, 2026
686b343
Add changelog entry for PyDABs catalogs support
Sankalp-Mittal Aug 27, 2026
11d06f5
Data-drive add_resource dispatch and add_resources merge
Sankalp-Mittal Aug 27, 2026
fe95df7
Store bundle resources in a single dict keyed by plural name
Sankalp-Mittal Aug 27, 2026
2f6c19a
Use uniform duplicate-resource error wording across resource types
Sankalp-Mittal Aug 27, 2026
28de668
Generate PyDABs core resource wiring
Sankalp-Mittal Aug 27, 2026
69fb519
Consume generated wiring in core and drop hand-written duplicates
Sankalp-Mittal Aug 27, 2026
9ce5799
remove the useless article generator
Sankalp-Mittal Aug 27, 2026
e1c2b7a
remove articles completely from the code
Sankalp-Mittal Aug 27, 2026
d385c7f
Generate core wiring as one file per resource from a template
Sankalp-Mittal Aug 28, 2026
cfeb0a7
Merge remote-tracking branch 'origin/main' into sankalp-mittal/pydabs…
Sankalp-Mittal Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions python/Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ tasks:
! -path databricks/bundles/core \
! -path databricks/bundles/resources \
-exec rm -rf {} \;
# core/ is hand-written except for the generated wiring under _generated/.
- rm -rf databricks/bundles/core/_generated
- cd codegen && uv run -m pytest codegen_tests
- cd codegen && uv run -m codegen.main --output ..
# Generated code is fixed and formatted by the global ruff (see ../ruff.toml).
Expand Down
199 changes: 199 additions & 0 deletions python/codegen/codegen/generated_wiring.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
"""
Generates the per-resource wiring in databricks.bundles.core that used to be
hand-written.

Each wired resource gets its own file _generated/<plural>.py (rendered from
wiring_resource.py.tmpl): the add_* method + collection property mixin, the
*_mutator decorator, and the _ResourceType entry. The generated
_generated/__init__.py collects them into _GeneratedResources (mixed into
Resources), _all_resource_types(), and the mutator re-exports. The core package
__init__ is generated too (static exports plus the generated mutators).
"""

from dataclasses import dataclass
from pathlib import Path
from string import Template

import codegen.packages as packages

HEADER = "# Code generated by pydabs-codegen. DO NOT EDIT.\n\n"

_RESOURCE_TEMPLATE = Template(
(Path(__file__).parent / "wiring_resource.py.tmpl").read_text()
)


@dataclass(frozen=True)
class _WiredResource:
class_name: str
"""Resource dataclass name, e.g. "Job"."""

singular_name: str
"""Singular name used in methods and messages, e.g. "job"."""

plural_name: str
"""Plural name, the same as the "resources" bundle section, e.g. "jobs"."""

model_module: str
"""Module the resource dataclass lives in, e.g. "databricks.bundles.jobs._models.job"."""


def _wired_resources() -> list[_WiredResource]:
# Every namespaced resource is wired. RESOURCE_NAMESPACE is the single source
# of truth for both model generation and wiring.
resources = []

for ref, namespace in packages.RESOURCE_NAMESPACE.items():
class_name = packages.get_class_name(ref)

resources.append(
_WiredResource(
class_name=class_name,
singular_name=class_name.lower(),
plural_name=namespace,
model_module=packages.get_package(namespace, ref),
)
)

resources.sort(key=lambda r: r.plural_name)

return resources


def write_wiring(output: str):
resources = _wired_resources()

core_path = Path(output) / "databricks" / "bundles" / "core"
generated_path = core_path / "_generated"
generated_path.mkdir(parents=True, exist_ok=True)

for r in resources:
code = _RESOURCE_TEMPLATE.substitute(
{
"class": r.class_name,
"singular": r.singular_name,
"plural": r.plural_name,
"model_module": r.model_module,
}
)
(generated_path / f"{r.plural_name}.py").write_text(HEADER + code)

(generated_path / "__init__.py").write_text(HEADER + _collector_code(resources))
(core_path / "__init__.py").write_text(HEADER + _core_init_code(resources))

print(f"Writing wiring into {generated_path}")


def _collector_code(resources: list[_WiredResource]) -> str:
imports = "\n".join(
f"from databricks.bundles.core._generated.{r.plural_name} import "
f"_{r.class_name}Resources, {r.singular_name}_mutator"
for r in resources
)

mutator_names = [f"{r.singular_name}_mutator" for r in resources]
exports = sorted(["_GeneratedResources", "_all_resource_types", *mutator_names])
all_block = "\n".join(f' "{name}",' for name in exports)

mixins = "\n".join(f" _{r.class_name}Resources," for r in resources)
module_imports = "\n".join(f" {r.plural_name}," for r in resources)
type_entries = "\n".join(
f" {r.plural_name}._resource_type()," for r in resources
)

return f"""from typing import TYPE_CHECKING

{imports}

if TYPE_CHECKING:
from databricks.bundles.core._resource_type import _ResourceType

__all__ = [
{all_block}
]


class _GeneratedResources(
{mixins}
):
pass


def _all_resource_types() -> "tuple[_ResourceType, ...]":
from databricks.bundles.core._generated import (
{module_imports}
)

return (
{type_entries}
)
"""


# Static, non-resource exports of databricks.bundles.core. Kept here because the
# package __init__ is generated; the resource mutators are appended and the whole
# list is sorted.
_CORE_INIT_STATIC_EXPORTS = [
"Bundle",
"Diagnostic",
"Diagnostics",
"Location",
"Resource",
"ResourceMutator",
"Resources",
"Severity",
"Variable",
"VariableOr",
"VariableOrDict",
"VariableOrList",
"VariableOrOptional",
"load_resources_from_current_package_module",
"load_resources_from_module",
"load_resources_from_modules",
"load_resources_from_package_module",
"variables",
]

_CORE_INIT_STATIC_IMPORTS = """from databricks.bundles.core._bundle import Bundle
from databricks.bundles.core._diagnostics import (
Diagnostic,
Diagnostics,
Severity,
)
from databricks.bundles.core._load import (
load_resources_from_current_package_module,
load_resources_from_module,
load_resources_from_modules,
load_resources_from_package_module,
)
from databricks.bundles.core._location import Location
from databricks.bundles.core._resource import Resource
from databricks.bundles.core._resource_mutator import ResourceMutator
from databricks.bundles.core._resources import Resources
from databricks.bundles.core._variable import (
Variable,
VariableOr,
VariableOrDict,
VariableOrList,
VariableOrOptional,
variables,
)"""


def _core_init_code(resources: list[_WiredResource]) -> str:
mutator_names = [f"{r.singular_name}_mutator" for r in resources]

all_exports = sorted(_CORE_INIT_STATIC_EXPORTS + mutator_names)
all_block = "\n".join(f' "{name}",' for name in all_exports)

mutator_imports = ",\n".join(f" {name}" for name in sorted(mutator_names))

return f"""__all__ = [
{all_block}
]

{_CORE_INIT_STATIC_IMPORTS}
from databricks.bundles.core._generated import (
{mutator_imports},
)
"""
6 changes: 6 additions & 0 deletions python/codegen/codegen/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import codegen.generated_dataclass_patch as generated_dataclass_patch
import codegen.generated_enum as generated_enum
import codegen.generated_imports as generated_imports
import codegen.generated_wiring as generated_wiring
import codegen.jsonschema as openapi
import codegen.jsonschema_patch as openapi_patch
import codegen.packages as packages
Expand Down Expand Up @@ -46,6 +47,11 @@ def main(output: str):

_write_exports(namespace, dataclasses, enums, output)

# Generate the per-resource wiring in databricks.bundles.core (the
# _ResourceType registry, Resources add_*/property methods, *_mutator
# decorators, and the core package __init__).
generated_wiring.write_wiring(output)


def _transitively_mark_deprecated_and_private(
roots: list[str],
Expand Down
113 changes: 113 additions & 0 deletions python/codegen/codegen/wiring_resource.py.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from collections.abc import Callable
from typing import TYPE_CHECKING, Optional, overload

from databricks.bundles.core._bundle import Bundle
from databricks.bundles.core._location import Location
from databricks.bundles.core._resource_mutator import ResourceMutator
from databricks.bundles.core._transform import _transform

if TYPE_CHECKING:
from databricks.bundles.core._resource_type import _ResourceType
from ${model_module} import ${class}, ${class}Param


def _resource_type() -> "_ResourceType":
from databricks.bundles.core._resource_type import _ResourceType
from ${model_module} import ${class}

return _ResourceType(
resource_type=${class},
singular_name="${singular}",
plural_name="${plural}",
)


class _${class}Resources:
"""
Generated ${singular} accessors, mixed into Resources.
"""

# Provided by the Resources subclass; declared here so the generated methods
# below type-check.
_resources: dict[str, dict]

if TYPE_CHECKING:

def add_location(self, path: tuple[str, ...], location: Location) -> None: ...

def add_diagnostic_error(
self,
msg: str,
*,
detail: Optional[str] = None,
path: Optional[tuple[str, ...]] = None,
location: Optional[Location] = None,
) -> None: ...

@property
def ${plural}(self) -> dict[str, "${class}"]:
return self._resources["${plural}"]

def add_${singular}(
self,
resource_name: str,
${singular}: "${class}Param",
*,
location: Optional[Location] = None,
) -> None:
"""
Adds the resource ${singular} to the collection of resources. Resource name must be unique across all ${plural}.

:param resource_name: unique identifier for the ${singular}
:param ${singular}: the ${singular} to add, can be ${class} or dict
:param location: optional location of the ${singular} in the source code
"""
from ${model_module} import ${class}

${singular} = _transform(${class}, ${singular})
path = ("resources", "${plural}", resource_name)
location = location or Location.from_stack_frame(depth=1)

if self._resources["${plural}"].get(resource_name):
self.add_diagnostic_error(
msg=f"Duplicate resource name '{resource_name}' for resource '${singular}'. Resource names must be unique.",
location=location,
path=path,
)
else:
if location:
self.add_location(path, location)

self._resources["${plural}"][resource_name] = ${singular}


@overload
def ${singular}_mutator(
function: Callable[[Bundle, "${class}"], "${class}"],
) -> ResourceMutator["${class}"]: ...


@overload
def ${singular}_mutator(
function: Callable[["${class}"], "${class}"],
) -> ResourceMutator["${class}"]: ...


def ${singular}_mutator(function: Callable) -> ResourceMutator["${class}"]:
"""
Decorator for defining mutator for ${plural}. Function should return a new instance of the ${singular}
with the desired changes, instead of mutating the input ${singular}.

Example:

.. code-block:: python

@${singular}_mutator
def my_${singular}_mutator(bundle: Bundle, ${singular}: ${class}) -> ${class}:
return replace(${singular}, ...)

:param function: Function that mutates ${plural}.
"""
from ${model_module} import ${class}

return ResourceMutator(resource_type=${class}, function=function)
5 changes: 3 additions & 2 deletions python/databricks/bundles/.gitattributes
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Generated by pydabs-codegen (see python/codegen). Each generated namespace
# has a _models/ tree and an __init__.py; core/ is hand-written, so unset it.
# has a _models/ tree and an __init__.py. In core/, only the _generated/ tree
# and the package __init__.py are generated; the rest is hand-written.
*/_models/** linguist-generated=true
*/__init__.py linguist-generated=true
core/__init__.py linguist-generated=false
core/_generated/** linguist-generated=true
20 changes: 11 additions & 9 deletions python/databricks/bundles/core/__init__.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading