Skip to content
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

feat : HEXA-1124 button to create a new pipeline from a template #887

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 28 additions & 0 deletions hexa/pipeline_templates/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ extend type Mutation {
Creates a new pipeline template version.
"""
createPipelineTemplateVersion(input: CreatePipelineTemplateVersionInput!): CreatePipelineTemplateVersionResult! @loginRequired
createPipelineFromTemplateVersion(input: CreatePipelineFromTemplateVersionInput!): CreatePipelineFromTemplateVersionResult! @loginRequired
}

"""
Expand All @@ -27,6 +28,33 @@ type CreatePipelineTemplateVersionResult {
errors: [CreatePipelineTemplateVersionError!] # The list of errors that occurred during the creation of the pipeline template version.
}

"""
Represents the input for creating a new pipeline from a template version.
"""
input CreatePipelineFromTemplateVersionInput {
workspaceSlug: String! # The slug of the pipeline workspace.
pipelineTemplateVersionId: UUID! # The ID of the pipeline template version.
}

"""
Represents the result of creating a new pipeline from a template version.
"""
type CreatePipelineFromTemplateVersionResult {
pipeline: Pipeline # The created pipeline.
success: Boolean! # Indicates if the pipeline was created successfully.
errors: [CreatePipelineFromTemplateVersionError!] # The list of errors that occurred during the creation of the pipeline.
}

"""
Enum representing the possible errors that can occur when creating a pipeline from a template version.
"""
enum CreatePipelineFromTemplateVersionError {
PERMISSION_DENIED
WORKSPACE_NOT_FOUND
PIPELINE_TEMPLATE_VERSION_NOT_FOUND
PIPELINE_ALREADY_EXISTS
}

"""
Enum representing the possible errors that can occur when creating a pipeline template version.
"""
Expand Down
21 changes: 21 additions & 0 deletions hexa/pipeline_templates/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,26 @@ class Meta:

objects = PipelineTemplateVersionQuerySet.as_manager()

def create_pipeline(self, code, workspace, user):
source_pipeline = self.template.source_pipeline
source_version = self.source_pipeline_version
pipeline = Pipeline.objects.create(
source_template=self.template,
code=code,
name=source_pipeline.name,
description=source_pipeline.description,
config=source_pipeline.config,
workspace=workspace,
)
PipelineVersion.objects.create(
user=user,
pipeline=pipeline,
zipfile=source_version.zipfile,
parameters=source_version.parameters,
config=source_version.config,
timeout=source_version.timeout,
)
return pipeline

def __str__(self):
return f"v{self.version_number} of {self.template.name}"
37 changes: 37 additions & 0 deletions hexa/pipeline_templates/schema/mutations.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from django.http import HttpRequest

from hexa.analytics.api import track
from hexa.pipeline_templates.models import PipelineTemplateVersion
from hexa.pipelines.models import Pipeline, PipelineVersion
from hexa.workspaces.models import Workspace

Expand Down Expand Up @@ -74,4 +75,40 @@ def resolve_create_pipeline_template_version(_, info, **kwargs):
return {"pipeline_template": pipeline_template, "success": True, "errors": []}


@pipeline_template_mutations.field("createPipelineFromTemplateVersion")
def resolve_create_pipeline_from_template_version(_, info, **kwargs):
request: HttpRequest = info.context["request"]
input = kwargs["input"]

workspace = get_workspace(request.user, input.get("workspace_slug"))
if not workspace:
return {"success": False, "errors": ["WORKSPACE_NOT_FOUND"]}

if not request.user.has_perm("pipelines.create_pipeline", workspace):
return {"success": False, "errors": ["PERMISSION_DENIED"]}

try:
template_version = PipelineTemplateVersion.objects.get(
id=input["pipeline_template_version_id"]
)
except PipelineTemplateVersion.DoesNotExist:
return {"success": False, "errors": ["PIPELINE_TEMPLATE_VERSION_NOT_FOUND"]}

pipeline_code = f"{template_version.template.source_pipeline.code} (from Template)"
Copy link
Contributor

@nazarfil nazarfil Dec 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you give more context about this comment? The (from Template) would be appended to the pipeline code and would break the parsing if we would reuse it as it.
So I would like to know if the expected flow is for the user to open the pipeline code and edit it, and the comment line will indicate that code is copied form template and has to be edited?
If the goal of it is to mark it as code form template then i would append it as a python comment "/n # (from Template)"

Copy link
Contributor Author

@YolanFery YolanFery Dec 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for raising this

The intent was to create a different name for the pipeline that is created but you are correct that it makes the code unparseable. In addition to mis-align the python code and the Database object (the @pipeline(code= , name= ) would need to be modified )

So, as part of #885, I am using the same name as the original Pipeline to avoid such complexity at this point

if Pipeline.objects.filter(workspace=workspace, code=pipeline_code).exists():
return {"success": False, "errors": ["PIPELINE_ALREADY_EXISTS"]}
pipeline = template_version.create_pipeline(pipeline_code, workspace, request.user)

track(
request,
"pipeline_templates.pipeline_created_from_template",
{
"pipeline_id": str(pipeline.id),
"template_version_id": str(template_version.id),
"workspace": workspace.slug,
},
)
return {"pipeline": pipeline, "success": True, "errors": []}


bindables = [pipeline_template_mutations]
70 changes: 69 additions & 1 deletion hexa/pipeline_templates/tests/test_schema/test_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,16 @@ def setUpTestData(cls):
name="WS1",
description="Workspace 1",
)
cls.PIPELINE = Pipeline.objects.create(name="Test Pipeline", workspace=cls.WS1)
cls.PIPELINE = Pipeline.objects.create(
name="Test Pipeline", code="Test Pipeline", workspace=cls.WS1
)
cls.PIPELINE_VERSION1 = PipelineVersion.objects.create(
pipeline=cls.PIPELINE,
version_number=1,
description="Initial version",
parameters=[{"code": "param_1"}],
config=[{"param_1": 1}],
zipfile=str.encode("some_bytes"),
)
cls.PIPELINE_VERSION2 = PipelineVersion.objects.create(
pipeline=cls.PIPELINE,
Expand Down Expand Up @@ -84,3 +89,66 @@ def test_create_template_version(self):
self.create_template_version(
self.PIPELINE_VERSION2.id, [{"versionNumber": 1}, {"versionNumber": 2}]
)

def test_create_pipeline_from_template_version(self):
self.client.force_login(self.USER_ROOT)
self.create_template_version(self.PIPELINE_VERSION1.id, [{"versionNumber": 1}])
r = self.run_query(
"""
mutation createPipelineFromTemplateVersion($input: CreatePipelineFromTemplateVersionInput!) {
createPipelineFromTemplateVersion(input: $input) {
success errors pipeline {name code currentVersion {zipfile parameters {code default} config}}
}
}
""",
{
"input": {
"workspaceSlug": self.WS1.slug,
"pipelineTemplateVersionId": str(
self.PIPELINE_VERSION1.template_version.id
),
}
},
)
self.assertEqual(
{
"success": True,
"errors": [],
"pipeline": {
"name": self.PIPELINE.name,
"code": "Test Pipeline (from Template)",
"currentVersion": {
"zipfile": "c29tZV9ieXRlcw==",
"parameters": [{"code": "param_1", "default": None}],
"config": [{"param_1": 1}],
},
},
},
r["data"]["createPipelineFromTemplateVersion"],
)

r = self.run_query(
"""
mutation createPipelineFromTemplateVersion($input: CreatePipelineFromTemplateVersionInput!) {
createPipelineFromTemplateVersion(input: $input) {
success errors pipeline {name code currentVersion {zipfile parameters {code default} config}}
}
}
""",
{
"input": {
"workspaceSlug": self.WS1.slug,
"pipelineTemplateVersionId": str(
self.PIPELINE_VERSION1.template_version.id
),
}
},
)
self.assertEqual(
{
"success": False,
"errors": ["PIPELINE_ALREADY_EXISTS"],
"pipeline": None,
},
r["data"]["createPipelineFromTemplateVersion"],
)
25 changes: 25 additions & 0 deletions hexa/pipelines/migrations/0054_pipeline_source_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated by Django 4.2.17 on 2024-12-20 16:26

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("pipeline_templates", "0002_alter_pipelinetemplate_name"),
("pipelines", "0053_pipelinerun_log_level"),
]

operations = [
migrations.AddField(
model_name="pipeline",
name="source_template",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="pipelines",
to="pipeline_templates.pipelinetemplate",
),
),
]
7 changes: 7 additions & 0 deletions hexa/pipelines/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,13 @@ class Meta:
default=PipelineType.ZIPFILE,
)
notebook_path = models.TextField(null=True, blank=True)
source_template = models.ForeignKey(
"pipeline_templates.PipelineTemplate",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="pipelines",
)

objects = DefaultSoftDeletedManager.from_queryset(PipelineQuerySet)()
all_objects = IncludeSoftDeletedManager.from_queryset(PipelineQuerySet)()
Expand Down
Loading