qase-python-commons 3.4.1__py3-none-any.whl → 4.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of qase-python-commons might be problematic. Click here for more details.

qase/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """
2
+ Qase Python Commons package.
3
+ """
@@ -3,7 +3,7 @@ from typing import Union
3
3
 
4
4
  import certifi
5
5
  from qase.api_client_v1 import ApiClient, ProjectsApi, Project, EnvironmentsApi, RunsApi, AttachmentsApi, \
6
- AttachmentGet, RunCreate
6
+ AttachmentGet, RunCreate, ConfigurationsApi, ConfigurationCreate, ConfigurationGroupCreate, RunPublic
7
7
  from qase.api_client_v1.configuration import Configuration
8
8
  from .. import Logger
9
9
  from .base_api_client import BaseApiClient
@@ -11,6 +11,7 @@ from ..exceptions.reporter import ReporterException
11
11
  from ..models import Attachment
12
12
  from ..models.config.framework import Video, Trace
13
13
  from ..models.config.qaseconfig import QaseConfig
14
+ from ..models.config.testops import ConfigurationValue
14
15
 
15
16
 
16
17
  class ApiV1Client(BaseApiClient):
@@ -66,7 +67,74 @@ class ApiV1Client(BaseApiClient):
66
67
  self.logger.log("Exception when calling EnvironmentsApi->get_environments: %s\n" % e, "error")
67
68
  raise ReporterException(e)
68
69
 
69
- def complete_run(self, project_code: str, run_id: str) -> None:
70
+ def get_configurations(self, project_code: str):
71
+ """Get all configurations for the project"""
72
+ try:
73
+ self.logger.log_debug(f"Getting configurations for project {project_code}")
74
+ api_instance = ConfigurationsApi(self.client)
75
+ response = api_instance.get_configurations(code=project_code)
76
+ if hasattr(response, 'result') and hasattr(response.result, 'entities'):
77
+ return response.result.entities
78
+ return []
79
+ except Exception as e:
80
+ self.logger.log(f"Exception when calling ConfigurationsApi->get_configurations: {e}", "error")
81
+ return []
82
+
83
+ def find_or_create_configuration(self, project_code: str, config_value: ConfigurationValue) -> Union[int, None]:
84
+ """Find existing configuration or create new one if createIfNotExists is True"""
85
+ try:
86
+ configurations = self.get_configurations(project_code)
87
+
88
+ # Search for existing configuration
89
+ for group in configurations:
90
+ if hasattr(group, 'configurations'):
91
+ for config in group.configurations:
92
+ # API returns configurations with 'title' field, not 'name' and 'value'
93
+ # We need to match group.title with config_value.name and config.title with config_value.value
94
+ config_title = config.title if hasattr(config, 'title') else 'No title'
95
+ group_title = group.title if hasattr(group, 'title') else 'No title'
96
+
97
+ if (group_title == config_value.name and config_title == config_value.value):
98
+ return config.id
99
+
100
+ # Configuration not found
101
+ if not self.config.testops.configurations.create_if_not_exists:
102
+ return None
103
+
104
+ # Create new configuration
105
+ # First, try to find existing group or create new one
106
+ group_id = None
107
+ for group in configurations:
108
+ if hasattr(group, 'title') and group.title == config_value.name:
109
+ group_id = group.id
110
+ break
111
+
112
+ if group_id is None:
113
+ # Create new group
114
+ group_create = ConfigurationGroupCreate(title=config_value.name)
115
+ group_response = ConfigurationsApi(self.client).create_configuration_group(
116
+ code=project_code,
117
+ configuration_group_create=group_create
118
+ )
119
+ group_id = group_response.result.id
120
+
121
+ # Create configuration in the group
122
+ config_create = ConfigurationCreate(
123
+ title=config_value.value,
124
+ group_id=group_id
125
+ )
126
+ config_response = ConfigurationsApi(self.client).create_configuration(
127
+ code=project_code,
128
+ configuration_create=config_create
129
+ )
130
+ config_id = config_response.result.id
131
+ return config_id
132
+
133
+ except Exception as e:
134
+ self.logger.log(f"Error at finding/creating configuration {config_value.name}={config_value.value}: {e}", "error")
135
+ return None
136
+
137
+ def complete_run(self, project_code: str, run_id: int) -> None:
70
138
  api_runs = RunsApi(self.client)
71
139
  self.logger.log_debug(f"Completing run {run_id}")
72
140
  res = api_runs.get_run(project_code, run_id).result
@@ -94,22 +162,42 @@ class ApiV1Client(BaseApiClient):
94
162
 
95
163
  def create_test_run(self, project_code: str, title: str, description: str, plan_id=None,
96
164
  environment_id=None) -> str:
165
+ # Process configurations
166
+ configuration_ids = []
167
+
168
+ if self.config.testops.configurations and self.config.testops.configurations.values:
169
+ for config_value in self.config.testops.configurations.values:
170
+ config_id = self.find_or_create_configuration(project_code, config_value)
171
+ if config_id:
172
+ configuration_ids.append(config_id)
173
+
97
174
  kwargs = dict(
98
175
  title=title,
99
176
  description=description,
100
177
  environment_id=(int(environment_id) if environment_id else None),
101
178
  plan_id=(int(plan_id) if plan_id else plan_id),
102
179
  is_autotest=True,
103
- start_time=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
180
+ start_time=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S"),
181
+ tags=self.config.testops.run.tags
104
182
  )
105
- self.logger.log_debug(f"Creating test run with parameters: {kwargs}")
183
+
184
+ # Add configurations if any found
185
+ if configuration_ids:
186
+ kwargs['configurations'] = configuration_ids
187
+
106
188
  try:
107
189
  result = RunsApi(self.client).create_run(
108
190
  code=project_code,
109
191
  run_create=RunCreate(**{k: v for k, v in kwargs.items() if v is not None})
110
192
  )
111
193
 
112
- return result.result.id
194
+ run_id = result.result.id
195
+
196
+ # Update external link if configured
197
+ if self.config.testops.run.external_link and run_id:
198
+ self.update_external_link(project_code, run_id)
199
+
200
+ return run_id
113
201
 
114
202
  except Exception as e:
115
203
  self.logger.log(f"Error at creating test run: {e}", "error")
@@ -122,6 +210,66 @@ class ApiV1Client(BaseApiClient):
122
210
  return True
123
211
  return False
124
212
 
213
+ def enable_public_report(self, project_code: str, run_id: int) -> str:
214
+ """
215
+ Enable public report for a test run and return the public link
216
+
217
+ :param project_code: project code
218
+ :param run_id: test run id
219
+ :return: public report link or None if failed
220
+ """
221
+ try:
222
+ self.logger.log_debug(f"Enabling public report for run {run_id}")
223
+ api_runs = RunsApi(self.client)
224
+
225
+ # Create RunPublic object with status=True
226
+ run_public = RunPublic(status=True)
227
+
228
+ # Call the API to enable public report
229
+ response = api_runs.update_run_publicity(project_code, run_id, run_public)
230
+
231
+ # Extract the public URL from response
232
+ if response.result and response.result.url:
233
+ public_url = response.result.url
234
+ self.logger.log_debug(f"Public report enabled for run {run_id}: {public_url}")
235
+ return public_url
236
+ else:
237
+ self.logger.log_debug(f"Public report enabled for run {run_id} but no URL returned")
238
+ return None
239
+
240
+ except Exception as e:
241
+ self.logger.log(f"Error at enabling public report for run {run_id}: {e}", "error")
242
+ return None
243
+
244
+ def update_external_link(self, project_code: str, run_id: int):
245
+ """Update external link for a test run"""
246
+ try:
247
+ from qase.api_client_v1.models.runexternal_issues import RunexternalIssues
248
+ from qase.api_client_v1.models.runexternal_issues_links_inner import RunexternalIssuesLinksInner
249
+
250
+ external_link = self.config.testops.run.external_link
251
+ api_type = external_link.to_api_type()
252
+
253
+ run_external_issues = RunexternalIssues(
254
+ type=api_type,
255
+ links=[
256
+ RunexternalIssuesLinksInner(
257
+ run_id=run_id,
258
+ external_issue=external_link.link
259
+ )
260
+ ]
261
+ )
262
+
263
+ RunsApi(self.client).run_update_external_issue(
264
+ code=project_code,
265
+ runexternal_issues=run_external_issues
266
+ )
267
+
268
+ self.logger.log(f"External link updated for run {run_id}: {external_link.link}", "debug")
269
+
270
+ except Exception as e:
271
+ self.logger.log(f"Error at updating external link: {e}", "error")
272
+
125
273
  def __should_skip_attachment(self, attachment, result):
126
274
  if (self.config.framework.playwright.video == Video.failed and
127
275
  result.execution.status != 'failed' and
@@ -84,7 +84,7 @@ class ApiV2Client(ApiV1Client):
84
84
  attachments=[attach.hash for attach in attached],
85
85
  steps=steps,
86
86
  steps_type=ResultStepsType.CLASSIC,
87
- params=result.params,
87
+ params=result.params if not self.config.exclude_params else {key: value for key, value in result.params.items() if key not in self.config.exclude_params},
88
88
  param_groups=result.param_groups,
89
89
  message=result.message,
90
90
  defect=self.config.testops.defect,
@@ -111,10 +111,16 @@ class ApiV2Client(ApiV1Client):
111
111
 
112
112
  try:
113
113
  prepared_step = {'execution': {}, 'data': {}, 'steps': []}
114
- prepared_step['execution']['status'] = ResultStepStatus(step.execution.status)
115
- prepared_step['execution']['duration'] = step.execution.duration
116
- prepared_step['execution']['start_time'] = step.execution.start_time
117
- prepared_step['execution']['end_time'] = step.execution.end_time
114
+ if step.execution.status == 'untested':
115
+ prepared_step['execution']['status'] = ResultStepStatus('skipped')
116
+ prepared_step['execution']['duration'] = 0
117
+ prepared_step['execution']['start_time'] = None
118
+ prepared_step['execution']['end_time'] = None
119
+ else:
120
+ prepared_step['execution']['status'] = ResultStepStatus(step.execution.status)
121
+ prepared_step['execution']['duration'] = step.execution.duration
122
+ prepared_step['execution']['start_time'] = step.execution.start_time
123
+ prepared_step['execution']['end_time'] = step.execution.end_time
118
124
 
119
125
  if step.step_type == StepType.TEXT:
120
126
  prepared_step['data']['action'] = step.data.action
@@ -149,6 +155,29 @@ class ApiV2Client(ApiV1Client):
149
155
  if step.step_type == StepType.SLEEP:
150
156
  prepared_step['data']['action'] = f"Sleep for {step.data.duration} seconds"
151
157
 
158
+ if step.step_type == StepType.DB_QUERY:
159
+ # Format database query as action
160
+ action_parts = []
161
+ if step.data.database_type:
162
+ action_parts.append(f"[{step.data.database_type}]")
163
+ action_parts.append(step.data.query)
164
+ prepared_step['data']['action'] = " ".join(action_parts)
165
+
166
+ # Add expected_result if available
167
+ if step.data.expected_result:
168
+ prepared_step['data']['expected_result'] = step.data.expected_result
169
+
170
+ # Add connection info and execution time as input_data
171
+ info_parts = []
172
+ if step.data.connection_info:
173
+ info_parts.append(f"Connection: {step.data.connection_info}")
174
+ if step.data.execution_time is not None:
175
+ info_parts.append(f"Execution time: {step.data.execution_time:.3f}s")
176
+ if step.data.rows_affected is not None:
177
+ info_parts.append(f"Rows affected: {step.data.rows_affected}")
178
+ if info_parts:
179
+ prepared_step['data']['input_data'] = " | ".join(info_parts)
180
+
152
181
  if step.execution.attachments:
153
182
  uploaded_attachments = []
154
183
  for file in step.execution.attachments:
@@ -30,7 +30,7 @@ class BaseApiClient(abc.ABC):
30
30
  pass
31
31
 
32
32
  @abc.abstractmethod
33
- def complete_run(self, project_code: str, run_id: str) -> None:
33
+ def complete_run(self, project_code: str, run_id: int) -> None:
34
34
  """
35
35
  Complete a test run in Qase TestOps
36
36
 
@@ -86,3 +86,14 @@ class BaseApiClient(abc.ABC):
86
86
  :return: None
87
87
  """
88
88
  pass
89
+
90
+ @abc.abstractmethod
91
+ def enable_public_report(self, project_code: str, run_id: int) -> str:
92
+ """
93
+ Enable public report for a test run and return the public link
94
+
95
+ :param project_code: project code
96
+ :param run_id: test run id
97
+ :return: public report link or None if failed
98
+ """
99
+ pass
qase/commons/config.py CHANGED
@@ -1,18 +1,28 @@
1
1
  import os
2
2
  import json
3
- from .logger import Logger
3
+ from .logger import Logger, LoggingOptions
4
4
  from .models.config.qaseconfig import QaseConfig, Mode
5
+ from .utils import QaseUtils
5
6
 
6
7
 
7
8
  class ConfigManager:
8
9
 
9
10
  def __init__(self, config_file='./qase.config.json'):
10
- self.logger = Logger()
11
11
  self.__config_file = config_file
12
12
  self.config = QaseConfig()
13
13
 
14
+ # Initialize temporary logger for error handling during config loading
15
+ self.logger = Logger(debug=False)
16
+
14
17
  self.__load_file_config()
15
18
  self.__load_env_config()
19
+
20
+ # Re-initialize logger with proper logging options after config is loaded
21
+ logging_options = LoggingOptions(
22
+ console=self.config.logging.console if self.config.logging.console is not None else True,
23
+ file=self.config.logging.file if self.config.logging.file is not None else self.config.debug
24
+ )
25
+ self.logger = Logger(debug=self.config.debug, logging_options=logging_options)
16
26
 
17
27
  def validate_config(self):
18
28
  errors: list[str] = []
@@ -53,15 +63,26 @@ class ConfigManager:
53
63
  if config.get("profilers"):
54
64
  self.config.set_profilers(config.get("profilers"))
55
65
 
56
- if config.get("debug"):
66
+ if config.get("debug") is not None:
57
67
  self.config.set_debug(
58
68
  config.get("debug")
59
69
  )
60
70
 
71
+ if config.get("excludeParams"):
72
+ self.config.set_exclude_params(
73
+ config.get("excludeParams")
74
+ )
75
+
76
+ if config.get("statusMapping"):
77
+ self.config.set_status_mapping(
78
+ config.get("statusMapping")
79
+ )
80
+
61
81
  if config.get("executionPlan"):
62
82
  execution_plan = config.get("executionPlan")
63
83
  if execution_plan.get("path"):
64
- self.config.execution_plan.set_path(execution_plan.get("path"))
84
+ self.config.execution_plan.set_path(
85
+ execution_plan.get("path"))
65
86
 
66
87
  if config.get("testops"):
67
88
  testops = config.get("testops")
@@ -70,15 +91,18 @@ class ConfigManager:
70
91
  api = testops.get("api")
71
92
 
72
93
  if api.get("host"):
73
- self.config.testops.api.set_host(api.get("host"))
94
+ self.config.testops.api.set_host(
95
+ api.get("host"))
74
96
 
75
97
  if api.get("token"):
76
- self.config.testops.api.set_token(api.get("token"))
98
+ self.config.testops.api.set_token(
99
+ api.get("token"))
77
100
 
78
101
  if testops.get("project"):
79
- self.config.testops.set_project(testops.get("project"))
102
+ self.config.testops.set_project(
103
+ testops.get("project"))
80
104
 
81
- if testops.get("defect"):
105
+ if testops.get("defect") is not None:
82
106
  self.config.testops.set_defect(
83
107
  testops.get("defect")
84
108
  )
@@ -96,21 +120,59 @@ class ConfigManager:
96
120
  self.config.testops.run.set_id(run.get("id"))
97
121
 
98
122
  if run.get("title"):
99
- self.config.testops.run.set_title(run.get("title"))
123
+ self.config.testops.run.set_title(
124
+ run.get("title"))
100
125
 
101
126
  if run.get("description"):
102
- self.config.testops.run.set_description(run.get("description"))
127
+ self.config.testops.run.set_description(
128
+ run.get("description"))
103
129
 
104
- if run.get("complete"):
130
+ if run.get("complete") is not None:
105
131
  self.config.testops.run.set_complete(
106
132
  run.get("complete")
107
133
  )
108
134
 
135
+ if run.get("tags"):
136
+ self.config.testops.run.set_tags(
137
+ [tag.strip() for tag in run.get("tags")])
138
+
139
+ if run.get("externalLink"):
140
+ self.config.testops.run.set_external_link(
141
+ run.get("externalLink"))
142
+
109
143
  if testops.get("batch"):
110
144
  batch = testops.get("batch")
111
145
 
112
146
  if batch.get("size"):
113
- self.config.testops.batch.set_size(batch.get("size"))
147
+ self.config.testops.batch.set_size(
148
+ batch.get("size"))
149
+
150
+ if testops.get("configurations"):
151
+ configurations = testops.get("configurations")
152
+
153
+ if configurations.get("values"):
154
+ values = configurations.get("values")
155
+ for value in values:
156
+ if value.get("name") and value.get("value"):
157
+ self.config.testops.configurations.add_value(
158
+ value.get("name"), value.get("value"))
159
+
160
+ if configurations.get("createIfNotExists") is not None:
161
+ self.config.testops.configurations.set_create_if_not_exists(
162
+ configurations.get("createIfNotExists"))
163
+
164
+ if testops.get("statusFilter"):
165
+ status_filter = testops.get("statusFilter")
166
+ if isinstance(status_filter, list):
167
+ self.config.testops.set_status_filter(status_filter)
168
+ elif isinstance(status_filter, str):
169
+ # Parse comma-separated string
170
+ self.config.testops.set_status_filter([s.strip() for s in status_filter.split(',')])
171
+
172
+ if testops.get("showPublicReportLink") is not None:
173
+ self.config.testops.set_show_public_report_link(
174
+ testops.get("showPublicReportLink")
175
+ )
114
176
 
115
177
  if config.get("report"):
116
178
  report = config.get("report")
@@ -122,7 +184,8 @@ class ConfigManager:
122
184
  connection = report.get("connection")
123
185
 
124
186
  if connection.get("path"):
125
- self.config.report.connection.set_path(connection.get("path"))
187
+ self.config.report.connection.set_path(
188
+ connection.get("path"))
126
189
 
127
190
  if connection.get("format"):
128
191
  self.config.report.connection.set_format(
@@ -135,7 +198,7 @@ class ConfigManager:
135
198
  if framework.get("pytest"):
136
199
  pytest = framework.get("pytest")
137
200
 
138
- if pytest.get("captureLogs"):
201
+ if pytest.get("captureLogs") is not None:
139
202
  self.config.framework.pytest.set_capture_logs(
140
203
  pytest.get("captureLogs")
141
204
  )
@@ -153,6 +216,9 @@ class ConfigManager:
153
216
  xfail_status.get("xpass")
154
217
  )
155
218
 
219
+ if config.get("logging"):
220
+ self.config.set_logging(config.get("logging"))
221
+
156
222
  except Exception as e:
157
223
  self.logger.log("Failed to load config from file", "error")
158
224
 
@@ -177,6 +243,23 @@ class ConfigManager:
177
243
  if key == 'QASE_DEBUG':
178
244
  self.config.set_debug(value)
179
245
 
246
+ if key == 'QASE_EXCLUDE_PARAMS':
247
+ self.config.set_exclude_params(
248
+ [param.strip() for param in value.split(',')])
249
+
250
+ if key == 'QASE_STATUS_MAPPING':
251
+ # Parse status mapping from environment variable
252
+ # Format: "source1=target1,source2=target2"
253
+ if value:
254
+ mapping_dict = {}
255
+ pairs = value.split(',')
256
+ for pair in pairs:
257
+ pair = pair.strip()
258
+ if pair and '=' in pair:
259
+ source_status, target_status = pair.split('=', 1)
260
+ mapping_dict[source_status.strip()] = target_status.strip()
261
+ self.config.set_status_mapping(mapping_dict)
262
+
180
263
  if key == 'QASE_EXECUTION_PLAN_PATH':
181
264
  self.config.execution_plan.set_path(value)
182
265
 
@@ -207,9 +290,45 @@ class ConfigManager:
207
290
  if key == 'QASE_TESTOPS_RUN_COMPLETE':
208
291
  self.config.testops.run.set_complete(value)
209
292
 
293
+ if key == 'QASE_TESTOPS_RUN_TAGS':
294
+ self.config.testops.run.set_tags(
295
+ [tag.strip() for tag in value.split(',')])
296
+
297
+ if key == 'QASE_TESTOPS_RUN_EXTERNAL_LINK_TYPE':
298
+ if not self.config.testops.run.external_link:
299
+ from .models.external_link import ExternalLinkConfig
300
+ self.config.testops.run.external_link = ExternalLinkConfig()
301
+ self.config.testops.run.external_link.set_type(value)
302
+
303
+ if key == 'QASE_TESTOPS_RUN_EXTERNAL_LINK_URL':
304
+ if not self.config.testops.run.external_link:
305
+ from .models.external_link import ExternalLinkConfig
306
+ self.config.testops.run.external_link = ExternalLinkConfig()
307
+ self.config.testops.run.external_link.set_link(value)
308
+
210
309
  if key == 'QASE_TESTOPS_BATCH_SIZE':
211
310
  self.config.testops.batch.set_size(value)
212
311
 
312
+ if key == 'QASE_TESTOPS_CONFIGURATIONS_VALUES':
313
+ # Parse configurations from environment variable
314
+ # Format: "group1=value1,group2=value2"
315
+ if value:
316
+ config_pairs = value.split(',')
317
+ for pair in config_pairs:
318
+ if '=' in pair:
319
+ name, config_value = pair.split('=', 1)
320
+ self.config.testops.configurations.add_value(name.strip(), config_value.strip())
321
+
322
+ if key == 'QASE_TESTOPS_CONFIGURATIONS_CREATE_IF_NOT_EXISTS':
323
+ self.config.testops.configurations.set_create_if_not_exists(value)
324
+
325
+ if key == 'QASE_TESTOPS_STATUS_FILTER':
326
+ # Parse comma-separated string
327
+ self.config.testops.set_status_filter([s.strip() for s in value.split(',')])
328
+
329
+ if key == 'QASE_TESTOPS_SHOW_PUBLIC_REPORT_LINK':
330
+ self.config.testops.set_show_public_report_link(value)
331
+
213
332
  if key == 'QASE_REPORT_DRIVER':
214
333
  self.config.report.set_driver(value)
215
334
 
@@ -228,6 +347,11 @@ class ConfigManager:
228
347
  if key == 'QASE_PYTEST_XFAIL_STATUS_XPASS':
229
348
  self.config.framework.pytest.xfail_status.set_xpass(value)
230
349
 
350
+ if key == 'QASE_LOGGING_CONSOLE':
351
+ self.config.logging.set_console(QaseUtils.parse_bool(value))
352
+
353
+ if key == 'QASE_LOGGING_FILE':
354
+ self.config.logging.set_file(QaseUtils.parse_bool(value))
231
355
 
232
356
  except Exception as e:
233
- self.logger.log("Failed to load config from env vars {e}", "error")
357
+ self.logger.log(f"Failed to load config from env vars {e}", "error")
qase/commons/logger.py CHANGED
@@ -1,14 +1,39 @@
1
1
  import os
2
2
  import datetime
3
3
  import threading
4
+ from typing import Optional
5
+
6
+
7
+ class LoggingOptions:
8
+ def __init__(self, console: bool = True, file: bool = False):
9
+ self.console = console
10
+ self.file = file
4
11
 
5
12
 
6
13
  class Logger:
7
14
  _log_file = None
8
15
 
9
- def __init__(self, debug: bool = False, prefix: str = '', dir: str = os.path.join('.', 'logs')) -> None:
16
+ def __init__(self, debug: bool = False, prefix: str = '', dir: str = os.path.join('.', 'logs'),
17
+ logging_options: Optional[LoggingOptions] = None) -> None:
10
18
  self.debug = debug
11
- if self.debug:
19
+ self.prefix = prefix
20
+ self.dir = dir
21
+
22
+ # Initialize logging options
23
+ if logging_options is None:
24
+ # Default behavior: console always enabled, file enabled only in debug mode
25
+ self.logging_options = LoggingOptions(
26
+ console=True,
27
+ file=debug
28
+ )
29
+ else:
30
+ self.logging_options = logging_options
31
+
32
+ # Override with environment variables if set
33
+ self._load_env_logging_options()
34
+
35
+ # Setup file logging if enabled
36
+ if self.logging_options.file:
12
37
  if Logger._log_file is None:
13
38
  timestamp = self._get_timestamp()
14
39
  filename = f'{prefix}_{timestamp}.log'
@@ -21,16 +46,38 @@ class Logger:
21
46
 
22
47
  self.lock = threading.Lock()
23
48
 
49
+ def _load_env_logging_options(self):
50
+ """Load logging options from environment variables"""
51
+ # QASE_LOGGING_CONSOLE
52
+ console_env = os.environ.get('QASE_LOGGING_CONSOLE')
53
+ if console_env is not None:
54
+ self.logging_options.console = console_env.lower() in ('true', '1', 'yes', 'on')
55
+
56
+ # QASE_LOGGING_FILE
57
+ file_env = os.environ.get('QASE_LOGGING_FILE')
58
+ if file_env is not None:
59
+ self.logging_options.file = file_env.lower() in ('true', '1', 'yes', 'on')
60
+
61
+ # Legacy QASE_DEBUG support
62
+ debug_env = os.environ.get('QASE_DEBUG')
63
+ if debug_env is not None and debug_env.lower() in ('true', '1', 'yes', 'on'):
64
+ # When debug is enabled via env, enable file logging if not explicitly disabled
65
+ if not hasattr(self.logging_options, 'file') or self.logging_options.file is None:
66
+ self.logging_options.file = True
67
+
24
68
  def log(self, message: str, level: str = 'info'):
25
69
  time_str = self._get_timestamp("%H:%M:%S")
26
70
  log = f"[Qase][{time_str}][{level}] {message}\n"
27
71
 
28
- try:
29
- print(log, end='')
30
- except (OSError, IOError):
31
- pass
72
+ # Console output
73
+ if self.logging_options.console:
74
+ try:
75
+ print(log, end='')
76
+ except (OSError, IOError):
77
+ pass
32
78
 
33
- if self.debug:
79
+ # File output
80
+ if self.logging_options.file:
34
81
  with self.lock:
35
82
  with open(Logger._log_file, 'a', encoding='utf-8') as f:
36
83
  f.write(log)
@@ -39,6 +86,15 @@ class Logger:
39
86
  if self.debug:
40
87
  self.log(message, 'debug')
41
88
 
89
+ def log_error(self, message: str):
90
+ self.log(message, 'error')
91
+
92
+ def log_warning(self, message: str):
93
+ self.log(message, 'warning')
94
+
95
+ def log_info(self, message: str):
96
+ self.log(message, 'info')
97
+
42
98
  @staticmethod
43
99
  def _get_timestamp(fmt: str = "%Y%m%d"):
44
100
  now = datetime.datetime.now()