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.
- mpratyush54_sdk-0.6.0/PKG-INFO +62 -0
- mpratyush54_sdk-0.6.0/README.md +39 -0
- mpratyush54_sdk-0.6.0/mpratyush54_sdk.egg-info/PKG-INFO +62 -0
- mpratyush54_sdk-0.6.0/mpratyush54_sdk.egg-info/SOURCES.txt +17 -0
- mpratyush54_sdk-0.6.0/mpratyush54_sdk.egg-info/dependency_links.txt +1 -0
- mpratyush54_sdk-0.6.0/mpratyush54_sdk.egg-info/requires.txt +3 -0
- mpratyush54_sdk-0.6.0/mpratyush54_sdk.egg-info/top_level.txt +1 -0
- mpratyush54_sdk-0.6.0/platform_sdk/__init__.py +170 -0
- mpratyush54_sdk-0.6.0/platform_sdk/db/__init__.py +5 -0
- mpratyush54_sdk-0.6.0/platform_sdk/db/mongo.py +32 -0
- mpratyush54_sdk-0.6.0/platform_sdk/db/postgres.py +49 -0
- mpratyush54_sdk-0.6.0/platform_sdk/db/redis.py +40 -0
- mpratyush54_sdk-0.6.0/setup.cfg +4 -0
- mpratyush54_sdk-0.6.0/setup.py +24 -0
- mpratyush54_sdk-0.6.0/tests/test_caps_client.py +170 -0
- mpratyush54_sdk-0.6.0/tests/test_integration.py +83 -0
- mpratyush54_sdk-0.6.0/tests/test_mongo.py +75 -0
- mpratyush54_sdk-0.6.0/tests/test_postgres.py +114 -0
- mpratyush54_sdk-0.6.0/tests/test_redis.py +102 -0
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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,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,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
|