Skip to content

Commit 6f495f7

Browse files
authored
feat: conditionally run pipelines in debug mode (#223)
* Consider the current log_level * Consider the current log_level * Unit test * Unit test simplify * Dockerfile and small bug fix * fix env conversion * Simplify Dockerfile * Instructions * Renames * Renames
1 parent 488b290 commit 6f495f7

9 files changed

Lines changed: 146 additions & 18 deletions

File tree

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,6 @@ python -m venv venv # Create a virtual environment for this project
112112
source venv/bin/activate # Activate the venv
113113
pip install -e ".[dev]" # Necessary to be able to run the openhexa CLI
114114
```
115-
116115
### Using a local installation of OpenHEXA to run pipelines
117116

118117
While it is possible to run pipelines locally using only the SDK, if you want to run OpenHEXA in a more realistic
@@ -127,6 +126,20 @@ openhexa config set_url http://localhost:8000
127126

128127
Notes: you can monitor the status of your pipelines using http://localhost:8000/pipelines/status
129128

129+
### Using a local version of the SDK to run pipelines
130+
131+
If you want to use a local version of the SDK to run pipelines, you can build a docker image with the local version of the SDK installed in it :
132+
133+
```shell
134+
docker build --platform linux/amd64 -t local_image:v1 -f images/Dockerfile .
135+
```
136+
137+
Then reference the image name and tag in the `.env` file of your OpenHexa app :
138+
139+
```
140+
DEFAULT_WORKSPACE_IMAGE=local_image:v1
141+
```
142+
130143
### Running the tests
131144

132145
You can run the tests using pytest:

images/Dockerfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM blsq/openhexa-base-environment:latest
2+
3+
USER root
4+
5+
WORKDIR /app
6+
COPY . /app
7+
8+
RUN pip install build
9+
RUN python -m build .
10+
11+
RUN pip install --no-cache-dir /app/dist/*.tar.gz && rm -rf /app/dist/*.tar.gz

openhexa/cli/settings.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
import click
88

9+
from openhexa.sdk.pipelines.log_level import LogLevel
10+
911
CONFIGFILE_PATH = os.path.expanduser("~") + "/.openhexa.ini"
1012

1113

@@ -76,6 +78,11 @@ def workspaces(self):
7678
"""Return the workspaces from the settings file."""
7779
return self._file_config["workspaces"]
7880

81+
@property
82+
def log_level(self) -> LogLevel:
83+
"""Return the log level from the environment variables."""
84+
return LogLevel.parse_log_level(os.getenv("HEXA_LOG_LEVEL"))
85+
7986
def activate(self, workspace: str):
8087
"""Set the current workspace in the settings file."""
8188
if workspace not in self.workspaces:
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""Log levels for the pipeline runs."""
2+
from enum import IntEnum
3+
4+
5+
class LogLevel(IntEnum):
6+
"""
7+
Enum representing different log levels.
8+
9+
- Attributes:
10+
DEBUG (int): Debug level, value 0.
11+
INFO (int): Info level, value 1.
12+
WARNING (int): Warning level, value 2.
13+
ERROR (int): Error level, value 3.
14+
CRITICAL (int): Critical level, value 4.
15+
16+
"""
17+
18+
DEBUG = 0
19+
INFO = 1
20+
WARNING = 2
21+
ERROR = 3
22+
CRITICAL = 4
23+
24+
@classmethod
25+
def parse_log_level(cls, value) -> "LogLevel":
26+
"""Parse a log level from a string or integer."""
27+
if isinstance(value, int) and 0 <= value <= 4:
28+
return LogLevel(value)
29+
if isinstance(value, str):
30+
if value.isdigit():
31+
return cls.parse_log_level(int(value))
32+
value = value.upper()
33+
if hasattr(cls, value):
34+
return getattr(cls, value)
35+
return cls.INFO

openhexa/sdk/pipelines/run.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
import datetime
44
import errno
55
import os
6-
import typing
76

7+
from openhexa.sdk.pipelines.log_level import LogLevel
88
from openhexa.sdk.utils import Environment, get_environment, graphql
99
from openhexa.sdk.workspaces import workspace
1010

@@ -75,45 +75,45 @@ def add_database_output(self, table_name: str):
7575
print(f"Sending output with table_name {table_name}")
7676

7777
def log_debug(self, message: str):
78-
"""Log a message with the DEBUG priority."""
79-
self._log_message("DEBUG", message)
78+
"""Log a message with the DEBUG level."""
79+
self._log_message(LogLevel.DEBUG, message)
8080

8181
def log_info(self, message: str):
82-
"""Log a message with the INFO priority."""
83-
self._log_message("INFO", message)
82+
"""Log a message with the INFO level."""
83+
self._log_message(LogLevel.INFO, message)
8484

8585
def log_warning(self, message: str):
86-
"""Log a message with the WARNING priority."""
87-
self._log_message("WARNING", message)
86+
"""Log a message with the WARNING level."""
87+
self._log_message(LogLevel.WARNING, message)
8888

8989
def log_error(self, message: str):
90-
"""Log a message with the ERROR priority."""
91-
self._log_message("ERROR", message)
90+
"""Log a message with the ERROR level."""
91+
self._log_message(LogLevel.ERROR, message)
9292

9393
def log_critical(self, message: str):
94-
"""Log a message with the CRITICAL priority."""
95-
self._log_message("CRITICAL", message)
94+
"""Log a message with the CRITICAL level."""
95+
self._log_message(LogLevel.CRITICAL, message)
9696

9797
def _log_message(
9898
self,
99-
priority: typing.Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
99+
log_level: LogLevel,
100100
message: str,
101101
):
102-
valid_priorities = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
103-
if priority not in valid_priorities:
104-
raise ValueError(f"priority must be one of {', '.join(valid_priorities)}")
102+
from openhexa.cli.settings import settings
105103

104+
if log_level < settings.log_level: # Ignore messages with lower log level than the settings
105+
return
106106
if self._connected:
107107
graphql(
108108
"""
109109
mutation logPipelineMessage ($input: LogPipelineMessageInput!) {
110110
logPipelineMessage(input: $input) { success errors }
111111
}""",
112-
{"input": {"priority": priority, "message": str(message)}},
112+
{"input": {"priority": log_level.name, "message": str(message)}},
113113
)
114114
else:
115115
now = datetime.datetime.now(tz=datetime.timezone.utc).replace(microsecond=0).isoformat()
116-
print(now, priority, message)
116+
print(now, log_level.name, message)
117117

118118

119119
if get_environment() == Environment.CLOUD_JUPYTER:

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ include-package-data = true
6767
[tool.ruff]
6868
line-length = 120
6969
ignore = ["E501"]
70+
per-file-ignores = { "tests/**/test_*.py" = ["D100","D101","D102", "D103"] } # Ignore missing docstrings in tests
7071

7172
[tool.ruff.lint]
7273
extend-select = [

tests/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import openhexa.cli
99
import openhexa.sdk
10+
from openhexa.sdk.pipelines.log_level import LogLevel
1011

1112

1213
@pytest.fixture(scope="function")
@@ -40,4 +41,5 @@ def settings(monkeypatch):
4041
settings_mock.workspaces = {"workspace-slug": "token", "another-workspace-slug": "token"}
4142
settings_mock.debug = False
4243
settings_mock.access_token = "token"
44+
settings_mock.log_level = LogLevel.INFO
4345
return settings_mock

tests/test_current_run.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from unittest.mock import ANY, patch
2+
3+
from openhexa.sdk.pipelines.run import CurrentRun, LogLevel
4+
5+
6+
@patch.object(CurrentRun, "_connected", True)
7+
@patch("openhexa.sdk.pipelines.run.graphql")
8+
def test_default_log_level(mock_graphql):
9+
current_run = CurrentRun()
10+
11+
current_run.log_debug("This is a debug message")
12+
current_run.log_info("This is an info message")
13+
14+
assert mock_graphql.call_count == 1
15+
mock_graphql.assert_any_call(ANY, {"input": {"priority": "INFO", "message": "This is an info message"}})
16+
17+
18+
@patch.object(CurrentRun, "_connected", True)
19+
@patch("openhexa.sdk.pipelines.run.graphql")
20+
def test_filtering_log_messages_based_on_settings(mock_graphql, settings):
21+
settings.log_level = LogLevel.ERROR
22+
current_run = CurrentRun()
23+
24+
current_run.log_warning("This is a warning message")
25+
current_run.log_error("This is an error message")
26+
current_run.log_critical("This is a critical message")
27+
28+
assert mock_graphql.call_count == 2
29+
mock_graphql.assert_any_call(ANY, {"input": {"priority": "ERROR", "message": "This is an error message"}})
30+
mock_graphql.assert_any_call(ANY, {"input": {"priority": "CRITICAL", "message": "This is a critical message"}})

tests/test_pipeline.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Pipeline test module."""
22

33
import os
4+
from unittest import TestCase
45
from unittest.mock import Mock, patch
56

67
import pytest
@@ -12,6 +13,7 @@
1213
PostgreSQLConnection,
1314
S3Connection,
1415
)
16+
from openhexa.sdk.pipelines.log_level import LogLevel
1517
from openhexa.sdk.pipelines.parameter import Parameter, ParameterValueError
1618
from openhexa.sdk.pipelines.pipeline import Pipeline
1719

@@ -218,3 +220,30 @@ def test_pipeline_parameters_spec():
218220
"default": None,
219221
},
220222
]
223+
224+
225+
class TestLogLevel(TestCase):
226+
def test_parse_log_level(self):
227+
test_cases = [
228+
(0, LogLevel.DEBUG),
229+
(1, LogLevel.INFO),
230+
(2, LogLevel.WARNING),
231+
(3, LogLevel.ERROR),
232+
(4, LogLevel.CRITICAL),
233+
("0", LogLevel.DEBUG),
234+
("1", LogLevel.INFO),
235+
("2", LogLevel.WARNING),
236+
("3", LogLevel.ERROR),
237+
("4", LogLevel.CRITICAL),
238+
("DEBUG", LogLevel.DEBUG),
239+
("INFO", LogLevel.INFO),
240+
("WARNING", LogLevel.WARNING),
241+
("ERROR", LogLevel.ERROR),
242+
("CRITICAL", LogLevel.CRITICAL),
243+
("invalid", LogLevel.INFO),
244+
(6, LogLevel.INFO),
245+
(-1, LogLevel.INFO),
246+
]
247+
for input_value, expected in test_cases:
248+
with self.subTest(input_value=input_value, expected=expected):
249+
self.assertEqual(LogLevel.parse_log_level(input_value), expected)

0 commit comments

Comments
 (0)