-
Notifications
You must be signed in to change notification settings - Fork 73
Python DSC Adapter and Test Resource implementation #1520
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
shammu1
wants to merge
7
commits into
PowerShell:main
Choose a base branch
from
shammu1:python-adapter-v1
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
d894912
Python DSC Adapter and Test Resource implementation
f6b7e86
python-adapter-v2
70438b5
python-adapter-v3
e82c452
python-adapter-v4
173a2d6
python-adapter-v4
7029eb3
python-adapter-v5
6bf54de
python-adapter-v6
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "Name": "python-adapter", | ||
| "Kind": "Adapter", | ||
| "CopyFiles": { | ||
| "All": [ | ||
| "pyDscAdapter/*", | ||
| "pythonadapter.dsc.resource.json" | ||
| ] | ||
| } | ||
| } | ||
Empty file.
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import sys | ||
| from cli import main # TODO: Currently using absolute imports. Switch to relative imports if this is later used as a module in the adapter manifest; otherwise, keep as-is for direct script execution. | ||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(main()) | ||
|
|
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import sys | ||
| import json | ||
| import argparse | ||
| from typing import Optional | ||
| # TODO: Currently using absolute imports. Switch to relative imports if this is later used as a module in the adapter manifest; otherwise, keep as-is for direct script execution. | ||
| from adapter import ResourceAdapter | ||
|
|
||
| # -------------------- | ||
| # CLI / entrypoint API | ||
| # -------------------- | ||
| def _build_parser() -> argparse.ArgumentParser: | ||
| """Construct the argument parser for the DSC adapter CLI.""" | ||
| parser = argparse.ArgumentParser( | ||
| prog="pyDscAdapter", | ||
| description="DSC v3 Python adapter." | ||
| ) | ||
| sub = parser.add_subparsers(dest="command", required=True) | ||
|
|
||
| adapter = sub.add_parser("adapter", help="Adapter operations") | ||
| adapter.add_argument("--operation", required=True, choices=["list", "get", "set", "test", "export", "validate"], | ||
| help="Adapter operation to execute.") | ||
| adapter.add_argument("--input", default="{}", help="JSON string with resource configuration (single input).") | ||
| adapter.add_argument("--resource", dest="ResourceType", default="", help="Resource type selector (e.g., Microsoft.Linux.Apt/Package).") | ||
| adapter.add_argument("--resource-path", dest="ResourcePath", default="", help="Optional resource module file path.") | ||
| return parser | ||
|
|
||
|
|
||
| def main(argv: Optional[list] = None) -> int: | ||
| """Main entry point for the DSC adapter CLI.""" | ||
| parser = _build_parser() | ||
| args = parser.parse_args(argv) | ||
|
|
||
| if args.command != "adapter": | ||
| print(json.dumps({"error": "Unsupported command"})) | ||
| return 2 | ||
|
|
||
| adapter = ResourceAdapter() | ||
|
|
||
|
|
||
| # 1. Start with --input as the authoritative source | ||
| input_str = args.input | ||
|
|
||
| # 2. If stdin has data, it overrides --input (DSC convention) for operations that accept input | ||
| if args.operation in ("get", "set", "test", "export", "validate"): | ||
| stdin_data = sys.stdin.read().strip() if not sys.stdin.isatty() else "" | ||
| if stdin_data: | ||
| input_str = stdin_data | ||
|
|
||
| # 3. Call operation handler | ||
| exit_code, result = adapter.run_operation( | ||
| args.operation, | ||
| input_str, | ||
| args.ResourceType, | ||
| getattr(args, "ResourcePath", "") | ||
| ) | ||
|
|
||
| # If set branch (or similar) already wrote to stdout, skip emitting a wrapper | ||
| if isinstance(result, dict) and result.get("_stdout_emitted"): | ||
| return exit_code | ||
|
|
||
| # 4. Capture EXACT output passed to DSC | ||
| out_json = json.dumps(result, ensure_ascii=False) | ||
|
|
||
| print(out_json) | ||
| return exit_code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| import importlib.util | ||
| from pathlib import Path | ||
| from typing import Any, Dict | ||
|
|
||
| try: | ||
| import tomllib as toml_parser | ||
| except ModuleNotFoundError: | ||
| toml_parser = None | ||
|
|
||
|
|
||
| def _load_pyproject_data(pyproject_path: Path) -> Dict[str, Any]: | ||
| """Load pyproject.toml into a dictionary when possible.""" | ||
| pyproject_path = Path(pyproject_path) | ||
| if not pyproject_path.exists() or toml_parser is None: | ||
| return {} | ||
|
|
||
| try: | ||
| with pyproject_path.open("rb") as f: | ||
| data = toml_parser.load(f) | ||
| return data if isinstance(data, dict) else {} | ||
| except Exception: | ||
| return {} | ||
|
|
||
|
|
||
| def get_project_metadata_from_pyproject(pyproject_path: Path) -> Dict[str, str]: | ||
| """Parse [project] metadata from pyproject.toml.""" | ||
| pyproject_path = Path(pyproject_path) | ||
|
|
||
| default_metadata = { | ||
| "version": "", | ||
| "description": "", | ||
| "author": "", | ||
| } | ||
|
|
||
| if not pyproject_path.exists(): | ||
| return default_metadata | ||
|
|
||
| data = _load_pyproject_data(pyproject_path) | ||
| if data: | ||
| project = data.get("project", {}) | ||
| if not isinstance(project, dict): | ||
| return default_metadata | ||
|
|
||
| authors = project.get("authors", []) | ||
| author = "" | ||
| if isinstance(authors, list): | ||
| for item in authors: | ||
| if isinstance(item, dict) and item.get("name"): | ||
| author = str(item["name"]) | ||
| break | ||
|
|
||
| return { | ||
| "version": str(project.get("version", "") or ""), | ||
| "description": str(project.get("description", "") or ""), | ||
| "author": author, | ||
| } | ||
|
|
||
| try: | ||
| content = pyproject_path.read_text(encoding="utf-8") | ||
| except Exception: | ||
| return default_metadata | ||
|
|
||
| in_project = False | ||
| in_authors = False | ||
| metadata = dict(default_metadata) | ||
|
|
||
| for raw_line in content.splitlines(): | ||
| stripped = raw_line.strip() | ||
| if stripped == "[project]": | ||
| in_project = True | ||
| in_authors = False | ||
| continue | ||
| if in_project and stripped.startswith("[") and stripped != "[[project.authors]]": | ||
| if not in_authors: | ||
| break | ||
| if not in_project: | ||
| continue | ||
|
|
||
| if stripped == "[[project.authors]]": | ||
| in_authors = True | ||
| continue | ||
|
|
||
| if in_authors: | ||
| if stripped.startswith("name") and "=" in stripped and not metadata["author"]: | ||
| _, value = stripped.split("=", 1) | ||
| metadata["author"] = value.strip().strip('"\'') | ||
| elif stripped.startswith("[[") or (stripped.startswith("[") and stripped != "[[project.authors]]"): | ||
| in_authors = False | ||
| continue | ||
|
|
||
| if stripped.startswith("version") and "=" in stripped: | ||
| _, value = stripped.split("=", 1) | ||
| metadata["version"] = value.strip().strip('"\'') | ||
| elif stripped.startswith("description") and "=" in stripped: | ||
| _, value = stripped.split("=", 1) | ||
| metadata["description"] = value.strip().strip('"\'') | ||
|
|
||
| return metadata | ||
|
|
||
| def get_class_map_from_pyproject(pyproject_path: Path) -> Dict[str, str]: | ||
| """ | ||
| Parse [tool.dsc.resources] section from pyproject.toml. | ||
| Returns: {"ResourceType": "ClassName", ...} | ||
| No external dependencies required. | ||
| """ | ||
| pyproject_path = Path(pyproject_path) | ||
|
|
||
| if not pyproject_path.exists(): | ||
| return {} | ||
|
|
||
| data = _load_pyproject_data(pyproject_path) | ||
| if data: | ||
| resources = ( | ||
| data.get("tool", {}) | ||
| .get("dsc", {}) | ||
| .get("resources", {}) | ||
| ) | ||
| if isinstance(resources, dict): | ||
| class_map: Dict[str, str] = {} | ||
| for resource_type, resource_value in resources.items(): | ||
| if isinstance(resource_value, dict): | ||
| class_name = resource_value.get("class", "") | ||
| if class_name: | ||
| class_map[str(resource_type)] = str(class_name) | ||
| elif resource_value is not None: | ||
| class_map[str(resource_type)] = str(resource_value) | ||
| if class_map: | ||
| return class_map | ||
|
|
||
| try: | ||
| content = pyproject_path.read_text(encoding="utf-8") | ||
| except Exception: | ||
| return {} | ||
|
|
||
| class_map = {} | ||
| in_section = False | ||
|
|
||
| for line in content.splitlines(): | ||
| stripped = line.strip() | ||
| if stripped == "[tool.dsc.resources]": | ||
| in_section = True | ||
| continue | ||
| if in_section: | ||
| if stripped.startswith("["): | ||
| break | ||
| if "=" in stripped and not stripped.startswith("#"): | ||
| key, val = stripped.split("=", 1) | ||
| key = key.strip().strip('"\'') | ||
| val = val.strip().strip('"\'') | ||
| class_map[key] = val | ||
|
|
||
| return class_map | ||
|
shammu1 marked this conversation as resolved.
|
||
|
|
||
|
|
||
| def get_resource_metadata_from_pyproject(pyproject_path: Path) -> Dict[str, Dict[str, str]]: | ||
| """Parse per-resource metadata from pyproject.toml.""" | ||
| pyproject_path = Path(pyproject_path) | ||
|
|
||
| if not pyproject_path.exists(): | ||
| return {} | ||
|
|
||
| data = _load_pyproject_data(pyproject_path) | ||
| if not data: | ||
| return {} | ||
|
|
||
| resources = ( | ||
| data.get("tool", {}) | ||
| .get("dsc", {}) | ||
| .get("resources", {}) | ||
| ) | ||
| if not isinstance(resources, dict): | ||
| return {} | ||
|
|
||
| metadata: Dict[str, Dict[str, str]] = {} | ||
| for resource_type, resource_value in resources.items(): | ||
| if not isinstance(resource_value, dict): | ||
| continue | ||
|
|
||
| metadata[str(resource_type)] = { | ||
| "version": str(resource_value.get("version", "") or ""), | ||
| "description": str(resource_value.get("description", "") or ""), | ||
| "author": str(resource_value.get("author", "") or ""), | ||
| } | ||
|
|
||
| return metadata | ||
|
|
||
| def import_class_from_file(resource_path: Path, resource_type: str, class_name: str) -> type: | ||
| """Dynamically import a class from a given file path.""" | ||
| module_name = f"dsc_{resource_type.replace('/', '_').replace('.', '_').lower()}" #if resource_type else f"dsc_{resource_path.stem.lower()}" | ||
| spec = importlib.util.spec_from_file_location(module_name, str(resource_path)) | ||
| if not spec or not spec.loader: | ||
| raise ImportError(f"Unable to load module '{resource_path}'") | ||
|
|
||
| mod = importlib.util.module_from_spec(spec) | ||
| spec.loader.exec_module(mod) | ||
| try: | ||
| return getattr(mod, class_name) | ||
| except AttributeError as e: | ||
| raise ImportError(f"Class '{class_name}' not found in '{resource_path}': {e}") | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.