Skip to content

Commit ac788e6

Browse files
authored
[connectors-sdk] BaseConnectorSettings support for connectors as module (#5295)
1 parent ada21b9 commit ac788e6

File tree

7 files changed

+311
-106
lines changed

7 files changed

+311
-106
lines changed

connectors-sdk/connectors_sdk/settings/base_settings.py

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,13 @@
66
These models can be extended to create specific configurations for different types of connectors.
77
"""
88

9-
import os
9+
import sys
1010
from abc import ABC
1111
from copy import deepcopy
1212
from datetime import timedelta
1313
from pathlib import Path
1414
from typing import Any, Literal, Self
1515

16-
import __main__
1716
from connectors_sdk.settings.annotated_types import ListFromString
1817
from connectors_sdk.settings.exceptions import ConfigValidationError
1918
from pydantic import (
@@ -87,6 +86,47 @@ class _SettingsLoader(BaseSettings):
8786
enable_decoding=False,
8887
)
8988

89+
@classmethod
90+
def _get_connector_main_path(cls) -> Path:
91+
"""Locate the main module of the running connector.
92+
This method is used to locate configuration files relative to connector's entrypoint.
93+
94+
Notes:
95+
- This method assumes that the connector is launched using a file-backed entrypoint
96+
(i.e., `python -m <module>` or `python <file>`).
97+
- At module import time, `__main__.__file__` might not be available yet,
98+
thus this method should be called at runtime only.
99+
"""
100+
main = sys.modules.get("__main__")
101+
if main and getattr(main, "__file__", None):
102+
return Path(main.__file__).resolve() # type: ignore
103+
104+
raise RuntimeError(
105+
"Cannot determine connector's location: __main__.__file__ is not available. "
106+
"Ensure the connector is launched using `python -m <module>` or a file-backed entrypoint."
107+
)
108+
109+
@classmethod
110+
def _get_config_yml_file_path(cls) -> Path | None:
111+
"""Locate the `config.yml` file of the running connector."""
112+
main_path = cls._get_connector_main_path()
113+
config_yml_legacy_file_path = main_path.parent / "config.yml"
114+
config_yml_file_path = main_path.parent.parent / "config.yml"
115+
116+
if config_yml_legacy_file_path.is_file():
117+
return config_yml_legacy_file_path
118+
elif config_yml_file_path.is_file():
119+
return config_yml_file_path
120+
return None
121+
122+
@classmethod
123+
def _get_dot_env_file_path(cls) -> Path | None:
124+
"""Locate the `.env` file of the running connector."""
125+
main_path = cls._get_connector_main_path()
126+
dot_env_file_path = main_path.parent.parent / ".env"
127+
128+
return dot_env_file_path if dot_env_file_path.is_file() else None
129+
90130
@classmethod
91131
def settings_customise_sources(
92132
cls,
@@ -109,26 +149,22 @@ def settings_customise_sources(
109149
1. If a config.yml file is found, the order will be: `ENV VAR` → config.yml → default value
110150
2. If a .env file is found, the order will be: `ENV VAR` → .env → default value
111151
"""
112-
_main_path = os.path.dirname(os.path.abspath(__main__.__file__))
113-
114-
settings_cls.model_config["env_file"] = f"{_main_path}/../.env"
115-
116-
if not settings_cls.model_config["yaml_file"]:
117-
if Path(f"{_main_path}/config.yml").is_file():
118-
settings_cls.model_config["yaml_file"] = f"{_main_path}/config.yml"
119-
if Path(f"{_main_path}/../config.yml").is_file():
120-
settings_cls.model_config["yaml_file"] = f"{_main_path}/../config.yml"
121-
122-
if Path(settings_cls.model_config["yaml_file"] or "").is_file(): # type: ignore
152+
config_yml_file_path = cls._get_config_yml_file_path()
153+
if config_yml_file_path:
154+
settings_cls.model_config["yaml_file"] = config_yml_file_path
123155
return (
124156
env_settings,
125157
YamlConfigSettingsSource(settings_cls),
126158
)
127-
if Path(settings_cls.model_config["env_file"] or "").is_file(): # type: ignore
159+
160+
dot_env_file_path = cls._get_dot_env_file_path()
161+
if dot_env_file_path:
162+
settings_cls.model_config["env_file"] = dot_env_file_path
128163
return (
129164
env_settings,
130165
DotEnvSettingsSource(settings_cls),
131166
)
167+
132168
return (env_settings,)
133169

134170
@classmethod

connectors-sdk/tests/test_models/__init__.py

Whitespace-only changes.

connectors-sdk/tests/test_settings/__init__.py

Whitespace-only changes.
Lines changed: 29 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
1+
import sys
2+
from pathlib import Path
3+
from types import SimpleNamespace
4+
15
import pytest
26

37

48
@pytest.fixture
5-
def mock_basic_environment(monkeypatch):
9+
def mock_main_path(monkeypatch):
10+
"""Mock the path of `__main__.__file__` for `_SettingsLoader._get_connector_main_path` calls."""
11+
12+
monkeypatch.setitem(
13+
sys.modules, "__main__", SimpleNamespace(__file__="/app/src/main.py")
14+
)
15+
16+
17+
@pytest.fixture
18+
def mock_environment(monkeypatch):
19+
"""Mock `os.environ` for `_SettingsLoader` and `BaseConnectorSettings` calls."""
20+
621
monkeypatch.setenv("OPENCTI_URL", "http://localhost:8080")
722
monkeypatch.setenv("OPENCTI_TOKEN", "changeme")
823
monkeypatch.setenv("CONNECTOR_ID", "connector-poc--uid")
@@ -13,60 +28,26 @@ def mock_basic_environment(monkeypatch):
1328

1429

1530
@pytest.fixture
16-
def mock_yaml_file_presence(monkeypatch):
17-
def is_file(self):
18-
if self.name == "config.yml":
19-
return True
20-
return False
21-
22-
monkeypatch.setattr("pathlib.Path.is_file", is_file)
31+
def mock_config_yml_file_presence(monkeypatch):
32+
"""Mock the path of `config.yml` for `_SettingsLoader` and `BaseConnectorSettings` calls."""
2333

24-
25-
@pytest.fixture
26-
def mock_env_file_presence(monkeypatch):
27-
def is_file(self):
28-
if self.name == ".env":
29-
return True
30-
return False
31-
32-
monkeypatch.setattr("pathlib.Path.is_file", is_file)
33-
34-
35-
@pytest.fixture
36-
def mock_yaml_config_settings_read_files(monkeypatch):
37-
def read_files(_, __):
38-
return {
39-
"connector": {
40-
"duration_period": "PT5M",
41-
"id": "connector-poc--uid",
42-
"log_level": "error",
43-
"name": "Test Connector",
44-
"scope": "test",
45-
},
46-
"opencti": {"token": "changeme", "url": "http://localhost:8080"},
47-
}
34+
def get_config_yml_file_path():
35+
return Path(__file__).parent / "data" / "config.test.yml"
4836

4937
monkeypatch.setattr(
50-
"pydantic_settings.YamlConfigSettingsSource._read_files", read_files
38+
"connectors_sdk.settings.base_settings._SettingsLoader._get_config_yml_file_path",
39+
get_config_yml_file_path,
5140
)
5241

5342

5443
@pytest.fixture
55-
def mock_env_config_settings_read_env_files(monkeypatch):
56-
def _read_env_files(self):
57-
if self.settings_cls.__name__ == "SettingsLoader":
58-
return {
59-
"opencti": {"url": "http://localhost:8080", "token": "changeme"},
60-
"connector": {
61-
"id": "connector-poc--uid",
62-
"name": "Test Connector",
63-
"duration_period": "PT5M",
64-
"log_level": "error",
65-
"scope": "test",
66-
},
67-
}
68-
return {}
44+
def mock_dot_env_file_presence(monkeypatch):
45+
"""Mock the path of `.env` for `_SettingsLoader` and `BaseConnectorSettings` calls."""
46+
47+
def get_dot_env_file_path():
48+
return Path(__file__).parent / "data" / ".env.test"
6949

7050
monkeypatch.setattr(
71-
"pydantic_settings.DotEnvSettingsSource._load_env_vars", _read_env_files
51+
"connectors_sdk.settings.base_settings._SettingsLoader._get_dot_env_file_path",
52+
get_dot_env_file_path,
7253
)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
OPENCTI_URL=http://localhost:8080
2+
OPENCTI_TOKEN=changeme
3+
CONNECTOR_ID=connector-poc--uid
4+
CONNECTOR_NAME=Test Connector
5+
CONNECTOR_SCOPE=test
6+
CONNECTOR_LOG_LEVEL=error
7+
CONNECTOR_DURATION_PERIOD=PT5M
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
opencti:
2+
url: http://localhost:8080
3+
token: changeme
4+
5+
connector:
6+
id: connector-poc--uid
7+
name: Test Connector
8+
scope: test
9+
log_level: error
10+
duration_period: PT5M
11+

0 commit comments

Comments
 (0)