Skip to content
Open
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
2 changes: 1 addition & 1 deletion .copier-answers.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Changes here will be overwritten by Copier
_commit: v7.3.0-23-g37cff5f
_commit: v8.5.0-4-gd5f8597
_src_path: gh:eccenca/cmem-plugin-template
author_mail: cmempy-developer@eccenca.com
author_name: eccenca GmbH
Expand Down
19 changes: 13 additions & 6 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ jobs:
- name: Check out repository
uses: actions/checkout@v5

- name: Cache Trivy DB
id: cache-trivydb
uses: actions/cache@v4
with:
path: .trivycache
key: ${{ runner.os }}-trivydb

- name: Install Task
uses: arduino/setup-task@v2

Expand All @@ -38,10 +45,6 @@ jobs:
virtualenvs-in-project: true
installer-parallel: true

- name: Install dynamic versioning plugin
run: |
poetry self add "poetry-dynamic-versioning[plugin]"

- name: mypy
run: |
task check:mypy
Expand All @@ -61,9 +64,13 @@ jobs:
run: |
task check:deptry

- name: safety
- name: trivy
env:
TRIVY_NO_PROGRESS: "true"
TRIVY_CACHE_DIR: ".trivycache/"
TRIVY_DISABLE_VEX_NOTICE: "true"
run: |
task check:safety
task check:trivy

- name: Publish Test Report in Action
uses: mikepenz/action-junit-report@v4
Expand Down
4 changes: 0 additions & 4 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,6 @@ jobs:
virtualenvs-in-project: true
installer-parallel: true

- name: Install dynamic versioning plugin
run: |
poetry self add "poetry-dynamic-versioning[plugin]"

- name: Publish Package
env:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ dmypy.json
# Cython debug symbols
cython_debug/

# Claude code specifics
.claude/settings.local.json

# project build plan specific ignores
version.py
co
Expand Down
15 changes: 11 additions & 4 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
default:
image: docker-registry.eccenca.com/eccenca-python:v3.13.8
image: docker-registry.eccenca.com/eccenca-python:v3.13.13
# all jobs can be interrupted in case a new commit is pushed
interruptible: true
before_script:
Expand Down Expand Up @@ -60,17 +60,24 @@ deptry:
script:
- task check:deptry

safety:
trivy:
stage: test
variables:
TRIVY_NO_PROGRESS: "true"
TRIVY_CACHE_DIR: ".trivycache/"
TRIVY_DISABLE_VEX_NOTICE: "true"
script:
- task check:safety
- task check:trivy
cache:
paths:
- .trivycache/

build:
stage: build
needs:
- mypy
- pytest
- safety
- trivy
- deptry
script:
- task build
Expand Down
9 changes: 9 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
---
default_language_version:
python: python3.13

repos:
- repo: local
hooks:
Expand Down Expand Up @@ -36,3 +39,9 @@ repos:
stages: [post-checkout, post-merge]
always_run: true

- id: trivy
name: check:trivy
description: run trivy to scan for vulnerabilities
entry: task check:trivy
language: python
pass_filenames: false
4 changes: 4 additions & 0 deletions .trivyignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# .trivyignore

# ignore 51358 safety - dev dependency only
CVE-2022-39280
44 changes: 11 additions & 33 deletions Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,36 +44,9 @@ tasks:
cmds:
- mkdir -p {{.DIST_DIR}}/coverage

poetry:check:
internal: true
platforms: [darwin, linux]
summary: |
Check poetry versioning plugin. Currently not under Windows
run: once
preconditions:
- sh: '[ -d .git ]'
msg: >
Your newly created project directory needs to be initialized
as a git repository.
- sh: '[[ {{.PDV_VERSION}} > {{.PDV_VERSION_MIN}} ]]'
msg: >
This project needs the poetry-dynamic-versioning
plugin > v{{.PDV_VERSION_MIN}}.

You can install it with the following command:
poetry self add "poetry-dynamic-versioning[plugin]"
vars:
PDV_VERSION_MIN: 0.20
PDV_VERSION:
sh: >
poetry self show --addons poetry-dynamic-versioning --tree
| head -1 | cut -d " " -f 2 | cut -d "." -f 1-2

poetry:install:
desc: Install dependencies managed by Poetry
run: once
deps:
- poetry:check
cmds:
- poetry install

Expand Down Expand Up @@ -112,7 +85,7 @@ tasks:
- task: check:ruff
- task: check:mypy
- task: check:deptry
- task: check:safety
- task: check:trivy

check:pytest:
desc: Run unit and integration tests
Expand Down Expand Up @@ -154,12 +127,17 @@ tasks:
vars:
JUNIT_FILE: ./{{.DIST_DIR}}/junit-mypy.xml

check:safety:
desc: Complain about vulnerabilities in dependencies
check:trivy:
desc: Scan for vulnerabilities using Trivy
<<: *preparation
cmds:
# ignore 51358 safety - dev dependency only
- poetry run safety check -i 51358
- >
poetry run trivy fs
--include-dev-deps
--scanners vuln
--skip-files .poetry/plugins/poetry.lock
--exit-code 1
.

check:deptry:
desc: Complain about unused or missing dependencies
Expand All @@ -185,7 +163,7 @@ tasks:
<<: *preparation
deps:
- clean
- poetry:check
cmds:
- poetry build
- poetry export --without=dev -f requirements.txt >dist/requirements.txt

104 changes: 68 additions & 36 deletions cmem_plugin_loopwf/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
from http import HTTPStatus
from time import sleep

from cmem.cmempy.api import config, get_json
from cmem.cmempy.workflow.workflow import execute_workflow_io, get_workflows_io
from cmem_client.client import Client
from cmem_plugin_base.dataintegration.context import ExecutionContext, ExecutionReport
from cmem_plugin_base.dataintegration.description import Icon, Plugin, PluginParameter
from cmem_plugin_base.dataintegration.entity import (
Expand All @@ -24,8 +23,7 @@
IntParameterType,
StringParameterType,
)
from cmem_plugin_base.dataintegration.utils import setup_cmempy_user_access
from requests import HTTPError
from httpx import HTTPStatusError

from cmem_plugin_loopwf import exceptions
from cmem_plugin_loopwf.workflow_type import SuitableWorkflowParameterType
Expand Down Expand Up @@ -103,6 +101,24 @@ class WorkflowExecution:
execution_context: ExecutionContext | None = None
logger: PluginLogger | None = None
input_mime_type: str = ""
client: Client | None = None

@property
def workflow_id(self) -> str:
"""The ID of the started workflow in the form of 'project_id:task_id'"""
return f"{self.project_id}:{self.task_id}"

def get_client(self) -> Client:
"""Get the client to talk to Corporate Memory

Falls back to a client created from the execution context, so a single client
can be shared by all executions of a run.
"""
if self.client is None:
if self.execution_context is None:
raise ValueError("Need a client or an execution context to start a workflow.")
self.client = Client.from_context(context=self.execution_context)
return self.client

@property
def is_finished(self) -> bool:
Expand All @@ -123,35 +139,50 @@ def start(self) -> bool:
"""Start the workflow"""
if self.logger:
self.logger.info(f"Starting workflow execution: {self.entity_as_json_str()}")
client = self.get_client()
if self.schema.type_uri == FileEntitySchema().type_uri and self.input_mime_type != "":
return self.start_with_file(client=client)
url = (
client.config.url_build_api
/ "api/workflow/executeAsync"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Now it is async, change it back since too much of a breaking change for upcoming release

/ self.project_id
/ self.task_id
)
try:
if self.execution_context:
setup_cmempy_user_access(context=self.execution_context.user)
if self.schema.type_uri == FileEntitySchema().type_uri and self.input_mime_type != "":
response = execute_workflow_io(
project_name=self.project_id,
task_name=self.task_id,
input_file=self.entity.values[0][0],
input_mime_type=self.input_mime_type,
)
# workflows are NOT executed async at the moment
self.status = "FINISHED"
return True
response = get_json(
f"{config.get_di_api_endpoint()}/api/workflow/executeAsync/{self.project_id}/{self.task_id}",
response = client.http.post(
url,
headers={"Content-Type": "application/json"},
method="POST",
data=self.entity_as_json_str(),
content=self.entity_as_json_str(),
)
except HTTPError as error:
response.raise_for_status()
except HTTPStatusError as error:
if error.response.status_code == HTTPStatus.SERVICE_UNAVAILABLE:
# 503 - no more execution capacity > no status change
return False
raise ValueError(str(error)) from error
self.instance_id = response["instanceId"]
self.activity_id = response["activityId"]
started = response.json()
self.instance_id = started["instanceId"]
self.activity_id = started["activityId"]
self.update()
return True

def start_with_file(self, client: Client) -> bool:
"""Start the workflow by sending the file of a file entity to it"""
with client.workflows.execute_io(
workflow_id=self.workflow_id,
input_file=self.entity.values[0][0],
input_mime_type=self.input_mime_type,
) as response:
response.read()
if response.status_code == HTTPStatus.SERVICE_UNAVAILABLE:
# 503 - no more execution capacity > no status change
return False
if not response.is_success:
raise ValueError(f"{response.status_code} {response.reason_phrase}: {response.text}")
# workflows are NOT executed async at the moment
self.status = "FINISHED"
return True

def wait_until_finished(self) -> None:
"""Wait until the workflow is finished"""
while self.is_running:
Expand All @@ -160,20 +191,21 @@ def wait_until_finished(self) -> None:

def update(self) -> None:
"""Update the execution status"""
if self.execution_context:
setup_cmempy_user_access(context=self.execution_context.user)
response = get_json(
f"{config.get_di_api_endpoint()}/workspace/activities/status",
client = self.get_client()
response = client.http.get(
client.config.url_build_api / "workspace/activities/status",

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

No equivalent in cmem-client? If not, write in ticket

params={
"project": self.project_id,
"task": self.task_id,
"activity": self.activity_id,
"instance": self.instance_id,
},
)
self.status = response["statusName"]
self.is_running = response["isRunning"]
self.raw = response
response.raise_for_status()
status = response.json()
self.status = status["statusName"]
self.is_running = status["isRunning"]
self.raw = status
if self.logger:
self.logger.debug(f"Updated Status: {self!s}")

Expand Down Expand Up @@ -304,6 +336,7 @@ class StartWorkflow(WorkflowPlugin):
"""Start Workflow per Entity"""

context: ExecutionContext
client: Client
executions: WorkflowExecutionList

def __init__(
Expand Down Expand Up @@ -340,6 +373,7 @@ def start_workflows(self, inputs: Sequence[Entities]) -> Entities:
execution_context=self.context,
logger=self.log,
input_mime_type=self.input_mime_type,
client=self.client,
)
self.log.info(f"Got new entity: {new_execution.entity_as_json_str()}")
self.executions.append(new_execution)
Expand All @@ -360,6 +394,7 @@ def execute(
"""Run the workflow operator."""
self.log.info("Start execute")
self.context = context
self.client = Client.from_context(context=context)
self.validate_inputs(inputs=inputs)
self.validate_workflow(workflow=self.workflow)
output_entities = self.start_workflows(inputs=inputs)
Expand All @@ -381,12 +416,9 @@ def validate_inputs(inputs: Sequence[Entities]) -> None:
def validate_workflow(self, workflow: str) -> None:
"""Validate a workflow (ID)"""
current_project = self.context.task.project_id()
setup_cmempy_user_access(context=self.context.user)
suitable_workflows: dict[str, dict] = {
f"{_['id']}": _
for _ in get_workflows_io()
if self.context.task.project_id() == _["projectId"] and len(_["variableInputs"]) == 1
}
suitable_workflows = SuitableWorkflowParameterType.get_suitable_workflows(
client=self.client, project_id=current_project
)
if workflow not in suitable_workflows:
raise exceptions.NoSuitableWorkflowError(
f"Workflow '{workflow}' does not exist in project '{current_project}'"
Expand Down
Loading
Loading