Skip to content

Commit

Permalink
Merge pull request #2 from AlejandroBaron/develop
Browse files Browse the repository at this point in the history
release v0.1.1
  • Loading branch information
AlejandroBaron authored Aug 8, 2024
2 parents edc611d + 2e8d9c7 commit f69571e
Show file tree
Hide file tree
Showing 9 changed files with 163 additions and 14 deletions.
26 changes: 26 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Build and Deploy
on:
workflow_dispatch:
pull_request:
permissions:
contents: write
jobs:
Testing:
runs-on: ${{ matrix.os }}
strategy:
matrix:
python-version: [3.9, '3.10', '3.11']
os: [ubuntu-latest, macOS-latest, windows-latest]

steps:
- uses: actions/checkout@v3
- name: Set up PDM
uses: pdm-project/setup-pdm@v4.1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pdm sync -d -G testing
- name: Run Tests
run: |
pdm run -v pytest tests
1 change: 1 addition & 0 deletions configs/environments/conda.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ dependencies:
- mldesigner==0.1.0b17
- azure-ai-ml==1.18.0
- azureml-mlflow==1.56.0
- ez-azml==0.1.0
13 changes: 12 additions & 1 deletion pdm.lock

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

5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[project]
name = "ez_azml"
name = "ez-azml"
dynamic = ["version"]
description = "Simplified yaml/jsonargparse driven framework for AzureML interactions"
authors = [
Expand All @@ -11,6 +11,7 @@ dependencies = [
"azure-identity>=1.17.1",
"loguru>=0.7.2",
"mldesigner>=0.1.0b18",
"setuptools>=72.1.0",
]
requires-python = ">=3.9"
readme = "README.md"
Expand Down Expand Up @@ -87,6 +88,8 @@ ignore = [
"UP007",
"UP038"
]
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["S101"]
[tool.ruff.lint.mccabe]
max-complexity = 10
[tool.ruff.lint.pydocstyle]
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ requests==2.32.3
requests-oauthlib==2.0.0
rpds-py==0.19.1
rsa==4.9
setuptools==72.1.0
six==1.16.0
strictyaml==1.7.3
tomli==2.0.1
Expand Down
4 changes: 2 additions & 2 deletions src/ez_azml/cloud_runs/pipelines/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .pipeline import Pipeline
from .pipeline import Pipeline, PipelineCommand

__all__ = ["Pipeline"]
__all__ = ["Pipeline", "PipelineCommand"]
30 changes: 20 additions & 10 deletions src/ez_azml/cloud_runs/pipelines/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@


@dataclass
class Command:
class PipelineCommand:
"""Class representing a mldesigner Azureml step (mldesigner.command_component).
Args:
Expand All @@ -27,6 +27,11 @@ class Command:
def __post_init__(self):
self.component_kwargs["environment"] = self.environment

@property
def name(self):
"""Command's name."""
return self.function.__name__


class Pipeline(CloudRun):
"""Cloud run that uses mldesigner pipelines.
Expand All @@ -41,7 +46,7 @@ class Pipeline(CloudRun):
def __init__(
self,
experiment_name: str,
commands: list[Command],
commands: list[PipelineCommand],
pipeline: Callable,
dec_kwargs: Optional[dict[str, Any]] = None,
**kwargs,
Expand All @@ -52,8 +57,9 @@ def __init__(
self.dec_kwargs = dec_kwargs or {}
self.pipeline = pipeline

def _register_components(self, commands: list[Command]) -> list[Callable]:
decorated = []
def _register_components(
self, commands: list[PipelineCommand]
) -> list[PipelineCommand]:
kwargs_pattern = r"(\w+)\s*=\s*(['\"]?)(\w+)\2"
for command in commands:
fn = command.function
Expand All @@ -68,9 +74,8 @@ def _register_components(self, commands: list[Command]) -> list[Callable]:
elif "Output(" in hint:
hint = mld.Output(**potential_kwargs)
fn.__annotations__[parameter] = hint
decorated_command = mld.command_component(**command.component_kwargs)(fn)
decorated.append(decorated_command)
return decorated
command.function = mld.command_component(**command.component_kwargs)(fn)
return commands

def _build_pipeline(self, pipeline: Callable, dec_kwargs: Optional[dict[str, Any]]):
# Inject the decorated functions
Expand All @@ -80,9 +85,14 @@ def _build_pipeline(self, pipeline: Callable, dec_kwargs: Optional[dict[str, Any
return pipeline_dec(**dec_kwargs)(pipeline)

def _setup_dec_kwargs(self, dec_kwargs: dict[str, Any]):
dec_kwargs["default_compute"] = dec_kwargs.get(
"default_compute", self.compute.name
)
if self.compute:
dec_kwargs["default_compute"] = dec_kwargs.get(
"default_compute", self.compute.name
)
if self.environment:
dec_kwargs["default_compute"] = dec_kwargs.get(
"default_compute", self.compute.name
)
return dec_kwargs

@override
Expand Down
49 changes: 49 additions & 0 deletions tests/cloud_runs/pipelines/test_pipeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import inspect
from typing import Callable
from unittest import mock

from ez_azml.cloud_runs.pipelines import Pipeline


def assert_function_wrapped(function: Callable, wrapped_function: Callable) -> None:
"""Asserts that a function is properly wrapped.
Args:
wrapped_function: The function that is potentially wrapped.
function: The original function to compare against after unwrapping.
Raises:
AssertionError: If the function is already wrapped or if unwrapping the wrapped
function does not yield the original function.
"""
assert not hasattr(function, "__wrapped__"), "The function should not be wrapped."
assert (
inspect.unwrap(wrapped_function) is function
), "Unwrapping the wrapped function did not yield the original function."


def test_pipeline_register_components(pipeline: Pipeline):
"""Tests that the components are properly decorated."""
raw_functions = [c.function for c in pipeline.commands]
registered_commands = pipeline._register_components(pipeline.commands)
for raw_function, registered in zip(raw_functions, registered_commands):
assert_function_wrapped(raw_function, registered.function)


def test_pipeline_setup_dec_kwargs(pipeline: Pipeline):
"""Tests that the pipeline decorator kwargs are properly postprocessed."""
pipeline.dec_kwargs = pipeline._setup_dec_kwargs(pipeline.dec_kwargs)


def test_build_pipeline(pipeline: Pipeline):
"""Tests that the pipeline is properly decorated."""

def f():
pass

with mock.patch.object(pipeline, "commands", return_value=f) as mocked:
mocked.__name__ = "command_fn"

raw_pipeline = pipeline.pipeline
built_pipeline = pipeline._build_pipeline(raw_pipeline, pipeline.dec_kwargs)
assert_function_wrapped(raw_pipeline, built_pipeline)
48 changes: 48 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from typing import Callable

import mldesigner as mld
import pytest
from azure.ai.ml import Input, Output
from ez_azml.cloud_runs.pipelines import Pipeline, PipelineCommand


@pytest.fixture()
def command_fn() -> Callable:
"""Function used in mldesigner command component."""

def command__fn(
command_input: mld.Input(type="uri_folder"), # type: ignore
command_output: mld.Output(type="uri_folder"), # type: ignore
):
print("this is a test_fn")

return command__fn


@pytest.fixture()
def pipeline_fn(command_fn) -> Callable:
"""Function used in azure.ai.ml pipelines."""

def pipeline__fn(
pipeline_input: Input,
pipeline_output: Output,
):
command_fn(pipeline_input)

return pipeline__fn


@pytest.fixture()
def pipeline_command(command_fn: Callable):
"""PipelineCommand fixture."""
return PipelineCommand(function=command_fn)


@pytest.fixture()
def pipeline(pipeline_fn: Callable, pipeline_command: PipelineCommand):
"""Pipeline cloud run fixture."""
return Pipeline(
experiment_name="fixture",
commands=[pipeline_command],
pipeline=pipeline_fn,
)

0 comments on commit f69571e

Please sign in to comment.