nd-sdk 1.0.0__tar.gz

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.
Files changed (54) hide show
  1. nd_sdk-1.0.0/PKG-INFO +16 -0
  2. nd_sdk-1.0.0/nd_sdk/__init__.py +14 -0
  3. nd_sdk-1.0.0/nd_sdk/caching/__init__.py +0 -0
  4. nd_sdk-1.0.0/nd_sdk/caching/base.py +9 -0
  5. nd_sdk-1.0.0/nd_sdk/caching/factory.py +11 -0
  6. nd_sdk-1.0.0/nd_sdk/caching/in_memory_cache.py +12 -0
  7. nd_sdk-1.0.0/nd_sdk/caching/redis_cache.py +425 -0
  8. nd_sdk-1.0.0/nd_sdk/config/__init__.py +0 -0
  9. nd_sdk-1.0.0/nd_sdk/config/data_config.py +42 -0
  10. nd_sdk-1.0.0/nd_sdk/config/factory.py +8 -0
  11. nd_sdk-1.0.0/nd_sdk/config/loaders.py +11 -0
  12. nd_sdk-1.0.0/nd_sdk/config/settings.py +5 -0
  13. nd_sdk-1.0.0/nd_sdk/observability/__init__.py +0 -0
  14. nd_sdk-1.0.0/nd_sdk/observability/context_manager.py +703 -0
  15. nd_sdk-1.0.0/nd_sdk/observability/factory.py +25 -0
  16. nd_sdk-1.0.0/nd_sdk/observability/logging/__init__.py +0 -0
  17. nd_sdk-1.0.0/nd_sdk/observability/logging/base.py +24 -0
  18. nd_sdk-1.0.0/nd_sdk/observability/logging/cloudwatch_logger.py +23 -0
  19. nd_sdk-1.0.0/nd_sdk/observability/logging/otel_logger.py +601 -0
  20. nd_sdk-1.0.0/nd_sdk/observability/logging/std_logger.py +23 -0
  21. nd_sdk-1.0.0/nd_sdk/observability/metrics/__init__.py +0 -0
  22. nd_sdk-1.0.0/nd_sdk/observability/metrics/base.py +50 -0
  23. nd_sdk-1.0.0/nd_sdk/observability/metrics/otel_metrics.py +611 -0
  24. nd_sdk-1.0.0/nd_sdk/observability/otel_exporter.py +372 -0
  25. nd_sdk-1.0.0/nd_sdk/observability/tracing/__init__.py +0 -0
  26. nd_sdk-1.0.0/nd_sdk/observability/tracing/base.py +144 -0
  27. nd_sdk-1.0.0/nd_sdk/observability/tracing/otel_tracer.py +1135 -0
  28. nd_sdk-1.0.0/nd_sdk/storage/__init__.py +0 -0
  29. nd_sdk-1.0.0/nd_sdk/storage/base.py +22 -0
  30. nd_sdk-1.0.0/nd_sdk/storage/factory.py +19 -0
  31. nd_sdk-1.0.0/nd_sdk/storage/storage_clients/__init__.py +0 -0
  32. nd_sdk-1.0.0/nd_sdk/storage/storage_clients/azure_client.py +57 -0
  33. nd_sdk-1.0.0/nd_sdk/storage/storage_clients/minio_client.py +49 -0
  34. nd_sdk-1.0.0/nd_sdk/storage/storage_clients/s3_client.py +52 -0
  35. nd_sdk-1.0.0/nd_sdk/storage/storage_factory/__init__.py +0 -0
  36. nd_sdk-1.0.0/nd_sdk/storage/storage_factory/azure_factory.py +24 -0
  37. nd_sdk-1.0.0/nd_sdk/storage/storage_factory/minio_factory.py +24 -0
  38. nd_sdk-1.0.0/nd_sdk/storage/storage_factory/s3_factory.py +24 -0
  39. nd_sdk-1.0.0/nd_sdk/utils/__init__.py +0 -0
  40. nd_sdk-1.0.0/nd_sdk/utils/decorators.py +8 -0
  41. nd_sdk-1.0.0/nd_sdk/utils/exceptions.py +2 -0
  42. nd_sdk-1.0.0/nd_sdk/utils/file_handler.py +154 -0
  43. nd_sdk-1.0.0/nd_sdk/utils/singleton.py +36 -0
  44. nd_sdk-1.0.0/nd_sdk/utils/string_utils.py +7 -0
  45. nd_sdk-1.0.0/nd_sdk/web/__init__.py +0 -0
  46. nd_sdk-1.0.0/nd_sdk/web/base.py +12 -0
  47. nd_sdk-1.0.0/nd_sdk/web/flask_wrapper.py +61 -0
  48. nd_sdk-1.0.0/nd_sdk.egg-info/PKG-INFO +16 -0
  49. nd_sdk-1.0.0/nd_sdk.egg-info/SOURCES.txt +52 -0
  50. nd_sdk-1.0.0/nd_sdk.egg-info/dependency_links.txt +1 -0
  51. nd_sdk-1.0.0/nd_sdk.egg-info/requires.txt +9 -0
  52. nd_sdk-1.0.0/nd_sdk.egg-info/top_level.txt +1 -0
  53. nd_sdk-1.0.0/setup.cfg +4 -0
  54. nd_sdk-1.0.0/setup.py +22 -0
nd_sdk-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.1
2
+ Name: nd-sdk
3
+ Version: 1.0.0
4
+ Summary: Unified SDK for Observability, Caching and Storage
5
+ Author: Jeyesh Vishnu
6
+ Author-email: jeyesh.vishnu@novacisdigital.com
7
+ Requires-Python: >=3.8
8
+ Requires-Dist: azure-storage-blob~=12.16.0
9
+ Requires-Dist: azure-core~=1.29.5
10
+ Requires-Dist: Flask~=2.1.2
11
+ Requires-Dist: redis~=6.1.1
12
+ Requires-Dist: Werkzeug~=2.2.2
13
+ Requires-Dist: opentelemetry-api>=1.20.0
14
+ Requires-Dist: opentelemetry-sdk>=1.20.0
15
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.20.0
16
+ Requires-Dist: aiohttp>=3.8.0
@@ -0,0 +1,14 @@
1
+ """
2
+ Your Company SDK - Unified Framework for Observability, Storage & Caching
3
+ """
4
+ from .storage.factory import get_storage
5
+ from .caching.factory import get_cache
6
+ from .observability.factory import get_logger, get_tracer, get_metrics
7
+
8
+ __all__ = [
9
+ "get_logger",
10
+ "get_tracer",
11
+ "get_metrics",
12
+ "get_storage",
13
+ "get_cache",
14
+ ]
File without changes
@@ -0,0 +1,9 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ class CacheProvider(ABC):
4
+
5
+ @abstractmethod
6
+ def get(self, key: str): pass
7
+
8
+ @abstractmethod
9
+ def set(self, key: str, value, ttl=None): pass
@@ -0,0 +1,11 @@
1
+ from .in_memory_cache import InMemoryCache
2
+ from .redis_cache import RedisCache
3
+ from ..utils.singleton import singleton
4
+
5
+ @singleton
6
+ def get_cache(provider="redis", environ="dev"):
7
+ if provider == "memory":
8
+ return InMemoryCache()
9
+ if provider == "redis":
10
+ return RedisCache(environment=environ)
11
+ raise ValueError(f"Unknown cache provider: {provider}")
@@ -0,0 +1,12 @@
1
+ from .base import CacheProvider
2
+
3
+ class InMemoryCache(CacheProvider):
4
+
5
+ def __init__(self):
6
+ self.store = {}
7
+
8
+ def get(self, key: str):
9
+ return self.store.get(key)
10
+
11
+ def set(self, key: str, value, ttl=None):
12
+ self.store[key] = value
@@ -0,0 +1,425 @@
1
+ from .base import CacheProvider
2
+ from ..observability.factory import get_logger
3
+ from ..config.loaders import load_env
4
+ from abc import ABC, abstractmethod
5
+ from typing import Any, Dict, List, Optional, Tuple
6
+ import redis
7
+ import json
8
+ import re
9
+
10
+
11
+ class RedisCache(CacheProvider):
12
+ """
13
+ Enhanced Redis cache provider with pattern matching support.
14
+
15
+ Features:
16
+ - Wildcard pattern matching with SCAN for memory-efficient key retrieval
17
+ - Support for multiple wildcards in a single pattern
18
+ - Configurable batch size for SCAN operations
19
+ - Automatic serialization/deserialization
20
+ """
21
+
22
+ DEFAULT_TTL = 3600 # 1 hour
23
+ SCAN_BATCH_SIZE = 1000
24
+
25
+ def __init__(self, scan_batch_size: int = SCAN_BATCH_SIZE, environment: str = "dev"):
26
+ """
27
+ Initialize Redis cache with configurable scan batch size.
28
+
29
+ Args:
30
+ scan_batch_size: Number of keys to retrieve per SCAN iteration
31
+ """
32
+ self.cache = redis.Redis(**load_env("cache_"))
33
+ self.serializer = JSONSerializer()
34
+ self.logger = get_logger.get()
35
+ self.scan_batch_size = scan_batch_size
36
+ self.environment = environment
37
+
38
+ def get(self, key: str) -> Any:
39
+ """
40
+ Retrieve a value from cache.
41
+
42
+ Args:
43
+ key: Cache key
44
+
45
+ Returns:
46
+ Deserialized value or None if not found
47
+ """
48
+ try:
49
+ data = self.cache.get(key)
50
+ if data is None:
51
+ self.logger.debug(f"Cache miss for key: {key}")
52
+ return None
53
+ return self.serializer.deserialize(data)
54
+ except Exception as e:
55
+ self.logger.error(f"Error retrieving key '{key}': {e}")
56
+ return None
57
+
58
+ def set(self, key: str, value: Any, ttl: Optional[int] = None) -> bool:
59
+ """
60
+ Store a value in cache with optional TTL.
61
+
62
+ Args:
63
+ key: Cache key
64
+ value: Value to store
65
+ ttl: Time to live in seconds (None for no expiration)
66
+
67
+ Returns:
68
+ True if successful, False otherwise
69
+ """
70
+ try:
71
+ serialized = self.serializer.serialize(value)
72
+ if ttl is not None:
73
+ result = self.cache.setex(key, ttl, serialized)
74
+ else:
75
+ result = self.cache.set(key, serialized)
76
+
77
+ self.logger.debug(f"[REDIS] SET {key} = {value} (TTL: {ttl})")
78
+ return bool(result)
79
+ except Exception as e:
80
+ self.logger.error(f"Error setting key '{key}': {e}")
81
+ return False
82
+
83
+ def delete(self, key: str) -> bool:
84
+ """
85
+ Delete a key from cache.
86
+
87
+ Args:
88
+ key: Cache key to delete
89
+
90
+ Returns:
91
+ True if key was deleted, False otherwise
92
+ """
93
+ try:
94
+ result = self.cache.delete(key)
95
+ self.logger.debug(f"[REDIS] DELETE {key}")
96
+ return bool(result)
97
+ except Exception as e:
98
+ self.logger.error(f"Error deleting key '{key}': {e}")
99
+ return False
100
+
101
+ def build_pattern(self, pattern_dict: Dict[str, str]) -> str:
102
+ """
103
+ Build a Redis pattern from a dictionary.
104
+
105
+ Args:
106
+ pattern_dict: Dictionary with keys as pattern components.
107
+ Use "*" for wildcard matching.
108
+
109
+ Returns:
110
+ Redis SCAN-compatible pattern string
111
+ '*:Cassandra'
112
+ """
113
+ pattern_dict['environment'] = self.environment
114
+ return ":".join(str(v) for v in pattern_dict.values())
115
+
116
+ def scan_keys(self, pattern: str) -> List[str]:
117
+ """
118
+ Scan for keys matching a pattern using SCAN command.
119
+
120
+ Uses SCAN instead of KEYS for production safety - it doesn't
121
+ block the Redis server and works incrementally.
122
+
123
+ Args:
124
+ pattern: Redis pattern (supports * and ? wildcards)
125
+
126
+ Returns:
127
+ List of matching keys (as strings)
128
+ """
129
+ pattern = f"{self.environment}:{pattern}"
130
+ matched_keys = []
131
+ cursor = 0
132
+
133
+ try:
134
+ while True:
135
+ cursor, keys = self.cache.scan(
136
+ cursor=cursor,
137
+ match=pattern,
138
+ count=self.scan_batch_size
139
+ )
140
+ # Decode bytes to strings
141
+ matched_keys.extend([k.decode('utf-8') if isinstance(k, bytes) else k
142
+ for k in keys])
143
+
144
+ if cursor == 0:
145
+ break
146
+
147
+ self.logger.info(f"Found {len(matched_keys)} keys matching pattern: {pattern}")
148
+ return matched_keys
149
+
150
+ except Exception as e:
151
+ self.logger.error(f"Error scanning keys with pattern '{pattern}': {e}")
152
+ return []
153
+
154
+ def get_multiple_by_pattern(self, pattern: str) -> Dict[str, Any]:
155
+ """
156
+ Retrieve all key-value pairs matching a pattern.
157
+
158
+ Args:
159
+ pattern: Redis pattern with wildcards
160
+
161
+ Returns:
162
+ Dictionary mapping keys to deserialized values
163
+ """
164
+ pattern = f"{self.environment}:{pattern}"
165
+ results = {}
166
+ keys = self.scan_keys(pattern)
167
+
168
+ if not keys:
169
+ return results
170
+
171
+ try:
172
+ # Use pipeline for efficient multi-get
173
+ pipeline = self.cache.pipeline()
174
+ for key in keys:
175
+ pipeline.get(key)
176
+
177
+ values = pipeline.execute()
178
+
179
+ for key, value in zip(keys, values):
180
+ if value is not None:
181
+ try:
182
+ results[key] = self.serializer.deserialize(value)
183
+ except Exception as e:
184
+ self.logger.warning(f"Failed to deserialize key '{key}': {e}")
185
+ results[key] = None
186
+
187
+ return results
188
+
189
+ except Exception as e:
190
+ self.logger.error(f"Error retrieving values for pattern '{pattern}': {e}")
191
+ return {}
192
+
193
+ def get_by_pattern(self, pattern: str) -> Optional[Any]:
194
+ """
195
+ Retrieve all key-value pairs matching a pattern.
196
+
197
+ Args:
198
+ pattern: Redis pattern with wildcards
199
+
200
+ Returns:
201
+ Dictionary mapping keys to deserialized values
202
+ """
203
+ pattern = f"{self.environment}:{pattern}"
204
+ keys = self.scan_keys(pattern)
205
+
206
+ if not keys:
207
+ return None
208
+
209
+ try:
210
+ # Use pipeline for efficient multi-get
211
+ pipeline = self.cache.pipeline()
212
+ for key in keys:
213
+ pipeline.get(key)
214
+
215
+ values = pipeline.execute()
216
+
217
+ for key, value in zip(keys, values):
218
+ if value is not None:
219
+ try:
220
+ return self.serializer.deserialize(value)
221
+ except Exception as e:
222
+ self.logger.warning(f"Failed to deserialize key '{key}': {e}")
223
+ return None
224
+
225
+ return None
226
+
227
+ except Exception as e:
228
+ self.logger.error(f"Error retrieving values for pattern '{pattern}': {e}")
229
+ return {}
230
+
231
+ def get_by_dict(self, pattern_dict: Dict[str, str], multiple: bool = False) -> Any:
232
+ """
233
+ Retrieve key-value pairs using a pattern dictionary.
234
+
235
+ Convenience method that builds the pattern and retrieves values.
236
+
237
+ Args:
238
+ pattern_dict: Dictionary defining the pattern
239
+
240
+ Returns:
241
+ Dictionary mapping keys to values
242
+ :param pattern_dict:
243
+ :param multiple:
244
+ """
245
+ pattern_dict['environment'] = self.environment
246
+ pattern = self.build_pattern(pattern_dict)
247
+ if multiple:
248
+ return self.get_multiple_by_pattern(pattern)
249
+ return self.get_by_pattern(pattern)
250
+
251
+ def delete_by_pattern(self, pattern: str, batch_size: int = 100) -> int:
252
+ """
253
+ Delete all keys matching a pattern.
254
+
255
+ Args:
256
+ pattern: Redis pattern with wildcards
257
+ batch_size: Number of keys to delete per batch
258
+
259
+ Returns:
260
+ Number of keys deleted
261
+ """
262
+ pattern = f"{self.environment}:{pattern}"
263
+ keys = self.scan_keys(pattern)
264
+
265
+ if not keys:
266
+ return 0
267
+
268
+ deleted_count = 0
269
+ try:
270
+ # Delete in batches using pipeline
271
+ for i in range(0, len(keys), batch_size):
272
+ batch = keys[i:i + batch_size]
273
+ pipeline = self.cache.pipeline()
274
+ for key in batch:
275
+ pipeline.delete(key)
276
+ results = pipeline.execute()
277
+ deleted_count += sum(results)
278
+
279
+ self.logger.info(f"Deleted {deleted_count} keys matching pattern: {pattern}")
280
+ return deleted_count
281
+
282
+ except Exception as e:
283
+ self.logger.error(f"Error deleting keys with pattern '{pattern}': {e}")
284
+ return deleted_count
285
+
286
+ def exists(self, key: str) -> bool:
287
+ """
288
+ Check if a key exists in cache.
289
+
290
+ Args:
291
+ key: Cache key to check
292
+
293
+ Returns:
294
+ True if key exists, False otherwise
295
+ """
296
+ try:
297
+ return bool(self.cache.exists(key))
298
+ except Exception as e:
299
+ self.logger.error(f"Error checking existence of key '{key}': {e}")
300
+ return False
301
+
302
+ def get_ttl(self, key: str) -> Optional[int]:
303
+ """
304
+ Get the remaining time to live for a key.
305
+
306
+ Args:
307
+ key: Cache key
308
+
309
+ Returns:
310
+ TTL in seconds, -1 if no expiration, None if key doesn't exist
311
+ """
312
+ try:
313
+ ttl = self.cache.ttl(key)
314
+ if ttl == -2: # Key doesn't exist
315
+ return None
316
+ return ttl
317
+ except Exception as e:
318
+ self.logger.error(f"Error getting TTL for key '{key}': {e}")
319
+ return None
320
+
321
+
322
+ class BaseSerializer(ABC):
323
+ """Abstract base class for serializers."""
324
+
325
+ @abstractmethod
326
+ def serialize(self, obj: Any) -> bytes:
327
+ """Serialize an object to bytes."""
328
+ pass
329
+
330
+ @abstractmethod
331
+ def deserialize(self, data: bytes) -> Any:
332
+ """Deserialize bytes to an object."""
333
+ pass
334
+
335
+
336
+ class JSONSerializer(BaseSerializer):
337
+ """JSON serializer for common data types."""
338
+
339
+ def __init__(self):
340
+ self.logger = get_logger.get()
341
+
342
+ def serialize(self, obj: Any) -> bytes:
343
+ """
344
+ Serialize object to JSON bytes.
345
+
346
+ Args:
347
+ obj: Object to serialize
348
+
349
+ Returns:
350
+ UTF-8 encoded JSON bytes
351
+
352
+ Raises:
353
+ TypeError: If object is not JSON serializable
354
+ ValueError: If serialization fails
355
+ """
356
+ try:
357
+ return json.dumps(obj, ensure_ascii=False).encode('utf-8')
358
+ except (TypeError, ValueError) as e:
359
+ self.logger.error(f"JSON serialization error: {e}")
360
+ raise
361
+
362
+ def deserialize(self, data: bytes) -> Any:
363
+ """
364
+ Deserialize JSON bytes to object.
365
+
366
+ Args:
367
+ data: UTF-8 encoded JSON bytes
368
+
369
+ Returns:
370
+ Deserialized Python object
371
+
372
+ Raises:
373
+ json.JSONDecodeError: If data is not valid JSON
374
+ UnicodeDecodeError: If data is not valid UTF-8
375
+ """
376
+ if data is None:
377
+ return None
378
+
379
+ try:
380
+ if isinstance(data, bytes):
381
+ return json.loads(data.decode('utf-8'))
382
+ return json.loads(data)
383
+ except (json.JSONDecodeError, UnicodeDecodeError) as e:
384
+ self.logger.error(f"JSON deserialization error: {e}")
385
+ raise
386
+
387
+
388
+ # Usage example (for documentation purposes)
389
+ """
390
+ # Initialize cache
391
+ cache = RedisCache()
392
+
393
+ # 1. Basic operations
394
+ cache.set("user:123", {"name": "John", "age": 30}, ttl=3600)
395
+ user = cache.get("user:123")
396
+
397
+ # 2. Pattern matching with wildcards
398
+ pattern_dict = {
399
+ "environment": "dev",
400
+ "service": "*", # Match any service
401
+ "provider": "Cassandra",
402
+ "category": "provider",
403
+ "identifier": "*" # Match any identifier
404
+ }
405
+
406
+ # Get all matching keys and values
407
+ results = cache.get_by_dict(pattern_dict)
408
+ for key, value in results.items():
409
+ print(f"{key}: {value}")
410
+
411
+ # 3. Direct pattern usage
412
+ results = cache.get_by_pattern("*:Cassandra:*:file")
413
+
414
+ # 4. Get only keys (no values)
415
+ keys = cache.scan_keys("dev:api-*:*:provider:config")
416
+
417
+ # 5. Delete by pattern
418
+ deleted = cache.delete_by_pattern("temp:*:*")
419
+ print(f"Deleted {deleted} temporary keys")
420
+
421
+ # 6. Check operations
422
+ if cache.exists("user:123"):
423
+ ttl = cache.get_ttl("user:123")
424
+ print(f"Key expires in {ttl} seconds")
425
+ """
File without changes
@@ -0,0 +1,42 @@
1
+ import os
2
+
3
+ class ServiceConfig:
4
+ """Service configuration - set by application at startup"""
5
+ service_name: str = os.getenv("service_name", "IDP Service")
6
+ service_version: str = os.getenv("service_version", "1.0.0")
7
+ environment: str = os.getenv("service_environment", "Dev")
8
+ container_logs: bool = os.getenv("container_logs", True)
9
+ export_logs: bool = os.getenv("export_logs", True)
10
+
11
+ def __init__(self, **kwargs):
12
+ for key, value in kwargs.items():
13
+ setattr(self, key, value)
14
+
15
+
16
+ class LogConfig:
17
+ log_level: str = os.getenv("log_level", "INFO")
18
+ log_exporter_format: str = os.getenv("log_exporter_format", "otlp")
19
+ log_exporter_protocol: str = os.getenv("log_exporter_protocol", "http")
20
+ log_provider: str = os.getenv("log_provider", "otel")
21
+
22
+
23
+ class ExporterConfig:
24
+ exporter_mode: str = os.getenv("exporter_mode", "common")
25
+ log_endpoint: str = os.getenv("log_endpoint", "http://localhost:4318/v1/logs")
26
+ trace_endpoint: str = os.getenv("trace_endpoint", "http://localhost:4318/v1/traces")
27
+ metrics_endpoint: str = os.getenv("metrics_endpoint", "http://localhost:4318/v1/metrics")
28
+
29
+ class TraceConfig:
30
+ trace_provider: str = os.getenv("trace_provider", "otel")
31
+ trace_exporter_format: str = os.getenv("trace_exporter_format", "otlp")
32
+ trace_exporter_protocol: str = os.getenv("trace_exporter_protocol", "http")
33
+
34
+
35
+ class MetricsConfig:
36
+ metrics_provider: str = os.getenv("metrics_provider", "otel")
37
+ metrics_exporter_format: str = os.getenv("metrics_exporter_format", "otlp")
38
+ metrics_exporter_protocol: str = os.getenv("metrics_exporter_protocol", "http")
39
+ metrics_export_interval_millis: int = int(
40
+ os.getenv("metrics_export_interval_millis", "60000")) # 60 seconds default
41
+ environment: str = os.getenv("environment", "Dev")
42
+ max_workers: int = int(os.getenv("max_metrics_worker", 4))
@@ -0,0 +1,8 @@
1
+ from ..utils.singleton import singleton
2
+ from ..config.data_config import ServiceConfig
3
+
4
+ @singleton
5
+ def get_config(config_data, provider="service"):
6
+ if provider == "service":
7
+ return ServiceConfig(**config_data)
8
+ return None
@@ -0,0 +1,11 @@
1
+ import json
2
+ import os
3
+ from ..utils.string_utils import to_snake_case
4
+
5
+ def load_json(path):
6
+ with open(path) as f:
7
+ return json.load(f)
8
+
9
+ def load_env(prefix="SDK_"):
10
+ prefix = to_snake_case(prefix)
11
+ return {f"{k[len(prefix):]}".lower(): v for k, v in os.environ.items() if k.startswith(prefix.upper())}
@@ -0,0 +1,5 @@
1
+ import os
2
+
3
+ class Settings:
4
+ ENV = os.getenv("SDK_ENV", "dev")
5
+ DEBUG = os.getenv("SDK_DEBUG", "false").lower() == "true"
File without changes