mpratyush54-sdk 0.6.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.
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: mpratyush54-sdk
3
+ Version: 0.6.0
4
+ Summary: Platform Python SDK
5
+ Home-page: https://github.com/Mpratyush54/server-automation
6
+ Author: Platform Team
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: psycopg2-binary>=2.9.0
13
+ Requires-Dist: pymongo>=4.5.0
14
+ Requires-Dist: redis>=5.0.0
15
+ Dynamic: author
16
+ Dynamic: classifier
17
+ Dynamic: description
18
+ Dynamic: description-content-type
19
+ Dynamic: home-page
20
+ Dynamic: requires-dist
21
+ Dynamic: requires-python
22
+ Dynamic: summary
23
+
24
+ # mpratyush54-sdk
25
+
26
+ The official Python SDK for the Platform.
27
+
28
+ This SDK provides core backend integrations for your Python microservices, including auto-registration with the platform, metrics aggregation, structured logging, and standardized database connections.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install mpratyush54-sdk
34
+ ```
35
+
36
+ ## Features
37
+
38
+ - **Service Registration:** Automatically registers your microservice with the Platform API on startup.
39
+ - **Metrics Aggregation:** Utility decorators and middleware to record route metrics, execution times, and memory deltas.
40
+ - **Database Connections:** Pre-configured Psycopg2 (PostgreSQL), PyMongo (MongoDB), and Redis connection utilities.
41
+ - **Logging:** Structured logging that automatically ships to the platform.
42
+
43
+ ## Usage
44
+
45
+ ```python
46
+ from platform_sdk import PlatformSDK
47
+
48
+ sdk = PlatformSDK(
49
+ project_id="your-project-id",
50
+ environment="production",
51
+ api_url="https://api.your-platform.com"
52
+ )
53
+
54
+ # Initialize connections and register service
55
+ sdk.initialize()
56
+
57
+ # Use database connections safely
58
+ db = sdk.connections.get_postgres()
59
+ cursor = db.cursor()
60
+ cursor.execute("SELECT version();")
61
+ print(cursor.fetchone())
62
+ ```
@@ -0,0 +1,39 @@
1
+ # mpratyush54-sdk
2
+
3
+ The official Python SDK for the Platform.
4
+
5
+ This SDK provides core backend integrations for your Python microservices, including auto-registration with the platform, metrics aggregation, structured logging, and standardized database connections.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install mpratyush54-sdk
11
+ ```
12
+
13
+ ## Features
14
+
15
+ - **Service Registration:** Automatically registers your microservice with the Platform API on startup.
16
+ - **Metrics Aggregation:** Utility decorators and middleware to record route metrics, execution times, and memory deltas.
17
+ - **Database Connections:** Pre-configured Psycopg2 (PostgreSQL), PyMongo (MongoDB), and Redis connection utilities.
18
+ - **Logging:** Structured logging that automatically ships to the platform.
19
+
20
+ ## Usage
21
+
22
+ ```python
23
+ from platform_sdk import PlatformSDK
24
+
25
+ sdk = PlatformSDK(
26
+ project_id="your-project-id",
27
+ environment="production",
28
+ api_url="https://api.your-platform.com"
29
+ )
30
+
31
+ # Initialize connections and register service
32
+ sdk.initialize()
33
+
34
+ # Use database connections safely
35
+ db = sdk.connections.get_postgres()
36
+ cursor = db.cursor()
37
+ cursor.execute("SELECT version();")
38
+ print(cursor.fetchone())
39
+ ```
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: mpratyush54-sdk
3
+ Version: 0.6.0
4
+ Summary: Platform Python SDK
5
+ Home-page: https://github.com/Mpratyush54/server-automation
6
+ Author: Platform Team
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: psycopg2-binary>=2.9.0
13
+ Requires-Dist: pymongo>=4.5.0
14
+ Requires-Dist: redis>=5.0.0
15
+ Dynamic: author
16
+ Dynamic: classifier
17
+ Dynamic: description
18
+ Dynamic: description-content-type
19
+ Dynamic: home-page
20
+ Dynamic: requires-dist
21
+ Dynamic: requires-python
22
+ Dynamic: summary
23
+
24
+ # mpratyush54-sdk
25
+
26
+ The official Python SDK for the Platform.
27
+
28
+ This SDK provides core backend integrations for your Python microservices, including auto-registration with the platform, metrics aggregation, structured logging, and standardized database connections.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install mpratyush54-sdk
34
+ ```
35
+
36
+ ## Features
37
+
38
+ - **Service Registration:** Automatically registers your microservice with the Platform API on startup.
39
+ - **Metrics Aggregation:** Utility decorators and middleware to record route metrics, execution times, and memory deltas.
40
+ - **Database Connections:** Pre-configured Psycopg2 (PostgreSQL), PyMongo (MongoDB), and Redis connection utilities.
41
+ - **Logging:** Structured logging that automatically ships to the platform.
42
+
43
+ ## Usage
44
+
45
+ ```python
46
+ from platform_sdk import PlatformSDK
47
+
48
+ sdk = PlatformSDK(
49
+ project_id="your-project-id",
50
+ environment="production",
51
+ api_url="https://api.your-platform.com"
52
+ )
53
+
54
+ # Initialize connections and register service
55
+ sdk.initialize()
56
+
57
+ # Use database connections safely
58
+ db = sdk.connections.get_postgres()
59
+ cursor = db.cursor()
60
+ cursor.execute("SELECT version();")
61
+ print(cursor.fetchone())
62
+ ```
@@ -0,0 +1,17 @@
1
+ README.md
2
+ setup.py
3
+ mpratyush54_sdk.egg-info/PKG-INFO
4
+ mpratyush54_sdk.egg-info/SOURCES.txt
5
+ mpratyush54_sdk.egg-info/dependency_links.txt
6
+ mpratyush54_sdk.egg-info/requires.txt
7
+ mpratyush54_sdk.egg-info/top_level.txt
8
+ platform_sdk/__init__.py
9
+ platform_sdk/db/__init__.py
10
+ platform_sdk/db/mongo.py
11
+ platform_sdk/db/postgres.py
12
+ platform_sdk/db/redis.py
13
+ tests/test_caps_client.py
14
+ tests/test_integration.py
15
+ tests/test_mongo.py
16
+ tests/test_postgres.py
17
+ tests/test_redis.py
@@ -0,0 +1,3 @@
1
+ psycopg2-binary>=2.9.0
2
+ pymongo>=4.5.0
3
+ redis>=5.0.0
@@ -0,0 +1 @@
1
+ platform_sdk
@@ -0,0 +1,170 @@
1
+ """
2
+ Platform Python SDK
3
+ Auto-registration, metrics, logging, config, storage, DB connections
4
+ """
5
+ import os
6
+ import time
7
+ import json
8
+ import logging
9
+ import threading
10
+ from typing import Optional, List, Dict, Any
11
+ from urllib.request import Request, urlopen
12
+ from urllib.error import URLError
13
+
14
+ logger = logging.getLogger('platform')
15
+
16
+
17
+ class PlatformClient:
18
+ def __init__(self):
19
+ self.http = None
20
+ self.options = {}
21
+ self.initialized = False
22
+ self._heartbeat_timer = None
23
+ self._db_connections = {}
24
+
25
+ def init(
26
+ self,
27
+ project_name: str,
28
+ platform_url: str,
29
+ environment_name: str = "development",
30
+ version: str = "1.0.0",
31
+ branch: str = "main",
32
+ commit_sha: Optional[str] = None,
33
+ namespace: Optional[str] = None,
34
+ hostname: Optional[str] = None,
35
+ databases: Optional[List[str]] = None,
36
+ ):
37
+ self.options = {
38
+ "project_name": project_name,
39
+ "platform_url": platform_url.rstrip("/"),
40
+ "environment_name": environment_name,
41
+ "version": version,
42
+ "branch": branch,
43
+ "commit_sha": commit_sha,
44
+ "namespace": namespace,
45
+ "hostname": hostname or os.uname().nodename,
46
+ "databases": databases or [],
47
+ }
48
+
49
+ self._register()
50
+ self._start_heartbeat()
51
+
52
+ if "postgres" in self.options["databases"]:
53
+ from .db.postgres import PostgresManager
54
+ self._db_connections["postgres"] = PostgresManager()
55
+ try:
56
+ self._db_connections["postgres"].connect()
57
+ except Exception as e:
58
+ logger.warning(f"PostgreSQL connection failed (non-blocking): {e}")
59
+
60
+ if "mongo" in self.options["databases"]:
61
+ from .db.mongo import MongoManager
62
+ self._db_connections["mongo"] = MongoManager()
63
+ try:
64
+ self._db_connections["mongo"].connect()
65
+ except Exception as e:
66
+ logger.warning(f"MongoDB connection failed (non-blocking): {e}")
67
+
68
+ if "redis" in self.options["databases"]:
69
+ from .db.redis import RedisManager
70
+ self._db_connections["redis"] = RedisManager()
71
+ try:
72
+ self._db_connections["redis"].connect()
73
+ except Exception as e:
74
+ logger.warning(f"Redis connection failed (non-blocking): {e}")
75
+
76
+ self.initialized = True
77
+
78
+ def _request(self, method: str, path: str, data: Optional[Dict] = None,
79
+ params: Optional[Dict] = None) -> Optional[Dict]:
80
+ url = f"{self.options['platform_url']}{path}"
81
+ if params:
82
+ qs = "&".join(f"{k}={v}" for k, v in params.items())
83
+ url = f"{url}?{qs}"
84
+
85
+ headers = {"Content-Type": "application/json"}
86
+ body = json.dumps(data).encode() if data else None
87
+
88
+ try:
89
+ req = Request(url, data=body, headers=headers, method=method)
90
+ with urlopen(req, timeout=5) as resp:
91
+ return json.loads(resp.read().decode())
92
+ except URLError as e:
93
+ logger.warning(f"[platform] Request failed (silent): {e}")
94
+ return None
95
+ except Exception as e:
96
+ logger.warning(f"[platform] Request error (silent): {e}")
97
+ return None
98
+
99
+ def _register(self):
100
+ return self._request("POST", "/api/sdk/register", {
101
+ "projectName": self.options["project_name"],
102
+ "environmentName": self.options["environment_name"],
103
+ "serviceName": self.options["project_name"],
104
+ "version": self.options["version"],
105
+ "branch": self.options["branch"],
106
+ "commitSha": self.options["commit_sha"],
107
+ "namespace": self.options["namespace"],
108
+ "hostname": self.options["hostname"],
109
+ "dbTypes": self.options["databases"],
110
+ })
111
+
112
+ def _start_heartbeat(self):
113
+ def heartbeat():
114
+ while True:
115
+ time.sleep(15)
116
+ self._request("POST", "/api/sdk/heartbeat", {
117
+ "projectId": self.options["project_name"],
118
+ "serviceName": self.options["project_name"],
119
+ "timestamp": time.time(),
120
+ })
121
+
122
+ t = threading.Thread(target=heartbeat, daemon=True)
123
+ t.start()
124
+
125
+ # --- Config ---
126
+ def config(self, key: str) -> Optional[Any]:
127
+ data = self._request("GET", "/api/config", params={
128
+ "projectId": self.options["project_name"],
129
+ })
130
+ if data and key in data:
131
+ return data[key]
132
+ return None
133
+
134
+ # --- Storage ---
135
+ def storage_upload(self, file_path: str, bucket: str):
136
+ import os as _os
137
+ size = _os.path.getsize(file_path)
138
+ return self._request("POST", "/api/storage/upload-url", {
139
+ "projectId": self.options["project_name"],
140
+ "bucket": bucket,
141
+ "originalName": _os.path.basename(file_path),
142
+ "mimeType": "application/octet-stream",
143
+ "size": size,
144
+ })
145
+
146
+ # --- Logging ---
147
+ def log(self, level: str, message: str, metadata: Optional[Dict] = None):
148
+ self._request("POST", "/api/logs/ingest", {
149
+ "projectId": self.options["project_name"],
150
+ "environmentId": self.options["environment_name"],
151
+ "serviceName": self.options["project_name"],
152
+ "level": level,
153
+ "message": message,
154
+ "metadata": metadata or {},
155
+ })
156
+
157
+ # --- DB Access ---
158
+ @property
159
+ def db(self):
160
+ return self._db_connections
161
+
162
+ def shutdown(self):
163
+ for name, conn in self._db_connections.items():
164
+ try:
165
+ conn.disconnect()
166
+ except Exception:
167
+ pass
168
+
169
+
170
+ platform = PlatformClient()
@@ -0,0 +1,5 @@
1
+ from .postgres import PostgresManager
2
+ from .mongo import MongoManager
3
+ from .redis import RedisManager
4
+
5
+ __all__ = ["PostgresManager", "MongoManager", "RedisManager"]
@@ -0,0 +1,32 @@
1
+ import pymongo
2
+ import logging
3
+
4
+ logger = logging.getLogger('platform')
5
+
6
+
7
+ class MongoManager:
8
+ def __init__(self, config: dict = None):
9
+ import os as _os
10
+ self.uri = (config or {}).get("uri") or _os.environ.get(
11
+ "PLATFORM_MONGO_URI", "mongodb://localhost:27017/platform"
12
+ )
13
+ self.client = None
14
+ self.connected = False
15
+
16
+ def connect(self):
17
+ self.client = pymongo.MongoClient(self.uri, serverSelectionTimeoutMS=5000)
18
+ self.client.admin.command("ping")
19
+ self.connected = True
20
+ logger.info("[platform] MongoDB connected")
21
+
22
+ @property
23
+ def db(self):
24
+ if self.client:
25
+ return self.client.get_default_database()
26
+ return None
27
+
28
+ def disconnect(self):
29
+ if self.client:
30
+ self.client.close()
31
+ self.connected = False
32
+ logger.info("[platform] MongoDB disconnected")
@@ -0,0 +1,49 @@
1
+ import psycopg2
2
+ from psycopg2 import pool
3
+ import logging
4
+
5
+ logger = logging.getLogger('platform')
6
+
7
+
8
+ class PostgresManager:
9
+ def __init__(self, config: dict = None):
10
+ self.config = config or {
11
+ "host": os.environ.get("PLATFORM_PG_HOST", "localhost"),
12
+ "port": int(os.environ.get("PLATFORM_PG_PORT", "5432")),
13
+ "user": os.environ.get("PLATFORM_PG_USER", "platform"),
14
+ "password": os.environ.get("PLATFORM_PG_PASSWORD", "platform"),
15
+ "database": os.environ.get("PLATFORM_PG_DB", "platform"),
16
+ }
17
+ import os as _os
18
+ self._pool = None
19
+ self.connected = False
20
+
21
+ def connect(self):
22
+ self._pool = pool.ThreadedConnectionPool(
23
+ minconn=1, maxconn=self.config.get("pool_size", 10), **self.config
24
+ )
25
+ conn = self._pool.getconn()
26
+ conn.close()
27
+ self._pool.putconn(conn)
28
+ self.connected = True
29
+ logger.info("[platform] PostgreSQL connected")
30
+
31
+ def execute(self, query: str, params: tuple = None) -> list:
32
+ if not self._pool:
33
+ raise RuntimeError("PostgreSQL not connected")
34
+ conn = self._pool.getconn()
35
+ try:
36
+ with conn.cursor() as cur:
37
+ cur.execute(query, params)
38
+ if cur.description:
39
+ return cur.fetchall()
40
+ conn.commit()
41
+ return []
42
+ finally:
43
+ self._pool.putconn(conn)
44
+
45
+ def disconnect(self):
46
+ if self._pool:
47
+ self._pool.closeall()
48
+ self.connected = False
49
+ logger.info("[platform] PostgreSQL disconnected")
@@ -0,0 +1,40 @@
1
+ import redis
2
+ import logging
3
+
4
+ logger = logging.getLogger('platform')
5
+
6
+
7
+ class RedisManager:
8
+ def __init__(self, config: dict = None):
9
+ import os as _os
10
+ self.config = config or {
11
+ "host": _os.environ.get("PLATFORM_REDIS_HOST", "localhost"),
12
+ "port": int(_os.environ.get("PLATFORM_REDIS_PORT", "6379")),
13
+ "password": _os.environ.get("PLATFORM_REDIS_PASSWORD", None),
14
+ }
15
+ self.client = None
16
+ self.connected = False
17
+
18
+ def connect(self):
19
+ self.client = redis.Redis(**self.config, decode_responses=True)
20
+ self.client.ping()
21
+ self.connected = True
22
+ logger.info("[platform] Redis connected")
23
+
24
+ def get(self, key: str) -> str:
25
+ if self.client:
26
+ return self.client.get(key)
27
+ return None
28
+
29
+ def set(self, key: str, value: str, ttl: int = None):
30
+ if self.client:
31
+ if ttl:
32
+ self.client.setex(key, ttl, value)
33
+ else:
34
+ self.client.set(key, value)
35
+
36
+ def disconnect(self):
37
+ if self.client:
38
+ self.client.close()
39
+ self.connected = False
40
+ logger.info("[platform] Redis disconnected")
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,24 @@
1
+ import os
2
+ from setuptools import setup, find_packages
3
+
4
+ setup(
5
+ name="mpratyush54-sdk",
6
+ version="0.6.0",
7
+ description="Platform Python SDK",
8
+ packages=find_packages(include=['platform_sdk', 'platform_sdk.*']),
9
+ install_requires=[
10
+ "psycopg2-binary>=2.9.0",
11
+ "pymongo>=4.5.0",
12
+ "redis>=5.0.0",
13
+ ],
14
+ author="Platform Team",
15
+ url="https://github.com/Mpratyush54/server-automation",
16
+ long_description=open("README.md").read() if os.path.exists("README.md") else "",
17
+ long_description_content_type="text/markdown",
18
+ python_requires=">=3.10",
19
+ classifiers=[
20
+ "Programming Language :: Python :: 3",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Operating System :: OS Independent",
23
+ ],
24
+ )
@@ -0,0 +1,170 @@
1
+ import pytest
2
+ from unittest.mock import patch, MagicMock, mock_open
3
+ import json
4
+ import sys
5
+ import os
6
+
7
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
8
+
9
+ from platform_sdk import PlatformClient
10
+
11
+
12
+ class TestPlatformClient:
13
+ def setup_method(self):
14
+ self.client = PlatformClient()
15
+
16
+ def test_init_defaults(self):
17
+ assert self.client.initialized is False
18
+ assert self.client.options == {}
19
+ assert self.client._db_connections == {}
20
+
21
+ @patch('platform_sdk.PlatformClient._request')
22
+ @patch('platform_sdk.PlatformClient._start_heartbeat')
23
+ def test_init_sets_options(self, mock_heartbeat, mock_request):
24
+ mock_request.return_value = {"id": "reg-1"}
25
+
26
+ self.client.init(
27
+ project_name="test-project",
28
+ platform_url="http://localhost:3000",
29
+ )
30
+
31
+ assert self.client.initialized is True
32
+ assert self.client.options["project_name"] == "test-project"
33
+ assert self.client.options["platform_url"] == "http://localhost:3000"
34
+ assert self.client.options["environment_name"] == "development"
35
+ assert self.client.options["version"] == "1.0.0"
36
+ assert self.client.options["branch"] == "main"
37
+
38
+ @patch('platform_sdk.PlatformClient._request')
39
+ @patch('platform_sdk.PlatformClient._start_heartbeat')
40
+ def test_init_custom_options(self, mock_heartbeat, mock_request):
41
+ mock_request.return_value = {}
42
+
43
+ self.client.init(
44
+ project_name="my-app",
45
+ platform_url="https://platform.example.com/",
46
+ environment_name="production",
47
+ version="2.0.0",
48
+ branch="develop",
49
+ )
50
+
51
+ assert self.client.options["environment_name"] == "production"
52
+ assert self.client.options["version"] == "2.0.0"
53
+ assert self.client.options["branch"] == "develop"
54
+ assert self.client.options["platform_url"] == "https://platform.example.com"
55
+
56
+ @patch('platform_sdk.PlatformClient._request')
57
+ @patch('platform_sdk.PlatformClient._start_heartbeat')
58
+ def test_init_calls_register(self, mock_heartbeat, mock_request):
59
+ mock_request.return_value = {"id": "reg-1"}
60
+
61
+ self.client.init(
62
+ project_name="test-project",
63
+ platform_url="http://localhost:3000",
64
+ )
65
+
66
+ mock_request.assert_called_with("POST", "/api/sdk/register", {
67
+ "projectName": "test-project",
68
+ "environmentName": "development",
69
+ "serviceName": "test-project",
70
+ "version": "1.0.0",
71
+ "branch": "main",
72
+ "commitSha": None,
73
+ "namespace": None,
74
+ "hostname": self.client.options["hostname"],
75
+ "dbTypes": [],
76
+ })
77
+
78
+ def test_request_returns_response(self):
79
+ with patch('platform_sdk.urlopen') as mock_urlopen:
80
+ mock_response = MagicMock()
81
+ mock_response.read.return_value = json.dumps({"key": "value"}).encode()
82
+ mock_urlopen.return_value.__enter__ = lambda s: mock_response
83
+ mock_urlopen.return_value.__exit__ = MagicMock(return_value=False)
84
+
85
+ self.client.options = {"platform_url": "http://localhost:3000"}
86
+ result = self.client._request("GET", "/api/test")
87
+ assert result == {"key": "value"}
88
+
89
+ def test_request_returns_none_on_error(self):
90
+ from urllib.error import URLError
91
+ with patch('platform_sdk.urlopen') as mock_urlopen:
92
+ mock_urlopen.side_effect = URLError("Connection refused")
93
+
94
+ self.client.options = {"platform_url": "http://localhost:3000"}
95
+ result = self.client._request("GET", "/api/test")
96
+ assert result is None
97
+
98
+ def test_config_returns_value(self):
99
+ self.client.options = {"project_name": "test"}
100
+ with patch.object(self.client, '_request', return_value={"KEY": "val"}):
101
+ assert self.client.config("KEY") == "val"
102
+
103
+ def test_config_returns_none_for_missing_key(self):
104
+ self.client.options = {"project_name": "test"}
105
+ with patch.object(self.client, '_request', return_value={"KEY": "val"}):
106
+ assert self.client.config("MISSING") is None
107
+
108
+ def test_log_calls_request(self):
109
+ self.client.options = {
110
+ "project_name": "test",
111
+ "environment_name": "dev",
112
+ }
113
+ with patch.object(self.client, '_request') as mock_request:
114
+ self.client.log("INFO", "Test message", {"extra": "data"})
115
+ mock_request.assert_called_once_with("POST", "/api/logs/ingest", {
116
+ "projectId": "test",
117
+ "environmentId": "dev",
118
+ "serviceName": "test",
119
+ "level": "INFO",
120
+ "message": "Test message",
121
+ "metadata": {"extra": "data"},
122
+ })
123
+
124
+ def test_log_without_metadata(self):
125
+ self.client.options = {
126
+ "project_name": "test",
127
+ "environment_name": "dev",
128
+ }
129
+ with patch.object(self.client, '_request') as mock_request:
130
+ self.client.log("ERROR", "Error occurred")
131
+ mock_request.assert_called_once_with("POST", "/api/logs/ingest", {
132
+ "projectId": "test",
133
+ "environmentId": "dev",
134
+ "serviceName": "test",
135
+ "level": "ERROR",
136
+ "message": "Error occurred",
137
+ "metadata": {},
138
+ })
139
+
140
+ def test_db_property(self):
141
+ assert self.client.db == {}
142
+
143
+ def test_shutdown_disconnects_all_dbs(self):
144
+ mock_pg = MagicMock()
145
+ mock_mongo = MagicMock()
146
+ mock_redis = MagicMock()
147
+ self.client._db_connections = {
148
+ "postgres": mock_pg,
149
+ "mongo": mock_mongo,
150
+ "redis": mock_redis,
151
+ }
152
+
153
+ self.client.shutdown()
154
+
155
+ mock_pg.disconnect.assert_called_once()
156
+ mock_mongo.disconnect.assert_called_once()
157
+ mock_redis.disconnect.assert_called_once()
158
+
159
+ def test_shutdown_handles_disconnect_errors(self):
160
+ mock_db = MagicMock()
161
+ mock_db.disconnect.side_effect = Exception("Disconnect failed")
162
+ self.client._db_connections = {"postgres": mock_db}
163
+
164
+ self.client.shutdown() # Should not raise
165
+
166
+
167
+ class TestPlatformClientSingleton:
168
+ def test_singleton_exists(self):
169
+ from platform_sdk import platform
170
+ assert isinstance(platform, PlatformClient)
@@ -0,0 +1,83 @@
1
+ import pytest
2
+ from unittest.mock import patch, MagicMock
3
+ import sys
4
+ import os
5
+
6
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
7
+
8
+ from platform_sdk import PlatformClient
9
+
10
+
11
+ class TestPlatformClientIntegration:
12
+ """Integration-style tests that test multiple components together."""
13
+
14
+ @patch('platform_sdk.PlatformClient._start_heartbeat')
15
+ @patch('platform_sdk.PlatformClient._request')
16
+ def test_full_init_and_shutdown_flow(self, mock_request, mock_heartbeat):
17
+ mock_request.return_value = {"id": "reg-1", "projectName": "test"}
18
+
19
+ client = PlatformClient()
20
+ client.init(
21
+ project_name="integration-test",
22
+ platform_url="http://localhost:3000",
23
+ environment_name="staging",
24
+ version="1.0.0",
25
+ )
26
+
27
+ assert client.initialized is True
28
+ assert client.options["project_name"] == "integration-test"
29
+ assert client.options["environment_name"] == "staging"
30
+
31
+ # Verify registration was called
32
+ mock_request.assert_called_with("POST", "/api/sdk/register", {
33
+ "projectName": "integration-test",
34
+ "environmentName": "staging",
35
+ "serviceName": "integration-test",
36
+ "version": "1.0.0",
37
+ "branch": "main",
38
+ "commitSha": None,
39
+ "namespace": None,
40
+ "hostname": client.options["hostname"],
41
+ "dbTypes": [],
42
+ })
43
+
44
+ client.shutdown()
45
+
46
+ @patch('platform_sdk.PlatformClient._start_heartbeat')
47
+ @patch('platform_sdk.PlatformClient._request')
48
+ def test_config_after_init(self, mock_request, mock_heartbeat):
49
+ mock_request.return_value = {}
50
+
51
+ client = PlatformClient()
52
+ client.init(
53
+ project_name="test",
54
+ platform_url="http://localhost:3000",
55
+ )
56
+
57
+ # Mock config endpoint
58
+ with patch.object(client, '_request') as mock_config:
59
+ mock_config.return_value = {"DB_HOST": "localhost", "FEATURE_X": "true"}
60
+ result = client.config("DB_HOST")
61
+ assert result == "localhost"
62
+
63
+ @patch('platform_sdk.PlatformClient._start_heartbeat')
64
+ @patch('platform_sdk.PlatformClient._request')
65
+ def test_logging_after_init(self, mock_request, mock_heartbeat):
66
+ mock_request.return_value = {}
67
+
68
+ client = PlatformClient()
69
+ client.init(
70
+ project_name="test",
71
+ platform_url="http://localhost:3000",
72
+ )
73
+
74
+ with patch.object(client, '_request') as mock_log:
75
+ client.log("INFO", "Service started")
76
+ mock_log.assert_called_with("POST", "/api/logs/ingest", {
77
+ "projectId": "test",
78
+ "environmentId": "development",
79
+ "serviceName": "test",
80
+ "level": "INFO",
81
+ "message": "Service started",
82
+ "metadata": {},
83
+ })
@@ -0,0 +1,75 @@
1
+ import pytest
2
+ from unittest.mock import patch, MagicMock
3
+ import sys
4
+ import os
5
+
6
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
7
+
8
+ from platform_sdk.db.mongo import MongoManager
9
+
10
+
11
+ class TestMongoManager:
12
+ def test_init_with_config(self):
13
+ manager = MongoManager({"uri": "mongodb://localhost:27017/test"})
14
+ assert manager.uri == "mongodb://localhost:27017/test"
15
+ assert manager.connected is False
16
+ assert manager.client is None
17
+
18
+ def test_init_without_config(self):
19
+ manager = MongoManager()
20
+ assert manager.uri == "mongodb://localhost:27017/platform"
21
+
22
+ @patch('platform_sdk.db.mongo.pymongo.MongoClient')
23
+ def test_connect_success(self, mock_client_class):
24
+ mock_client = MagicMock()
25
+ mock_client_class.return_value = mock_client
26
+
27
+ manager = MongoManager({"uri": "mongodb://localhost:27017/test"})
28
+ manager.connect()
29
+
30
+ assert manager.connected is True
31
+ mock_client.admin.command.assert_called_once_with("ping")
32
+
33
+ @patch('platform_sdk.db.mongo.pymongo.MongoClient')
34
+ def test_connect_failure(self, mock_client_class):
35
+ mock_client = MagicMock()
36
+ mock_client.admin.command.side_effect = Exception("Connection refused")
37
+ mock_client_class.return_value = mock_client
38
+
39
+ manager = MongoManager({"uri": "mongodb://localhost:27017/test"})
40
+ with pytest.raises(Exception, match="Connection refused"):
41
+ manager.connect()
42
+
43
+ assert manager.connected is False
44
+
45
+ def test_db_property_when_not_connected(self):
46
+ manager = MongoManager()
47
+ assert manager.db is None
48
+
49
+ @patch('platform_sdk.db.mongo.pymongo.MongoClient')
50
+ def test_db_property_when_connected(self, mock_client_class):
51
+ mock_client = MagicMock()
52
+ mock_db = MagicMock()
53
+ mock_client.get_default_database.return_value = mock_db
54
+ mock_client_class.return_value = mock_client
55
+
56
+ manager = MongoManager({"uri": "mongodb://localhost:27017/test"})
57
+ manager.connect()
58
+
59
+ assert manager.db == mock_db
60
+
61
+ @patch('platform_sdk.db.mongo.pymongo.MongoClient')
62
+ def test_disconnect(self, mock_client_class):
63
+ mock_client = MagicMock()
64
+ mock_client_class.return_value = mock_client
65
+
66
+ manager = MongoManager({"uri": "mongodb://localhost:27017/test"})
67
+ manager.connect()
68
+ manager.disconnect()
69
+
70
+ assert manager.connected is False
71
+ mock_client.close.assert_called_once()
72
+
73
+ def test_disconnect_when_not_connected(self):
74
+ manager = MongoManager()
75
+ manager.disconnect() # Should not raise
@@ -0,0 +1,114 @@
1
+ import pytest
2
+ from unittest.mock import patch, MagicMock
3
+ import sys
4
+ import os
5
+
6
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
7
+
8
+ from platform_sdk.db.postgres import PostgresManager
9
+
10
+
11
+ class TestPostgresManager:
12
+ def test_init_with_config(self):
13
+ config = {
14
+ "host": "localhost",
15
+ "port": 5432,
16
+ "user": "test",
17
+ "password": "test",
18
+ "database": "test_db",
19
+ }
20
+ manager = PostgresManager(config)
21
+ assert manager.config == config
22
+ assert manager.connected is False
23
+
24
+ def test_init_with_empty_config(self):
25
+ manager = PostgresManager({})
26
+ assert manager.connected is False
27
+
28
+ @patch('platform_sdk.db.postgres.pool.ThreadedConnectionPool')
29
+ def test_connect_success(self, mock_pool_class):
30
+ mock_pool = MagicMock()
31
+ mock_conn = MagicMock()
32
+ mock_pool.getconn.return_value = mock_conn
33
+ mock_pool_class.return_value = mock_pool
34
+
35
+ manager = PostgresManager({"host": "localhost", "port": 5432})
36
+ manager.connect()
37
+
38
+ assert manager.connected is True
39
+ mock_pool.getconn.assert_called_once()
40
+ mock_conn.close.assert_called_once()
41
+ mock_pool.putconn.assert_called_once_with(mock_conn)
42
+
43
+ @patch('platform_sdk.db.postgres.pool.ThreadedConnectionPool')
44
+ def test_connect_failure(self, mock_pool_class):
45
+ mock_pool_class.side_effect = Exception("Connection refused")
46
+
47
+ manager = PostgresManager({"host": "localhost"})
48
+ with pytest.raises(Exception, match="Connection refused"):
49
+ manager.connect()
50
+
51
+ assert manager.connected is False
52
+
53
+ def test_execute_without_connection(self):
54
+ manager = PostgresManager({})
55
+ with pytest.raises(RuntimeError, match="PostgreSQL not connected"):
56
+ manager.execute("SELECT 1")
57
+
58
+ @patch('platform_sdk.db.postgres.pool.ThreadedConnectionPool')
59
+ def test_execute_query(self, mock_pool_class):
60
+ mock_pool = MagicMock()
61
+ mock_conn = MagicMock()
62
+ mock_cursor = MagicMock()
63
+ mock_cursor.__enter__ = MagicMock(return_value=mock_cursor)
64
+ mock_cursor.__exit__ = MagicMock(return_value=False)
65
+ mock_cursor.description = [("id",)]
66
+ mock_cursor.fetchall.return_value = [(1,), (2,)]
67
+ mock_conn.cursor.return_value = mock_cursor
68
+ mock_pool.getconn.return_value = mock_conn
69
+ mock_pool_class.return_value = mock_pool
70
+
71
+ manager = PostgresManager({})
72
+ manager._pool = mock_pool
73
+ manager.connected = True
74
+
75
+ result = manager.execute("SELECT id FROM users")
76
+ assert result == [(1,), (2,)]
77
+
78
+ @patch('platform_sdk.db.postgres.pool.ThreadedConnectionPool')
79
+ def test_execute_mutation(self, mock_pool_class):
80
+ mock_pool = MagicMock()
81
+ mock_conn = MagicMock()
82
+ mock_cursor = MagicMock()
83
+ mock_cursor.__enter__ = MagicMock(return_value=mock_cursor)
84
+ mock_cursor.__exit__ = MagicMock(return_value=False)
85
+ mock_cursor.description = None
86
+ mock_conn.cursor.return_value = mock_cursor
87
+ mock_pool.getconn.return_value = mock_conn
88
+ mock_pool_class.return_value = mock_pool
89
+
90
+ manager = PostgresManager({})
91
+ manager._pool = mock_pool
92
+ manager.connected = True
93
+
94
+ result = manager.execute("INSERT INTO users (name) VALUES (%s)", ("test",))
95
+ assert result == []
96
+ mock_conn.commit.assert_called_once()
97
+
98
+ @patch('platform_sdk.db.postgres.pool.ThreadedConnectionPool')
99
+ def test_disconnect(self, mock_pool_class):
100
+ mock_pool = MagicMock()
101
+ mock_pool_class.return_value = mock_pool
102
+
103
+ manager = PostgresManager({})
104
+ manager._pool = mock_pool
105
+ manager.connected = True
106
+
107
+ manager.disconnect()
108
+
109
+ assert manager.connected is False
110
+ mock_pool.closeall.assert_called_once()
111
+
112
+ def test_disconnect_when_not_connected(self):
113
+ manager = PostgresManager({})
114
+ manager.disconnect() # Should not raise
@@ -0,0 +1,102 @@
1
+ import pytest
2
+ from unittest.mock import patch, MagicMock
3
+ import sys
4
+ import os
5
+
6
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
7
+
8
+ from platform_sdk.db.redis import RedisManager
9
+
10
+
11
+ class TestRedisManager:
12
+ def test_init_with_config(self):
13
+ config = {"host": "localhost", "port": 6379, "password": None}
14
+ manager = RedisManager(config)
15
+ assert manager.config == config
16
+ assert manager.connected is False
17
+
18
+ def test_init_without_config(self):
19
+ manager = RedisManager()
20
+ assert manager.connected is False
21
+
22
+ @patch('platform_sdk.db.redis.redis.Redis')
23
+ def test_connect_success(self, mock_redis_class):
24
+ mock_client = MagicMock()
25
+ mock_redis_class.return_value = mock_client
26
+
27
+ manager = RedisManager({"host": "localhost", "port": 6379})
28
+ manager.connect()
29
+
30
+ assert manager.connected is True
31
+ mock_client.ping.assert_called_once()
32
+
33
+ @patch('platform_sdk.db.redis.redis.Redis')
34
+ def test_connect_failure(self, mock_redis_class):
35
+ mock_client = MagicMock()
36
+ mock_client.ping.side_effect = Exception("Connection refused")
37
+ mock_redis_class.return_value = mock_client
38
+
39
+ manager = RedisManager({"host": "localhost"})
40
+ with pytest.raises(Exception, match="Connection refused"):
41
+ manager.connect()
42
+
43
+ assert manager.connected is False
44
+
45
+ def test_get_when_not_connected(self):
46
+ manager = RedisManager()
47
+ assert manager.get("key") is None
48
+
49
+ @patch('platform_sdk.db.redis.redis.Redis')
50
+ def test_get_when_connected(self, mock_redis_class):
51
+ mock_client = MagicMock()
52
+ mock_client.get.return_value = "test-value"
53
+ mock_redis_class.return_value = mock_client
54
+
55
+ manager = RedisManager({})
56
+ manager.connect()
57
+
58
+ result = manager.get("test-key")
59
+ assert result == "test-value"
60
+ mock_client.get.assert_called_once_with("test-key")
61
+
62
+ def test_set_when_not_connected(self):
63
+ manager = RedisManager()
64
+ manager.set("key", "value") # Should not raise
65
+
66
+ @patch('platform_sdk.db.redis.redis.Redis')
67
+ def test_set_when_connected(self, mock_redis_class):
68
+ mock_client = MagicMock()
69
+ mock_redis_class.return_value = mock_client
70
+
71
+ manager = RedisManager({})
72
+ manager.connect()
73
+ manager.set("key", "value")
74
+
75
+ mock_client.set.assert_called_once_with("key", "value")
76
+
77
+ @patch('platform_sdk.db.redis.redis.Redis')
78
+ def test_set_with_ttl(self, mock_redis_class):
79
+ mock_client = MagicMock()
80
+ mock_redis_class.return_value = mock_client
81
+
82
+ manager = RedisManager({})
83
+ manager.connect()
84
+ manager.set("key", "value", ttl=3600)
85
+
86
+ mock_client.setex.assert_called_once_with("key", 3600, "value")
87
+
88
+ @patch('platform_sdk.db.redis.redis.Redis')
89
+ def test_disconnect(self, mock_redis_class):
90
+ mock_client = MagicMock()
91
+ mock_redis_class.return_value = mock_client
92
+
93
+ manager = RedisManager({})
94
+ manager.connect()
95
+ manager.disconnect()
96
+
97
+ assert manager.connected is False
98
+ mock_client.close.assert_called_once()
99
+
100
+ def test_disconnect_when_not_connected(self):
101
+ manager = RedisManager()
102
+ manager.disconnect() # Should not raise