inter-service-sdk 1.0.2__py3-none-any.whl

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,42 @@
1
+ """
2
+ Inter-Service SDK - Complete framework for service-to-service communication.
3
+
4
+ Provides both client-side and server-side utilities:
5
+ - Client: InterServiceClient for making inter-service HTTP requests
6
+ - Server: FastAPI utilities for creating inter-service endpoints
7
+ - Crypto: Optional ECC encryption/decryption
8
+ - Exceptions: Custom exception hierarchy
9
+ """
10
+
11
+ from .client import InterServiceClient
12
+ from .exceptions import (
13
+ InterServiceError,
14
+ AuthenticationError,
15
+ RequestError,
16
+ EncryptionError,
17
+ URLBuildError
18
+ )
19
+ from .server import (
20
+ create_inter_service_router,
21
+ inter_service_endpoint,
22
+ format_error_response,
23
+ format_success_response
24
+ )
25
+
26
+ __version__ = "1.0.2"
27
+
28
+ __all__ = [
29
+ # Client
30
+ "InterServiceClient",
31
+ # Exceptions
32
+ "InterServiceError",
33
+ "AuthenticationError",
34
+ "RequestError",
35
+ "EncryptionError",
36
+ "URLBuildError",
37
+ # Server utilities
38
+ "create_inter_service_router",
39
+ "inter_service_endpoint",
40
+ "format_error_response",
41
+ "format_success_response",
42
+ ]
@@ -0,0 +1,251 @@
1
+ """
2
+ Inter-service HTTP client with bearer auth and optional encryption.
3
+ """
4
+
5
+ import time
6
+ import logging
7
+ from typing import Optional, Dict, Any
8
+ import requests
9
+ from requests.exceptions import RequestException, Timeout
10
+
11
+ from .utils import build_url
12
+ from .exceptions import AuthenticationError, RequestError
13
+ from . import crypto
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class InterServiceClient:
19
+ """
20
+ Generic HTTP client for inter-service communication.
21
+
22
+ Features:
23
+ - Bearer token authentication
24
+ - Optional ECC encryption/decryption
25
+ - Path and query parameter handling
26
+ - Automatic retry with exponential backoff
27
+ - Consistent response format
28
+
29
+ Example:
30
+ >>> client = InterServiceClient(
31
+ ... base_url="https://api.example.com",
32
+ ... api_key="your-secret-key"
33
+ ... )
34
+ >>> response = client.request(
35
+ ... endpoint="users/{user_id}",
36
+ ... path_params={"user_id": 123}
37
+ ... )
38
+ >>> print(response["data"])
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ base_url: str,
44
+ api_key: str,
45
+ api_prefix: str = "/api/v1/inter-service",
46
+ timeout: int = 30,
47
+ retry_attempts: int = 3,
48
+ ecc_private_key: Optional[str] = None,
49
+ ecc_public_key: Optional[str] = None
50
+ ):
51
+ """
52
+ Initialize inter-service client.
53
+
54
+ Args:
55
+ base_url: Base URL (e.g., "https://api.example.com")
56
+ api_key: Bearer token for authentication
57
+ api_prefix: API prefix (default: "/api/v1/inter-service")
58
+ timeout: Request timeout in seconds (default: 30)
59
+ retry_attempts: Number of retry attempts (default: 3)
60
+ ecc_private_key: ECC private key for decryption (PEM format)
61
+ ecc_public_key: ECC public key for encryption (PEM format)
62
+ """
63
+ self.base_url = base_url.rstrip('/')
64
+ self.api_key = api_key
65
+ self.default_api_prefix = api_prefix
66
+ self.timeout = timeout
67
+ self.retry_attempts = retry_attempts
68
+ self.ecc_private_key = ecc_private_key
69
+ self.ecc_public_key = ecc_public_key
70
+
71
+ # Setup session with auth headers
72
+ self.session = requests.Session()
73
+ self.session.headers.update({
74
+ 'Authorization': f'Bearer {api_key}',
75
+ 'User-Agent': 'InterServiceSDK/1.0.0',
76
+ 'Content-Type': 'application/json',
77
+ 'Accept': 'application/json'
78
+ })
79
+
80
+ logger.info(f"InterServiceClient initialized: {base_url}{api_prefix}")
81
+
82
+ def request(
83
+ self,
84
+ endpoint: str,
85
+ path_params: Optional[Dict[str, Any]] = None,
86
+ query_params: Optional[Dict[str, Any]] = None,
87
+ method: str = "GET",
88
+ data: Optional[Dict[str, Any]] = None,
89
+ headers: Optional[Dict[str, str]] = None,
90
+ encrypt: bool = False,
91
+ decrypt: bool = False,
92
+ timeout: Optional[int] = None,
93
+ api_prefix: Optional[str] = None
94
+ ) -> Dict[str, Any]:
95
+ """
96
+ Make HTTP request to inter-service API.
97
+
98
+ Args:
99
+ endpoint: Endpoint template (e.g., "users/{user_id}")
100
+ path_params: Path parameters for substitution
101
+ query_params: Query string parameters
102
+ method: HTTP method (GET, POST, PUT, DELETE, PATCH)
103
+ data: Request body (JSON)
104
+ headers: Additional headers
105
+ encrypt: Auto-encrypt request data with ECC
106
+ decrypt: Auto-decrypt response with ECC
107
+ timeout: Override default timeout
108
+ api_prefix: Override default API prefix
109
+
110
+ Returns:
111
+ {
112
+ "status": "success" | "error",
113
+ "data": {...} | None,
114
+ "status_code": int,
115
+ "error": None | str
116
+ }
117
+
118
+ Example:
119
+ >>> response = client.request(
120
+ ... endpoint="users/{user_id}",
121
+ ... path_params={"user_id": 123},
122
+ ... query_params={"correlation_id": "track-001"}
123
+ ... )
124
+ """
125
+ # Build URL
126
+ url = build_url(
127
+ self.base_url,
128
+ api_prefix if api_prefix is not None else self.default_api_prefix,
129
+ endpoint,
130
+ path_params,
131
+ query_params
132
+ )
133
+
134
+ # Prepare request data
135
+ json_data = None
136
+ if data:
137
+ if encrypt and self.ecc_public_key:
138
+ try:
139
+ correlation_id = query_params.get("correlation_id", "default") if query_params else "default"
140
+ encrypted = crypto.encrypt_data(data, self.ecc_public_key, correlation_id)
141
+ json_data = encrypted
142
+ except Exception as e:
143
+ logger.error(f"Encryption failed: {e}")
144
+ return {
145
+ "status": "error",
146
+ "data": None,
147
+ "status_code": None,
148
+ "error": f"Encryption failed: {str(e)}"
149
+ }
150
+ else:
151
+ json_data = data
152
+
153
+ # Merge headers
154
+ request_headers = self.session.headers.copy()
155
+ if headers:
156
+ request_headers.update(headers)
157
+
158
+ # Make request with retry logic
159
+ for attempt in range(self.retry_attempts):
160
+ try:
161
+ logger.debug(f"{method} {url}")
162
+
163
+ response = self.session.request(
164
+ method=method.upper(),
165
+ url=url,
166
+ json=json_data,
167
+ headers=request_headers,
168
+ timeout=timeout or self.timeout
169
+ )
170
+
171
+ # Handle HTTP errors
172
+ if response.status_code >= 400:
173
+ logger.error(f"HTTP {response.status_code}: {response.text}")
174
+ return {
175
+ "status": "error",
176
+ "data": None,
177
+ "status_code": response.status_code,
178
+ "error": response.text
179
+ }
180
+
181
+ # Parse response
182
+ response_data = response.json()
183
+
184
+ # Decrypt if needed
185
+ if decrypt and self.ecc_private_key:
186
+ try:
187
+ # Extract encrypted data from response
188
+ if "encrypted_data" in response_data:
189
+ correlation_id = query_params.get("correlation_id", "default") if query_params else "default"
190
+ decrypted = crypto.decrypt_data(
191
+ response_data["encrypted_data"],
192
+ response_data["ephemeral_public_key"],
193
+ response_data["nonce"],
194
+ self.ecc_private_key,
195
+ correlation_id
196
+ )
197
+ response_data["data"] = decrypted
198
+ except Exception as e:
199
+ logger.error(f"Decryption failed: {e}")
200
+ return {
201
+ "status": "error",
202
+ "data": None,
203
+ "status_code": response.status_code,
204
+ "error": f"Decryption failed: {str(e)}"
205
+ }
206
+
207
+ return {
208
+ "status": "success",
209
+ "data": response_data.get("data", response_data),
210
+ "status_code": response.status_code,
211
+ "error": None
212
+ }
213
+
214
+ except Timeout:
215
+ logger.warning(f"Request timeout (attempt {attempt + 1}/{self.retry_attempts})")
216
+ if attempt == self.retry_attempts - 1:
217
+ return {
218
+ "status": "error",
219
+ "data": None,
220
+ "status_code": None,
221
+ "error": f"Request timed out after {timeout or self.timeout} seconds"
222
+ }
223
+ time.sleep(2 ** attempt) # Exponential backoff
224
+
225
+ except RequestException as e:
226
+ logger.warning(f"Request failed (attempt {attempt + 1}/{self.retry_attempts}): {e}")
227
+ if attempt == self.retry_attempts - 1:
228
+ return {
229
+ "status": "error",
230
+ "data": None,
231
+ "status_code": None,
232
+ "error": str(e)
233
+ }
234
+ time.sleep(2 ** attempt) # Exponential backoff
235
+
236
+ except Exception as e:
237
+ logger.error(f"Unexpected error: {e}", exc_info=True)
238
+ return {
239
+ "status": "error",
240
+ "data": None,
241
+ "status_code": None,
242
+ "error": f"Unexpected error: {str(e)}"
243
+ }
244
+
245
+ # Should not reach here
246
+ return {
247
+ "status": "error",
248
+ "data": None,
249
+ "status_code": None,
250
+ "error": "Max retries exceeded"
251
+ }
@@ -0,0 +1,177 @@
1
+ """
2
+ ECC encryption/decryption for inter-service communication.
3
+ Optional module - requires cryptography package.
4
+ """
5
+
6
+ import base64
7
+ import os
8
+ from typing import Optional, Dict, Any
9
+ from .exceptions import EncryptionError
10
+
11
+ try:
12
+ from cryptography.hazmat.primitives import serialization, hashes
13
+ from cryptography.hazmat.primitives.asymmetric import ec
14
+ from cryptography.hazmat.primitives.kdf.hkdf import HKDF
15
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
16
+ from cryptography.hazmat.backends import default_backend
17
+ CRYPTO_AVAILABLE = True
18
+ except ImportError:
19
+ CRYPTO_AVAILABLE = False
20
+
21
+
22
+ def encrypt_data(
23
+ data: Dict[str, Any],
24
+ public_key_pem: str,
25
+ correlation_id: str = "default"
26
+ ) -> Dict[str, Any]:
27
+ """
28
+ Encrypt data using ECC P-256 + ECDH-ES + AES-256-GCM.
29
+
30
+ Args:
31
+ data: Data dictionary to encrypt
32
+ public_key_pem: Public key in PEM format
33
+ correlation_id: Correlation ID for key derivation
34
+
35
+ Returns:
36
+ Dictionary with encrypted data structure:
37
+ {
38
+ "encrypted_data": str (base64),
39
+ "ephemeral_public_key": str (PEM),
40
+ "nonce": str (base64),
41
+ "encryption_algorithm": str,
42
+ "key_version": str
43
+ }
44
+
45
+ Raises:
46
+ EncryptionError: If encryption fails
47
+ """
48
+ if not CRYPTO_AVAILABLE:
49
+ raise EncryptionError(
50
+ "Cryptography package not installed. "
51
+ "Install with: pip install inter-service-sdk[crypto]"
52
+ )
53
+
54
+ try:
55
+ import json
56
+
57
+ # Parse public key
58
+ public_key = serialization.load_pem_public_key(
59
+ public_key_pem.encode('utf-8'),
60
+ backend=default_backend()
61
+ )
62
+
63
+ # Generate ephemeral key pair
64
+ ephemeral_private = ec.generate_private_key(ec.SECP256R1())
65
+ ephemeral_public = ephemeral_private.public_key()
66
+
67
+ # ECDH key exchange
68
+ shared_key = ephemeral_private.exchange(ec.ECDH(), public_key)
69
+
70
+ # Key derivation using HKDF
71
+ derived_key = HKDF(
72
+ algorithm=hashes.SHA256(),
73
+ length=32, # 256-bit key for AES-256
74
+ salt=None,
75
+ info=f"ECC-inter-service-{correlation_id}".encode('utf-8'),
76
+ backend=default_backend()
77
+ ).derive(shared_key)
78
+
79
+ # Convert data to JSON bytes
80
+ data_json = json.dumps(data, separators=(',', ':'))
81
+ data_bytes = data_json.encode('utf-8')
82
+
83
+ # Generate random nonce (96-bit for GCM)
84
+ nonce = os.urandom(12)
85
+
86
+ # Encrypt using AES-256-GCM
87
+ aead = AESGCM(derived_key)
88
+ ciphertext = aead.encrypt(nonce, data_bytes, None)
89
+
90
+ # Serialize ephemeral public key
91
+ ephemeral_public_pem = ephemeral_public.public_bytes(
92
+ encoding=serialization.Encoding.PEM,
93
+ format=serialization.PublicFormat.SubjectPublicKeyInfo
94
+ ).decode('utf-8')
95
+
96
+ return {
97
+ "encrypted_data": base64.b64encode(ciphertext).decode('utf-8'),
98
+ "ephemeral_public_key": ephemeral_public_pem,
99
+ "nonce": base64.b64encode(nonce).decode('utf-8'),
100
+ "encryption_algorithm": "ECC-P256+ECDH-ES+AES-256-GCM",
101
+ "key_version": "v2.0"
102
+ }
103
+
104
+ except Exception as e:
105
+ raise EncryptionError(f"Encryption failed: {str(e)}")
106
+
107
+
108
+ def decrypt_data(
109
+ encrypted_data: str,
110
+ ephemeral_public_key_pem: str,
111
+ nonce: str,
112
+ private_key_pem: str,
113
+ correlation_id: str = "default"
114
+ ) -> Dict[str, Any]:
115
+ """
116
+ Decrypt data using ECC P-256 + ECDH-ES + AES-256-GCM.
117
+
118
+ Args:
119
+ encrypted_data: Base64-encoded ciphertext
120
+ ephemeral_public_key_pem: Ephemeral public key in PEM format
121
+ nonce: Base64-encoded nonce
122
+ private_key_pem: Private key in PEM format
123
+ correlation_id: Correlation ID for key derivation
124
+
125
+ Returns:
126
+ Decrypted data dictionary
127
+
128
+ Raises:
129
+ EncryptionError: If decryption fails
130
+ """
131
+ if not CRYPTO_AVAILABLE:
132
+ raise EncryptionError(
133
+ "Cryptography package not installed. "
134
+ "Install with: pip install inter-service-sdk[crypto]"
135
+ )
136
+
137
+ try:
138
+ import json
139
+
140
+ # Load private key
141
+ private_key = serialization.load_pem_private_key(
142
+ private_key_pem.encode('utf-8'),
143
+ password=None,
144
+ backend=default_backend()
145
+ )
146
+
147
+ # Parse ephemeral public key
148
+ ephemeral_public_key = serialization.load_pem_public_key(
149
+ ephemeral_public_key_pem.encode('utf-8'),
150
+ backend=default_backend()
151
+ )
152
+
153
+ # Perform ECDH key exchange
154
+ shared_key = private_key.exchange(ec.ECDH(), ephemeral_public_key)
155
+
156
+ # Derive AES key using HKDF
157
+ derived_key = HKDF(
158
+ algorithm=hashes.SHA256(),
159
+ length=32,
160
+ salt=None,
161
+ info=f"ECC-inter-service-{correlation_id}".encode('utf-8'),
162
+ backend=default_backend()
163
+ ).derive(shared_key)
164
+
165
+ # Decode base64 data
166
+ nonce_bytes = base64.b64decode(nonce)
167
+ ciphertext_bytes = base64.b64decode(encrypted_data)
168
+
169
+ # Decrypt using AES-256-GCM
170
+ aead = AESGCM(derived_key)
171
+ decrypted_bytes = aead.decrypt(nonce_bytes, ciphertext_bytes, None)
172
+ decrypted_json = decrypted_bytes.decode('utf-8')
173
+
174
+ return json.loads(decrypted_json)
175
+
176
+ except Exception as e:
177
+ raise EncryptionError(f"Decryption failed: {str(e)}")
@@ -0,0 +1,31 @@
1
+ """
2
+ Custom exceptions for inter-service SDK.
3
+ """
4
+
5
+
6
+ class InterServiceError(Exception):
7
+ """Base exception for inter-service SDK."""
8
+ pass
9
+
10
+
11
+ class AuthenticationError(InterServiceError):
12
+ """Authentication failed."""
13
+ pass
14
+
15
+
16
+ class RequestError(InterServiceError):
17
+ """Request failed."""
18
+
19
+ def __init__(self, message: str, status_code: int = None):
20
+ super().__init__(message)
21
+ self.status_code = status_code
22
+
23
+
24
+ class EncryptionError(InterServiceError):
25
+ """Encryption/decryption failed."""
26
+ pass
27
+
28
+
29
+ class URLBuildError(InterServiceError):
30
+ """URL building failed."""
31
+ pass
@@ -0,0 +1,238 @@
1
+ """
2
+ Server-side utilities for creating inter-service API endpoints with FastAPI.
3
+
4
+ Provides decorators and utilities to reduce boilerplate in inter-service endpoints:
5
+ - Router factory with configurable auth
6
+ - Automatic error handling and logging
7
+ - Consistent response formatting
8
+ - Correlation ID management
9
+ """
10
+
11
+ import logging
12
+ from typing import Callable, Any, Dict, Optional
13
+ from functools import wraps
14
+ from datetime import datetime, timezone
15
+ from fastapi import APIRouter, Request, HTTPException, status
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def create_inter_service_router(
21
+ prefix: str = "/api/v1/inter-service",
22
+ tags: Optional[list] = None,
23
+ auth_dependency: Optional[Any] = None
24
+ ) -> APIRouter:
25
+ """
26
+ Create FastAPI router for inter-service endpoints.
27
+
28
+ Returns router with:
29
+ - Configurable prefix (default: /api/v1/inter-service)
30
+ - Optional authentication dependency
31
+ - Custom or default tags
32
+
33
+ Example:
34
+ from fastapi import Depends
35
+ from your_auth import require_inter_service_auth
36
+
37
+ router = create_inter_service_router(
38
+ auth_dependency=Depends(require_inter_service_auth)
39
+ )
40
+
41
+ @router.get("/users/{user_id}")
42
+ async def get_user(user_id: int):
43
+ return {"user_id": user_id}
44
+
45
+ Args:
46
+ prefix: API prefix path
47
+ tags: List of tags for OpenAPI docs
48
+ auth_dependency: FastAPI dependency for authentication
49
+
50
+ Returns:
51
+ Configured APIRouter instance
52
+ """
53
+ dependencies = []
54
+ if auth_dependency is not None:
55
+ dependencies.append(auth_dependency)
56
+
57
+ return APIRouter(
58
+ prefix=prefix,
59
+ tags=tags or ["Inter-Service API"],
60
+ dependencies=dependencies
61
+ )
62
+
63
+
64
+ def inter_service_endpoint(
65
+ endpoint_name: str,
66
+ require_correlation_id: bool = True
67
+ ):
68
+ """
69
+ Decorator for inter-service endpoints with automatic logging and error handling.
70
+
71
+ Provides:
72
+ - Request/response logging with correlation IDs
73
+ - Client host logging
74
+ - Automatic error handling and formatting
75
+ - Consistent timestamp formatting
76
+
77
+ Example:
78
+ @router.get("/users/{user_id}")
79
+ @inter_service_endpoint("get_user")
80
+ async def get_user(user_id: int, correlation_id: str, request: Request):
81
+ # Your endpoint logic here
82
+ return {"user_id": user_id, "name": "John Doe"}
83
+
84
+ # Automatically wrapped response:
85
+ # {
86
+ # "status": "success",
87
+ # "data": {"user_id": 123, "name": "John Doe"},
88
+ # "correlation_id": "req-001",
89
+ # "timestamp": "2025-01-01T00:00:00Z"
90
+ # }
91
+
92
+ Args:
93
+ endpoint_name: Name for logging (e.g., "get_credentials")
94
+ require_correlation_id: Whether correlation_id parameter is required
95
+
96
+ Returns:
97
+ Decorated function with automatic logging and error handling
98
+ """
99
+ def decorator(func: Callable) -> Callable:
100
+ @wraps(func)
101
+ async def wrapper(*args, **kwargs):
102
+ # Extract common parameters
103
+ request: Optional[Request] = kwargs.get("request")
104
+ correlation_id = kwargs.get("correlation_id", "unknown")
105
+
106
+ # Validation
107
+ if require_correlation_id and correlation_id == "unknown":
108
+ logger.error(f"❌ [{endpoint_name}] Missing required correlation_id parameter")
109
+ raise HTTPException(
110
+ status_code=status.HTTP_400_BAD_REQUEST,
111
+ detail="correlation_id query parameter is required"
112
+ )
113
+
114
+ # Log request
115
+ client_host = request.client.host if request and request.client else "unknown"
116
+ user_agent = request.headers.get("user-agent", "unknown") if request else "unknown"
117
+
118
+ logger.info(f"🔗 [{endpoint_name}] Request started - Correlation: {correlation_id}")
119
+ logger.info(f" Client: {client_host}, User-Agent: {user_agent}")
120
+
121
+ try:
122
+ # Execute endpoint function
123
+ result = await func(*args, **kwargs)
124
+
125
+ # If result is already a dict with status field, return as-is
126
+ if isinstance(result, dict) and "status" in result:
127
+ logger.info(f"✅ [{endpoint_name}] Request completed - Status: {result.get('status')}")
128
+ return result
129
+
130
+ # Otherwise, wrap in standard response format
131
+ response = {
132
+ "status": "success",
133
+ "data": result,
134
+ "correlation_id": correlation_id,
135
+ "timestamp": datetime.now(timezone.utc).isoformat()
136
+ }
137
+
138
+ logger.info(f"✅ [{endpoint_name}] Request completed successfully")
139
+ return response
140
+
141
+ except HTTPException:
142
+ # Re-raise HTTP exceptions (already have proper status codes)
143
+ raise
144
+
145
+ except Exception as e:
146
+ logger.error(f"❌ [{endpoint_name}] Error: {str(e)}", exc_info=True)
147
+ raise HTTPException(
148
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
149
+ detail=f"Internal server error in {endpoint_name}"
150
+ )
151
+
152
+ return wrapper
153
+ return decorator
154
+
155
+
156
+ def format_error_response(
157
+ message: str,
158
+ correlation_id: str,
159
+ status_code: int = 500,
160
+ **extra_fields
161
+ ) -> Dict[str, Any]:
162
+ """
163
+ Format standard error response.
164
+
165
+ Args:
166
+ message: Error message
167
+ correlation_id: Request correlation ID
168
+ status_code: HTTP status code (not included in response, for reference)
169
+ **extra_fields: Additional fields to include in response
170
+
171
+ Returns:
172
+ Formatted error response dict
173
+
174
+ Example:
175
+ return format_error_response(
176
+ message="User not found",
177
+ correlation_id="req-001",
178
+ status_code=404,
179
+ user_id=123
180
+ )
181
+ # Returns:
182
+ # {
183
+ # "status": "error",
184
+ # "error": "User not found",
185
+ # "correlation_id": "req-001",
186
+ # "timestamp": "2025-01-01T00:00:00Z",
187
+ # "user_id": 123
188
+ # }
189
+ """
190
+ response = {
191
+ "status": "error",
192
+ "error": message,
193
+ "correlation_id": correlation_id,
194
+ "timestamp": datetime.now(timezone.utc).isoformat()
195
+ }
196
+ response.update(extra_fields)
197
+ return response
198
+
199
+
200
+ def format_success_response(
201
+ data: Any,
202
+ correlation_id: str,
203
+ **extra_fields
204
+ ) -> Dict[str, Any]:
205
+ """
206
+ Format standard success response.
207
+
208
+ Args:
209
+ data: Response data
210
+ correlation_id: Request correlation ID
211
+ **extra_fields: Additional fields to include in response
212
+
213
+ Returns:
214
+ Formatted success response dict
215
+
216
+ Example:
217
+ return format_success_response(
218
+ data={"user_id": 123, "name": "John"},
219
+ correlation_id="req-001",
220
+ cache_hit=True
221
+ )
222
+ # Returns:
223
+ # {
224
+ # "status": "success",
225
+ # "data": {"user_id": 123, "name": "John"},
226
+ # "correlation_id": "req-001",
227
+ # "timestamp": "2025-01-01T00:00:00Z",
228
+ # "cache_hit": True
229
+ # }
230
+ """
231
+ response = {
232
+ "status": "success",
233
+ "data": data,
234
+ "correlation_id": correlation_id,
235
+ "timestamp": datetime.now(timezone.utc).isoformat()
236
+ }
237
+ response.update(extra_fields)
238
+ return response
@@ -0,0 +1,61 @@
1
+ """
2
+ Utility functions for inter-service SDK.
3
+ """
4
+
5
+ from typing import Dict, Any, Optional
6
+ from urllib.parse import urlencode
7
+ from .exceptions import URLBuildError
8
+
9
+
10
+ def build_url(
11
+ base_url: str,
12
+ api_prefix: str,
13
+ endpoint: str,
14
+ path_params: Optional[Dict[str, Any]] = None,
15
+ query_params: Optional[Dict[str, Any]] = None
16
+ ) -> str:
17
+ """
18
+ Build complete URL with path parameter substitution.
19
+
20
+ Args:
21
+ base_url: Base URL (e.g., "https://api.example.com")
22
+ api_prefix: API prefix (e.g., "/api/v1/inter-service")
23
+ endpoint: Endpoint template (e.g., "users/{user_id}")
24
+ path_params: Path parameters for substitution
25
+ query_params: Query string parameters
26
+
27
+ Returns:
28
+ Complete URL string
29
+
30
+ Example:
31
+ >>> build_url(
32
+ ... "https://api.example.com",
33
+ ... "/api/v1/inter-service",
34
+ ... "users/{user_id}",
35
+ ... {"user_id": 123},
36
+ ... {"correlation_id": "track-001"}
37
+ ... )
38
+ 'https://api.example.com/api/v1/inter-service/users/123?correlation_id=track-001'
39
+ """
40
+ try:
41
+ # Substitute path parameters
42
+ if path_params is not None:
43
+ endpoint = endpoint.format(**path_params)
44
+
45
+ # Build full URL
46
+ url = f"{base_url}{api_prefix}/{endpoint}"
47
+
48
+ # Add query parameters
49
+ if query_params:
50
+ # Filter out None values
51
+ clean_params = {k: v for k, v in query_params.items() if v is not None}
52
+ if clean_params:
53
+ query_string = urlencode(clean_params)
54
+ url = f"{url}?{query_string}"
55
+
56
+ return url
57
+
58
+ except KeyError as e:
59
+ raise URLBuildError(f"Missing path parameter: {e}")
60
+ except Exception as e:
61
+ raise URLBuildError(f"Failed to build URL: {e}")
@@ -0,0 +1,332 @@
1
+ Metadata-Version: 2.4
2
+ Name: inter-service-sdk
3
+ Version: 1.0.2
4
+ Summary: Complete framework for inter-service communication with client, server utilities, auth and encryption
5
+ Home-page: https://github.com/AlexanderRyzhko/inter-service-sdk
6
+ Author: Blazel
7
+ Author-email: Blazel <dev@blazel.com>
8
+ Project-URL: Homepage, https://github.com/AlexanderRyzhko/inter-service-sdk
9
+ Project-URL: Bug Tracker, https://github.com/AlexanderRyzhko/inter-service-sdk/issues
10
+ Project-URL: Documentation, https://github.com/AlexanderRyzhko/inter-service-sdk#readme
11
+ Project-URL: Source Code, https://github.com/AlexanderRyzhko/inter-service-sdk
12
+ Keywords: http,client,server,api,rest,fastapi,inter-service,microservices,authentication,encryption,ecc,bearer-token
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Topic :: Internet :: WWW/HTTP
25
+ Classifier: Topic :: Security :: Cryptography
26
+ Requires-Python: >=3.8
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: requests>=2.31.0
30
+ Requires-Dist: fastapi>=0.104.0
31
+ Provides-Extra: crypto
32
+ Requires-Dist: cryptography>=41.0.0; extra == "crypto"
33
+ Provides-Extra: dev
34
+ Requires-Dist: pytest>=7.4.0; extra == "dev"
35
+ Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
36
+ Requires-Dist: pytest-mock>=3.11.1; extra == "dev"
37
+ Requires-Dist: requests-mock>=1.11.0; extra == "dev"
38
+ Requires-Dist: black>=23.0.0; extra == "dev"
39
+ Requires-Dist: flake8>=6.1.0; extra == "dev"
40
+ Requires-Dist: mypy>=1.5.0; extra == "dev"
41
+ Requires-Dist: types-requests>=2.31.0; extra == "dev"
42
+ Requires-Dist: build>=1.0.0; extra == "dev"
43
+ Requires-Dist: twine>=4.0.0; extra == "dev"
44
+ Requires-Dist: cryptography>=41.0.0; extra == "dev"
45
+ Dynamic: author
46
+ Dynamic: home-page
47
+ Dynamic: license-file
48
+ Dynamic: requires-python
49
+
50
+ # Inter-Service SDK
51
+
52
+ Generic HTTP client for secure service-to-service communication with bearer token authentication and optional ECC encryption.
53
+
54
+ ## Features
55
+
56
+ - 🔐 **Bearer Token Auth** - Automatic authentication headers
57
+ - 🔒 **Optional ECC Encryption** - End-to-end encryption for sensitive data
58
+ - 🎯 **Path & Query Parameters** - Clean parameter substitution
59
+ - 🔄 **Automatic Retries** - Exponential backoff for failed requests
60
+ - 📝 **Structured Logging** - Request/response tracking
61
+ - 🚀 **Zero Dependencies** - Only requests and cryptography (optional)
62
+
63
+ ## Installation
64
+
65
+ ```bash
66
+ pip install inter-service-sdk
67
+ ```
68
+
69
+ ## Quick Start
70
+
71
+ ```python
72
+ from inter_service_sdk import InterServiceClient
73
+
74
+ # Initialize client
75
+ client = InterServiceClient(
76
+ base_url="https://api.example.com",
77
+ api_key="your-secret-key"
78
+ )
79
+
80
+ # Make a request
81
+ response = client.request(
82
+ endpoint="users/{user_id}",
83
+ path_params={"user_id": 123}
84
+ )
85
+
86
+ print(response["data"])
87
+ ```
88
+
89
+ ## Usage
90
+
91
+ ### Basic GET Request
92
+
93
+ ```python
94
+ client = InterServiceClient(
95
+ base_url="https://api.example.com",
96
+ api_key="your-api-key"
97
+ )
98
+
99
+ # GET /api/v1/inter-service/users/123
100
+ user = client.request(
101
+ endpoint="users/{user_id}",
102
+ path_params={"user_id": 123}
103
+ )
104
+ ```
105
+
106
+ ### With Query Parameters
107
+
108
+ ```python
109
+ # GET /api/v1/inter-service/users/search?q=john&type=email&limit=10
110
+ results = client.request(
111
+ endpoint="users/search",
112
+ query_params={
113
+ "q": "john",
114
+ "type": "email",
115
+ "limit": 10
116
+ }
117
+ )
118
+ ```
119
+
120
+ ### POST Request
121
+
122
+ ```python
123
+ # POST /api/v1/inter-service/users
124
+ new_user = client.request(
125
+ endpoint="users",
126
+ method="POST",
127
+ data={
128
+ "name": "John Doe",
129
+ "email": "john@example.com"
130
+ }
131
+ )
132
+ ```
133
+
134
+ ### With ECC Encryption
135
+
136
+ ```python
137
+ client = InterServiceClient(
138
+ base_url="https://api.example.com",
139
+ api_key="your-api-key",
140
+ ecc_private_key=os.getenv("PRIVATE_KEY"),
141
+ ecc_public_key=os.getenv("PUBLIC_KEY")
142
+ )
143
+
144
+ # Auto-decrypt response
145
+ credentials = client.request(
146
+ endpoint="credentials/{id}",
147
+ path_params={"id": 456},
148
+ decrypt=True
149
+ )
150
+
151
+ # Auto-encrypt request
152
+ client.request(
153
+ endpoint="secrets",
154
+ method="POST",
155
+ data={"secret": "sensitive data"},
156
+ encrypt=True
157
+ )
158
+ ```
159
+
160
+ ### Custom API Prefix
161
+
162
+ ```python
163
+ # Default prefix: /api/v1/inter-service
164
+ client = InterServiceClient(
165
+ base_url="https://api.example.com",
166
+ api_key="key",
167
+ api_prefix="/v2/api" # Custom prefix
168
+ )
169
+
170
+ # Override per request
171
+ response = client.request(
172
+ endpoint="custom",
173
+ api_prefix="/internal"
174
+ )
175
+ ```
176
+
177
+ ## API Reference
178
+
179
+ ### InterServiceClient
180
+
181
+ ```python
182
+ InterServiceClient(
183
+ base_url: str,
184
+ api_key: str,
185
+ api_prefix: str = "/api/v1/inter-service",
186
+ timeout: int = 30,
187
+ retry_attempts: int = 3,
188
+ ecc_private_key: str = None,
189
+ ecc_public_key: str = None
190
+ )
191
+ ```
192
+
193
+ #### Parameters
194
+
195
+ - `base_url` (str): Base URL of the API (e.g., "https://api.example.com")
196
+ - `api_key` (str): Bearer token for authentication
197
+ - `api_prefix` (str, optional): API prefix. Default: "/api/v1/inter-service"
198
+ - `timeout` (int, optional): Request timeout in seconds. Default: 30
199
+ - `retry_attempts` (int, optional): Number of retry attempts. Default: 3
200
+ - `ecc_private_key` (str, optional): ECC private key for decryption
201
+ - `ecc_public_key` (str, optional): ECC public key for encryption
202
+
203
+ ### request()
204
+
205
+ ```python
206
+ client.request(
207
+ endpoint: str,
208
+ path_params: dict = None,
209
+ query_params: dict = None,
210
+ method: str = "GET",
211
+ data: dict = None,
212
+ headers: dict = None,
213
+ encrypt: bool = False,
214
+ decrypt: bool = False,
215
+ timeout: int = None,
216
+ api_prefix: str = None
217
+ ) -> dict
218
+ ```
219
+
220
+ #### Parameters
221
+
222
+ - `endpoint` (str): Endpoint template (e.g., "users/{user_id}")
223
+ - `path_params` (dict, optional): Path parameters for substitution
224
+ - `query_params` (dict, optional): Query string parameters
225
+ - `method` (str, optional): HTTP method. Default: "GET"
226
+ - `data` (dict, optional): Request body (JSON)
227
+ - `headers` (dict, optional): Additional headers
228
+ - `encrypt` (bool, optional): Auto-encrypt request data. Default: False
229
+ - `decrypt` (bool, optional): Auto-decrypt response. Default: False
230
+ - `timeout` (int, optional): Override default timeout
231
+ - `api_prefix` (str, optional): Override default API prefix
232
+
233
+ #### Returns
234
+
235
+ ```python
236
+ {
237
+ "status": "success" | "error",
238
+ "data": {...} | None,
239
+ "status_code": int,
240
+ "error": None | str
241
+ }
242
+ ```
243
+
244
+ ## Error Handling
245
+
246
+ ```python
247
+ response = client.request(endpoint="users/123")
248
+
249
+ if response["status"] == "success":
250
+ user = response["data"]
251
+ print(user)
252
+ else:
253
+ print(f"Error: {response['error']}")
254
+ print(f"Status code: {response['status_code']}")
255
+ ```
256
+
257
+ ## Configuration
258
+
259
+ ### Environment Variables
260
+
261
+ ```bash
262
+ # Recommended approach
263
+ export API_BASE_URL="https://api.example.com"
264
+ export API_KEY="your-secret-key"
265
+ export ECC_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----..."
266
+ export ECC_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----..."
267
+ ```
268
+
269
+ ```python
270
+ import os
271
+ from inter_service_sdk import InterServiceClient
272
+
273
+ client = InterServiceClient(
274
+ base_url=os.getenv("API_BASE_URL"),
275
+ api_key=os.getenv("API_KEY"),
276
+ ecc_private_key=os.getenv("ECC_PRIVATE_KEY"),
277
+ ecc_public_key=os.getenv("ECC_PUBLIC_KEY")
278
+ )
279
+ ```
280
+
281
+ ## Development
282
+
283
+ ### Install Development Dependencies
284
+
285
+ ```bash
286
+ pip install -r requirements-dev.txt
287
+ ```
288
+
289
+ ### Run Tests
290
+
291
+ ```bash
292
+ pytest tests/ -v
293
+ ```
294
+
295
+ ### Run Tests with Coverage
296
+
297
+ ```bash
298
+ pytest tests/ --cov=inter_service_sdk --cov-report=html
299
+ ```
300
+
301
+ ### Code Formatting
302
+
303
+ ```bash
304
+ black inter_service_sdk tests
305
+ ```
306
+
307
+ ### Type Checking
308
+
309
+ ```bash
310
+ mypy inter_service_sdk
311
+ ```
312
+
313
+ ## Examples
314
+
315
+ See the `examples/` directory for more usage examples:
316
+
317
+ - `basic_usage.py` - Simple GET request
318
+ - `with_encryption.py` - ECC encryption example
319
+ - `search_example.py` - Query parameters example
320
+ - `post_example.py` - POST request example
321
+
322
+ ## License
323
+
324
+ MIT License - see LICENSE file for details.
325
+
326
+ ## Contributing
327
+
328
+ Contributions are welcome! Please open an issue or submit a pull request.
329
+
330
+ ## Support
331
+
332
+ For questions or issues, please open a GitHub issue.
@@ -0,0 +1,11 @@
1
+ inter_service_sdk/__init__.py,sha256=jh8ssdcY-xx8YqU-95bS66Hwk0MuTBgb4WLa29mm4KA,1029
2
+ inter_service_sdk/client.py,sha256=vVxVFwz1PUAqar2oB4xvnjnK9Q2K7aUXHOD61c1LhEQ,9083
3
+ inter_service_sdk/crypto.py,sha256=TAZdYwY31oqcXtD-jiMGI78JB8sXLmcH-5FtbrW9QzM,5454
4
+ inter_service_sdk/exceptions.py,sha256=fsSTlzFONRkp-XyDTurBukYLxVW0Jv92oiRzJckSf20,613
5
+ inter_service_sdk/server.py,sha256=o_yTeJsZEaENaWu7sXsoOoYIHyTaQlKIErPZ97LnMhc,7444
6
+ inter_service_sdk/utils.py,sha256=RUsAvfW4SR88pEIAeALlsgCVvZW0dh7fMrFXyKoJLAI,1814
7
+ inter_service_sdk-1.0.2.dist-info/licenses/LICENSE,sha256=XEFRrh9UxgguPd7guDSATVu6fD-ELas15sua3tt-P1A,1062
8
+ inter_service_sdk-1.0.2.dist-info/METADATA,sha256=K0vtMkaqzUPwdk0mJehg7yw5os58Bp3uh000uwlVgbo,8264
9
+ inter_service_sdk-1.0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
+ inter_service_sdk-1.0.2.dist-info/top_level.txt,sha256=xamGXNnKO2HPkQewZcNFzMi8ex2aur43r-ENiu5Lmj8,18
11
+ inter_service_sdk-1.0.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Blazel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ inter_service_sdk