Skip to content

Commit 04d7968

Browse files
skamanbourajat
authored andcommitted
Restore config singleton and fix package declarations
1 parent 917562f commit 04d7968

3 files changed

Lines changed: 88 additions & 7 deletions

File tree

aidefense/config.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from abc import ABC, abstractmethod
2020
import logging
21+
import threading
2122

2223
import aiohttp
2324
import requests
@@ -30,6 +31,9 @@
3031
class BaseConfig(ABC):
3132
"""Base configuration class for all AI Defense SDK clients."""
3233

34+
_instances = {}
35+
_lock = threading.Lock()
36+
3337
DEFAULT_STATUS_FORCELIST = (429, 500, 502, 503, 504)
3438
DEFAULT_BACKOFF_FACTOR = 0.5
3539
DEFAULT_TOTAL = 3
@@ -58,16 +62,44 @@ class BaseConfig(ABC):
5862
"me-central-1": "https://uae.api.aidefense.security.cisco.com",
5963
}
6064

65+
# Backward-compat: map legacy short codes to canonical AWS region names.
6166
_SHORT_REGION_MAP = {
6267
"us": "us-west-2",
6368
"eu": "eu-central-1",
6469
"apj": "ap-northeast-1",
6570
}
6671

72+
def __new__(cls, *args, **kwargs):
73+
if cls is BaseConfig:
74+
raise TypeError("BaseConfig is abstract and cannot be instantiated directly")
75+
76+
# Singleton constructor for Config. Ensures only one instance is created per subclass.
77+
# Acquiring a lock is expensive, so we only do it if we need to. Future initializations will be fast.
78+
# Once the lock is acquired, we check again to ensure no other thread has created an instance.
79+
if cls not in cls._instances:
80+
with cls._lock:
81+
if cls not in cls._instances:
82+
cls._instances[cls] = super().__new__(cls)
83+
84+
return cls._instances[cls]
85+
6786
_logger = logging.getLogger("aidefense_sdk.config")
6887

6988
def __init__(self, *args, **kwargs):
70-
self._initialize(*args, **kwargs)
89+
# Double-checked locking: fast path avoids the lock for already-init'd
90+
# singletons; the lock prevents concurrent first-time callers from both
91+
# running _initialize on the same instance.
92+
if not getattr(self, "_initialized", False):
93+
with self._lock:
94+
if not getattr(self, "_initialized", False):
95+
try:
96+
self._initialize(*args, **kwargs)
97+
self._initialized = True
98+
except Exception:
99+
self._instances.pop(type(self), None)
100+
raise
101+
elif args or kwargs:
102+
self._warn_if_params_differ(*args, **kwargs)
71103

72104
def _set_region(self, region: str):
73105
if not isinstance(region, str):
@@ -150,6 +182,53 @@ def _set_pool_config(self, pool_config: dict):
150182
"pool_maxsize": pool_config.get("pool_maxsize", self.DEFAULT_POOL_MAXSIZE),
151183
}
152184

185+
_INIT_PARAM_NAMES = (
186+
"region", "runtime_base_url", "management_base_url", "timeout",
187+
)
188+
189+
def _warn_if_params_differ(self, *args, **kwargs):
190+
"""Log a warning when the singleton is re-requested with different parameters."""
191+
merged = dict(zip(self._INIT_PARAM_NAMES, args))
192+
merged.update(kwargs)
193+
194+
_NORMALIZERS = {
195+
"region": self._normalize_requested_region,
196+
"runtime_base_url": self._normalize_url,
197+
"management_base_url": self._normalize_url,
198+
}
199+
diffs = []
200+
for key in self._INIT_PARAM_NAMES:
201+
if key not in merged or merged[key] is None:
202+
continue
203+
requested = merged[key]
204+
normalizer = _NORMALIZERS.get(key)
205+
if normalizer:
206+
requested = normalizer(requested)
207+
current = getattr(self, key, None)
208+
if current is not None and requested != current:
209+
diffs.append(f"{key}={current!r} (requested {merged[key]!r})")
210+
if diffs:
211+
self._logger.warning(
212+
"%s singleton already initialized. Ignoring different "
213+
"parameters: %s. Construct %s once and share it, or clear "
214+
"%s._instances to re-initialize.",
215+
type(self).__name__,
216+
", ".join(diffs),
217+
type(self).__name__,
218+
type(self).__name__,
219+
)
220+
221+
@staticmethod
222+
def _normalize_requested_region(region):
223+
"""Map short-code regions to canonical names for comparison."""
224+
_SHORT = {"us": "us-west-2", "eu": "eu-central-1", "apj": "ap-northeast-1"}
225+
return _SHORT.get(region, region) if isinstance(region, str) else region
226+
227+
@staticmethod
228+
def _normalize_url(url):
229+
"""Strip trailing slash to match stored value normalization."""
230+
return url.rstrip("/") if isinstance(url, str) else url
231+
153232
@abstractmethod
154233
def _initialize(self, *args, **kwargs):
155234
pass

aidefense/tests/test_config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ def test_config_default():
3939
assert hasattr(config, "connection_pool")
4040

4141

42+
def test_config_is_singleton():
43+
first = Config(region="us-west-2")
44+
second = Config()
45+
46+
assert second is first
47+
48+
4249
def test_config_with_runtime_base_url():
4350
url = "https://custom.endpoint.com"
4451
config = Config(runtime_base_url=url)

pyproject.toml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,7 @@ classifiers = [
2020
"Operating System :: OS Independent",
2121
"Intended Audience :: Developers"
2222
]
23-
packages = [
24-
{include = "aidefense"},
25-
{include = "ai_validation"},
26-
{include = "ai_validation_service"},
27-
{include = "ai_defense"},
28-
]
23+
packages = [{include = "aidefense"}]
2924

3025
[tool.poetry.dependencies]
3126
aiohttp = "^3.13.2"

0 commit comments

Comments
 (0)