miso-client 0.1.0__py3-none-any.whl → 3.7.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.
Files changed (69) hide show
  1. miso_client/__init__.py +523 -130
  2. miso_client/api/__init__.py +35 -0
  3. miso_client/api/auth_api.py +367 -0
  4. miso_client/api/logs_api.py +91 -0
  5. miso_client/api/permissions_api.py +88 -0
  6. miso_client/api/roles_api.py +88 -0
  7. miso_client/api/types/__init__.py +75 -0
  8. miso_client/api/types/auth_types.py +183 -0
  9. miso_client/api/types/logs_types.py +71 -0
  10. miso_client/api/types/permissions_types.py +31 -0
  11. miso_client/api/types/roles_types.py +31 -0
  12. miso_client/errors.py +30 -4
  13. miso_client/models/__init__.py +4 -0
  14. miso_client/models/config.py +275 -72
  15. miso_client/models/error_response.py +39 -0
  16. miso_client/models/filter.py +255 -0
  17. miso_client/models/pagination.py +44 -0
  18. miso_client/models/sort.py +25 -0
  19. miso_client/services/__init__.py +6 -5
  20. miso_client/services/auth.py +496 -87
  21. miso_client/services/cache.py +42 -41
  22. miso_client/services/encryption.py +18 -17
  23. miso_client/services/logger.py +467 -328
  24. miso_client/services/logger_chain.py +288 -0
  25. miso_client/services/permission.py +130 -67
  26. miso_client/services/redis.py +28 -23
  27. miso_client/services/role.py +145 -62
  28. miso_client/utils/__init__.py +3 -3
  29. miso_client/utils/audit_log_queue.py +222 -0
  30. miso_client/utils/auth_strategy.py +88 -0
  31. miso_client/utils/auth_utils.py +65 -0
  32. miso_client/utils/circuit_breaker.py +125 -0
  33. miso_client/utils/client_token_manager.py +244 -0
  34. miso_client/utils/config_loader.py +88 -17
  35. miso_client/utils/controller_url_resolver.py +80 -0
  36. miso_client/utils/data_masker.py +104 -33
  37. miso_client/utils/environment_token.py +126 -0
  38. miso_client/utils/error_utils.py +216 -0
  39. miso_client/utils/fastapi_endpoints.py +166 -0
  40. miso_client/utils/filter.py +364 -0
  41. miso_client/utils/filter_applier.py +143 -0
  42. miso_client/utils/filter_parser.py +110 -0
  43. miso_client/utils/flask_endpoints.py +169 -0
  44. miso_client/utils/http_client.py +494 -262
  45. miso_client/utils/http_client_logging.py +352 -0
  46. miso_client/utils/http_client_logging_helpers.py +197 -0
  47. miso_client/utils/http_client_query_helpers.py +138 -0
  48. miso_client/utils/http_error_handler.py +92 -0
  49. miso_client/utils/http_log_formatter.py +115 -0
  50. miso_client/utils/http_log_masker.py +203 -0
  51. miso_client/utils/internal_http_client.py +435 -0
  52. miso_client/utils/jwt_tools.py +125 -16
  53. miso_client/utils/logger_helpers.py +206 -0
  54. miso_client/utils/logging_helpers.py +70 -0
  55. miso_client/utils/origin_validator.py +128 -0
  56. miso_client/utils/pagination.py +275 -0
  57. miso_client/utils/request_context.py +285 -0
  58. miso_client/utils/sensitive_fields_loader.py +116 -0
  59. miso_client/utils/sort.py +116 -0
  60. miso_client/utils/token_utils.py +114 -0
  61. miso_client/utils/url_validator.py +66 -0
  62. miso_client/utils/user_token_refresh.py +245 -0
  63. miso_client-3.7.2.dist-info/METADATA +1021 -0
  64. miso_client-3.7.2.dist-info/RECORD +68 -0
  65. miso_client-0.1.0.dist-info/METADATA +0 -551
  66. miso_client-0.1.0.dist-info/RECORD +0 -23
  67. {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/WHEEL +0 -0
  68. {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/licenses/LICENSE +0 -0
  69. {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,206 @@
1
+ """
2
+ Logger helper functions for building log entries.
3
+
4
+ Extracted from logger.py to reduce file size and improve maintainability.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ from datetime import datetime
10
+ from typing import Any, Dict, Literal, Optional, Union
11
+
12
+ from ..models.config import ClientLoggingOptions, ForeignKeyReference, LogEntry
13
+ from ..utils.data_masker import DataMasker
14
+ from ..utils.jwt_tools import decode_token
15
+
16
+
17
+ def extract_jwt_context(token: Optional[str]) -> Dict[str, Any]:
18
+ """
19
+ Extract JWT token information.
20
+
21
+ Args:
22
+ token: JWT token string
23
+
24
+ Returns:
25
+ Dictionary with userId, applicationId, sessionId, roles, permissions
26
+ """
27
+ if not token:
28
+ return {}
29
+
30
+ try:
31
+ decoded = decode_token(token)
32
+ if not decoded:
33
+ return {}
34
+
35
+ # Extract roles - handle different formats
36
+ roles = []
37
+ if "roles" in decoded:
38
+ roles = decoded["roles"] if isinstance(decoded["roles"], list) else []
39
+ elif "realm_access" in decoded and isinstance(decoded["realm_access"], dict):
40
+ roles = decoded["realm_access"].get("roles", [])
41
+
42
+ # Extract permissions - handle different formats
43
+ permissions = []
44
+ if "permissions" in decoded:
45
+ permissions = decoded["permissions"] if isinstance(decoded["permissions"], list) else []
46
+ elif "scope" in decoded and isinstance(decoded["scope"], str):
47
+ permissions = decoded["scope"].split()
48
+
49
+ return {
50
+ "userId": decoded.get("sub") or decoded.get("userId") or decoded.get("user_id"),
51
+ "applicationId": decoded.get("applicationId") or decoded.get("app_id"),
52
+ "sessionId": decoded.get("sessionId") or decoded.get("sid"),
53
+ "roles": roles,
54
+ "permissions": permissions,
55
+ }
56
+ except Exception:
57
+ # JWT parsing failed, return empty context
58
+ return {}
59
+
60
+
61
+ def extract_metadata() -> Dict[str, Any]:
62
+ """
63
+ Extract metadata from environment (browser or Node.js).
64
+
65
+ Returns:
66
+ Dictionary with hostname, userAgent, etc.
67
+ """
68
+ metadata: Dict[str, Any] = {}
69
+
70
+ # Try to extract Node.js/Python metadata
71
+ if hasattr(os, "environ"):
72
+ metadata["hostname"] = os.environ.get("HOSTNAME", "unknown")
73
+
74
+ # In Python, we don't have browser metadata like in TypeScript
75
+ # But we can capture some environment info
76
+ metadata["platform"] = sys.platform
77
+ metadata["python_version"] = sys.version
78
+
79
+ return metadata
80
+
81
+
82
+ def _convert_to_foreign_key_reference(
83
+ value: Optional[Union[str, ForeignKeyReference]], entity_type: str
84
+ ) -> Optional[ForeignKeyReference]:
85
+ """
86
+ Convert string ID or ForeignKeyReference to ForeignKeyReference object.
87
+
88
+ Args:
89
+ value: String ID or ForeignKeyReference object
90
+ entity_type: Entity type (e.g., 'User', 'Application')
91
+
92
+ Returns:
93
+ ForeignKeyReference object or None
94
+ """
95
+ if value is None:
96
+ return None
97
+
98
+ # If already a ForeignKeyReference, return as-is
99
+ if isinstance(value, ForeignKeyReference):
100
+ return value
101
+
102
+ # If string, create minimal ForeignKeyReference
103
+ # Note: This is a minimal conversion - full ForeignKeyReference should come from API responses
104
+ if isinstance(value, str):
105
+ return ForeignKeyReference(
106
+ id=value,
107
+ key=value, # Use id as key when key is not available
108
+ name=value, # Use id as name when name is not available
109
+ type=entity_type,
110
+ )
111
+
112
+ return None
113
+
114
+
115
+ def build_log_entry(
116
+ level: Literal["error", "audit", "info", "debug"],
117
+ message: str,
118
+ context: Optional[Dict[str, Any]],
119
+ config_client_id: str,
120
+ correlation_id: Optional[str] = None,
121
+ jwt_token: Optional[str] = None,
122
+ stack_trace: Optional[str] = None,
123
+ options: Optional[ClientLoggingOptions] = None,
124
+ metadata: Optional[Dict[str, Any]] = None,
125
+ mask_sensitive: bool = True,
126
+ ) -> LogEntry:
127
+ """
128
+ Build LogEntry object from parameters.
129
+
130
+ Args:
131
+ level: Log level
132
+ message: Log message
133
+ context: Additional context data
134
+ config_client_id: Client ID from config
135
+ correlation_id: Optional correlation ID
136
+ jwt_token: Optional JWT token for context extraction
137
+ stack_trace: Stack trace for errors
138
+ options: Logging options
139
+ metadata: Environment metadata
140
+ mask_sensitive: Whether to mask sensitive data
141
+
142
+ Returns:
143
+ LogEntry object
144
+ """
145
+ # Extract JWT context if token provided
146
+ jwt_context = extract_jwt_context(jwt_token or (options.token if options else None))
147
+
148
+ # Extract environment metadata
149
+ env_metadata = metadata or extract_metadata()
150
+
151
+ # Generate correlation ID if not provided
152
+ final_correlation_id = correlation_id or (options.correlationId if options else None)
153
+
154
+ # Mask sensitive data in context if enabled
155
+ should_mask = (options.maskSensitiveData if options else None) is not False and mask_sensitive
156
+ masked_context = DataMasker.mask_sensitive_data(context) if should_mask and context else context
157
+
158
+ # Convert applicationId and userId to ForeignKeyReference if needed
159
+ application_id_value = options.applicationId if options else None
160
+ user_id_value = (options.userId if options else None) or jwt_context.get("userId")
161
+
162
+ application_id_ref = _convert_to_foreign_key_reference(application_id_value, "Application")
163
+ user_id_ref = _convert_to_foreign_key_reference(user_id_value, "User")
164
+
165
+ log_entry_data = {
166
+ "timestamp": datetime.utcnow().isoformat(),
167
+ "level": level,
168
+ "environment": "unknown", # Backend extracts from client credentials
169
+ "application": config_client_id, # Use clientId as application identifier
170
+ "applicationId": application_id_ref,
171
+ "message": message,
172
+ "context": masked_context,
173
+ "stackTrace": stack_trace,
174
+ "correlationId": final_correlation_id,
175
+ "userId": user_id_ref,
176
+ "sessionId": (options.sessionId if options else None) or jwt_context.get("sessionId"),
177
+ "requestId": options.requestId if options else None,
178
+ "ipAddress": options.ipAddress if options else None,
179
+ "userAgent": options.userAgent if options else None,
180
+ **env_metadata,
181
+ # Indexed context fields from options
182
+ "sourceKey": options.sourceKey if options else None,
183
+ "sourceDisplayName": options.sourceDisplayName if options else None,
184
+ "externalSystemKey": options.externalSystemKey if options else None,
185
+ "externalSystemDisplayName": options.externalSystemDisplayName if options else None,
186
+ "recordKey": options.recordKey if options else None,
187
+ "recordDisplayName": options.recordDisplayName if options else None,
188
+ # Credential context
189
+ "credentialId": options.credentialId if options else None,
190
+ "credentialType": options.credentialType if options else None,
191
+ # Request metrics
192
+ "requestSize": options.requestSize if options else None,
193
+ "responseSize": options.responseSize if options else None,
194
+ "durationMs": options.durationMs if options else None,
195
+ "durationSeconds": options.durationSeconds if options else None,
196
+ "timeout": options.timeout if options else None,
197
+ "retryCount": options.retryCount if options else None,
198
+ # Error classification
199
+ "errorCategory": options.errorCategory if options else None,
200
+ "httpStatusCategory": options.httpStatusCategory if options else None,
201
+ }
202
+
203
+ # Remove None values
204
+ log_entry_data = {k: v for k, v in log_entry_data.items() if v is not None}
205
+
206
+ return LogEntry(**log_entry_data)
@@ -0,0 +1,70 @@
1
+ """Logging context helpers for extracting indexed fields."""
2
+
3
+ from typing import Any, Dict, Optional, Protocol, runtime_checkable
4
+
5
+
6
+ @runtime_checkable
7
+ class HasKey(Protocol):
8
+ """Protocol for objects with key and displayName."""
9
+
10
+ key: str
11
+ displayName: Optional[str]
12
+
13
+
14
+ @runtime_checkable
15
+ class HasExternalSystem(Protocol):
16
+ """Protocol for objects with key, displayName, and optional externalSystem."""
17
+
18
+ key: str
19
+ displayName: Optional[str]
20
+ externalSystem: Optional[HasKey]
21
+
22
+
23
+ def extract_logging_context(
24
+ source: Optional[HasExternalSystem] = None,
25
+ record: Optional[HasKey] = None,
26
+ external_system: Optional[HasKey] = None,
27
+ ) -> Dict[str, Any]:
28
+ """
29
+ Extract indexed fields for logging.
30
+
31
+ Indexed fields:
32
+ - sourceKey, sourceDisplayName
33
+ - externalSystemKey, externalSystemDisplayName
34
+ - recordKey, recordDisplayName
35
+
36
+ Args:
37
+ source: ExternalDataSource object (optional)
38
+ record: ExternalRecord object (optional)
39
+ external_system: ExternalSystem object (optional)
40
+
41
+ Returns:
42
+ Dictionary with indexed context fields (only non-None values)
43
+
44
+ Design principles:
45
+ - No DB access
46
+ - Explicit context passing
47
+ - Safe to use in hot paths
48
+ """
49
+ context: Dict[str, Any] = {}
50
+
51
+ if source:
52
+ context["sourceKey"] = source.key
53
+ if source.displayName:
54
+ context["sourceDisplayName"] = source.displayName
55
+ if source.externalSystem:
56
+ context["externalSystemKey"] = source.externalSystem.key
57
+ if source.externalSystem.displayName:
58
+ context["externalSystemDisplayName"] = source.externalSystem.displayName
59
+
60
+ if external_system:
61
+ context["externalSystemKey"] = external_system.key
62
+ if external_system.displayName:
63
+ context["externalSystemDisplayName"] = external_system.displayName
64
+
65
+ if record:
66
+ context["recordKey"] = record.key
67
+ if record.displayName:
68
+ context["recordDisplayName"] = record.displayName
69
+
70
+ return context
@@ -0,0 +1,128 @@
1
+ """
2
+ Origin validation utility for CORS security.
3
+
4
+ This module provides utilities for validating request origins against
5
+ a list of allowed origins, with support for wildcard port matching.
6
+ """
7
+
8
+ from typing import Any, Dict, List, Optional
9
+ from urllib.parse import urlparse
10
+
11
+
12
+ def validate_origin(headers: Any, allowed_origins: List[str]) -> Dict[str, Any]:
13
+ """
14
+ Validate request origin against allowed origins list.
15
+
16
+ Checks the 'origin' header first, then falls back to 'referer' header.
17
+ Supports wildcard ports (e.g., 'http://localhost:*' matches any port).
18
+
19
+ Args:
20
+ headers: Request headers dict or object with headers attribute
21
+ allowed_origins: List of allowed origin URLs (supports wildcard ports)
22
+
23
+ Returns:
24
+ Dictionary with:
25
+ - valid: bool - Whether origin is valid
26
+ - error: Optional[str] - Error message if invalid, None if valid
27
+
28
+ Example:
29
+ >>> headers = {"origin": "http://localhost:3000"}
30
+ >>> result = validate_origin(headers, ["http://localhost:*"])
31
+ >>> result["valid"]
32
+ True
33
+ """
34
+ if not allowed_origins:
35
+ # If no allowed origins configured, allow all (backward compatibility)
36
+ return {"valid": True, "error": None}
37
+
38
+ # Extract headers dict from various request object types
39
+ headers_dict: Optional[Dict[str, Any]] = None
40
+ if isinstance(headers, dict):
41
+ headers_dict = headers
42
+ elif hasattr(headers, "headers"):
43
+ # FastAPI, Flask style: request.headers
44
+ headers_obj = getattr(headers, "headers")
45
+ if isinstance(headers_obj, dict):
46
+ headers_dict = headers_obj
47
+ elif hasattr(headers_obj, "get"):
48
+ # Headers object with get method (like Starlette headers)
49
+ headers_dict = dict(headers_obj)
50
+ elif hasattr(headers, "get"):
51
+ # Already a dict-like object
52
+ headers_dict = dict(headers)
53
+
54
+ if headers_dict is None:
55
+ return {"valid": False, "error": "Unable to extract headers from request"}
56
+
57
+ # Extract origin from headers (case-insensitive)
58
+ origin = None
59
+ for key in ["origin", "Origin", "ORIGIN"]:
60
+ if key in headers_dict:
61
+ origin_value = headers_dict[key]
62
+ if isinstance(origin_value, str) and origin_value.strip():
63
+ origin = origin_value.strip()
64
+ break
65
+
66
+ # Fallback to referer header if origin not found
67
+ if not origin:
68
+ for key in ["referer", "Referer", "REFERER", "referrer", "Referrer", "REFERRER"]:
69
+ if key in headers_dict:
70
+ referer_value = headers_dict[key]
71
+ if isinstance(referer_value, str) and referer_value.strip():
72
+ # Extract origin from referer URL
73
+ try:
74
+ parsed = urlparse(referer_value.strip())
75
+ if parsed.scheme and parsed.netloc:
76
+ origin = f"{parsed.scheme}://{parsed.netloc}"
77
+ break
78
+ except Exception:
79
+ pass
80
+
81
+ if not origin:
82
+ return {"valid": False, "error": "No origin or referer header found"}
83
+
84
+ # Normalize origin (remove trailing slash, lowercase scheme/host)
85
+ try:
86
+ parsed_origin = urlparse(origin)
87
+ if not parsed_origin.scheme or not parsed_origin.netloc:
88
+ return {"valid": False, "error": f"Invalid origin format: {origin}"}
89
+ origin_scheme = parsed_origin.scheme.lower()
90
+ origin_netloc = parsed_origin.netloc.lower()
91
+ origin_normalized = f"{origin_scheme}://{origin_netloc}"
92
+ except Exception:
93
+ return {"valid": False, "error": f"Invalid origin format: {origin}"}
94
+
95
+ # Check against allowed origins
96
+ for allowed in allowed_origins:
97
+ if not allowed or not isinstance(allowed, str):
98
+ continue
99
+
100
+ try:
101
+ parsed_allowed = urlparse(allowed)
102
+ allowed_scheme = parsed_allowed.scheme.lower()
103
+ allowed_netloc = parsed_allowed.netloc.lower()
104
+ allowed_normalized = f"{allowed_scheme}://{allowed_netloc}"
105
+
106
+ # Check for exact match
107
+ if origin_normalized == allowed_normalized:
108
+ return {"valid": True, "error": None}
109
+
110
+ # Check for wildcard port match (e.g., localhost:* matches localhost:3000)
111
+ if "*" in allowed_netloc:
112
+ # Extract host from origin
113
+ origin_host = origin_netloc.split(":")[0]
114
+
115
+ # Extract host from allowed (may have wildcard port)
116
+ allowed_host = allowed_netloc.split(":")[0]
117
+ allowed_port = allowed_netloc.split(":")[1] if ":" in allowed_netloc else None
118
+
119
+ # Match if host matches and allowed has wildcard port
120
+ if origin_host == allowed_host and allowed_port == "*":
121
+ if origin_scheme == allowed_scheme:
122
+ return {"valid": True, "error": None}
123
+
124
+ except Exception:
125
+ # Skip invalid allowed origin format
126
+ continue
127
+
128
+ return {"valid": False, "error": f"Origin '{origin}' is not in allowed origins list"}
@@ -0,0 +1,275 @@
1
+ """
2
+ Pagination utilities for MisoClient SDK.
3
+
4
+ This module provides reusable pagination utilities for parsing pagination parameters,
5
+ creating meta objects, and working with paginated responses.
6
+ """
7
+
8
+ from typing import Dict, List, Tuple, TypeVar
9
+
10
+ from ..models.pagination import Meta, PaginatedListResponse
11
+
12
+ T = TypeVar("T")
13
+
14
+
15
+ def parsePaginationParams(params: dict) -> Dict[str, int]:
16
+ """
17
+ Parse query parameters into pagination values.
18
+
19
+ Parses `page` and `page_size` query parameters into `currentPage` and `pageSize`.
20
+ Both are 1-based (page starts at 1).
21
+
22
+ Args:
23
+ params: Dictionary with query parameters (e.g., {'page': '1', 'page_size': '25'})
24
+
25
+ Returns:
26
+ Dictionary with 'currentPage' and 'pageSize' keys (camelCase)
27
+
28
+ Examples:
29
+ >>> parsePaginationParams({'page': '1', 'page_size': '25'})
30
+ {'currentPage': 1, 'pageSize': 25}
31
+ >>> parsePaginationParams({'page': '2'})
32
+ {'currentPage': 2, 'pageSize': 20} # Default pageSize is 20
33
+ """
34
+ # Default values (matching TypeScript default of 20)
35
+ default_page = 1
36
+ default_page_size = 20
37
+
38
+ # Parse page (must be >= 1)
39
+ page_str = params.get("page") or params.get("current_page")
40
+ if page_str is None:
41
+ current_page = default_page
42
+ else:
43
+ try:
44
+ current_page = int(page_str)
45
+ if current_page < 1:
46
+ current_page = default_page
47
+ except (ValueError, TypeError):
48
+ current_page = default_page
49
+
50
+ # Parse page_size (must be >= 1)
51
+ page_size_str = params.get("page_size") or params.get("pageSize")
52
+ if page_size_str is None:
53
+ page_size = default_page_size
54
+ else:
55
+ try:
56
+ page_size = int(page_size_str)
57
+ if page_size < 1:
58
+ page_size = default_page_size
59
+ except (ValueError, TypeError):
60
+ page_size = default_page_size
61
+
62
+ return {"currentPage": current_page, "pageSize": page_size}
63
+
64
+
65
+ # Alias for backward compatibility
66
+ def parse_pagination_params(params: dict) -> Tuple[int, int]:
67
+ """
68
+ Parse query parameters to pagination values (legacy function).
69
+
70
+ Parses `page` and `page_size` query parameters into `current_page` and `page_size`.
71
+ Both are 1-based (page starts at 1).
72
+
73
+ Args:
74
+ params: Dictionary with query parameters (e.g., {'page': '1', 'page_size': '25'})
75
+
76
+ Returns:
77
+ Tuple of (current_page, page_size) as integers
78
+
79
+ Examples:
80
+ >>> parse_pagination_params({'page': '1', 'page_size': '25'})
81
+ (1, 25)
82
+ >>> parse_pagination_params({'page': '2'})
83
+ (2, 20) # Default page_size is 20
84
+ """
85
+ result = parsePaginationParams(params)
86
+ return (result["currentPage"], result["pageSize"])
87
+
88
+
89
+ def createMetaObject(totalItems: int, currentPage: int, pageSize: int, type: str) -> Meta:
90
+ """
91
+ Construct meta object for API response.
92
+
93
+ Args:
94
+ totalItems: Total number of items available in full dataset
95
+ currentPage: Current page index (1-based)
96
+ pageSize: Number of items per page
97
+ type: Logical resource type (e.g., "application", "environment")
98
+
99
+ Returns:
100
+ Meta object
101
+
102
+ Examples:
103
+ >>> meta = createMetaObject(120, 1, 25, 'item')
104
+ >>> meta.totalItems
105
+ 120
106
+ >>> meta.currentPage
107
+ 1
108
+ """
109
+ return Meta(
110
+ totalItems=totalItems,
111
+ currentPage=currentPage,
112
+ pageSize=pageSize,
113
+ type=type,
114
+ )
115
+
116
+
117
+ def create_meta_object(total_items: int, current_page: int, page_size: int, type: str) -> Meta:
118
+ """
119
+ Construct Meta object from pagination parameters.
120
+
121
+ Args:
122
+ total_items: Total number of items across all pages
123
+ current_page: Current page number (1-based)
124
+ page_size: Number of items per page
125
+ type: Resource type identifier (e.g., 'item', 'user', 'group')
126
+
127
+ Returns:
128
+ Meta object with pagination metadata
129
+
130
+ Examples:
131
+ >>> meta = create_meta_object(120, 1, 25, 'item')
132
+ >>> meta.totalItems
133
+ 120
134
+ >>> meta.currentPage
135
+ 1
136
+ """
137
+ return Meta(
138
+ totalItems=total_items,
139
+ currentPage=current_page,
140
+ pageSize=page_size,
141
+ type=type,
142
+ )
143
+
144
+
145
+ def applyPaginationToArray(items: List[T], currentPage: int, pageSize: int) -> List[T]:
146
+ """
147
+ Apply pagination to an array (for mock/testing).
148
+
149
+ Args:
150
+ items: Array of items to paginate
151
+ currentPage: Current page index (1-based)
152
+ pageSize: Number of items per page
153
+
154
+ Returns:
155
+ Paginated slice of the array
156
+
157
+ Examples:
158
+ >>> items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
159
+ >>> applyPaginationToArray(items, 1, 3)
160
+ [1, 2, 3]
161
+ >>> applyPaginationToArray(items, 2, 3)
162
+ [4, 5, 6]
163
+ """
164
+ if not items:
165
+ return []
166
+
167
+ if currentPage < 1:
168
+ currentPage = 1
169
+ if pageSize < 1:
170
+ pageSize = 25
171
+
172
+ # Calculate start and end indices
173
+ start_index = (currentPage - 1) * pageSize
174
+ end_index = start_index + pageSize
175
+
176
+ # Return paginated subset
177
+ return items[start_index:end_index]
178
+
179
+
180
+ def apply_pagination_to_array(items: List[T], current_page: int, page_size: int) -> List[T]:
181
+ """
182
+ Apply pagination to array (for testing/mocks).
183
+
184
+ Args:
185
+ items: Array of items to paginate
186
+ current_page: Current page number (1-based)
187
+ page_size: Number of items per page
188
+
189
+ Returns:
190
+ Paginated subset of items for the specified page
191
+
192
+ Examples:
193
+ >>> items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
194
+ >>> apply_pagination_to_array(items, 1, 3)
195
+ [1, 2, 3]
196
+ >>> apply_pagination_to_array(items, 2, 3)
197
+ [4, 5, 6]
198
+ """
199
+ if not items:
200
+ return []
201
+
202
+ if current_page < 1:
203
+ current_page = 1
204
+ if page_size < 1:
205
+ page_size = 25
206
+
207
+ # Calculate start and end indices
208
+ start_index = (current_page - 1) * page_size
209
+ end_index = start_index + page_size
210
+
211
+ # Return paginated subset
212
+ return items[start_index:end_index]
213
+
214
+
215
+ def createPaginatedListResponse(
216
+ items: List[T],
217
+ totalItems: int,
218
+ currentPage: int,
219
+ pageSize: int,
220
+ type: str,
221
+ ) -> PaginatedListResponse[T]:
222
+ """
223
+ Wrap array + meta into a standard paginated response.
224
+
225
+ Args:
226
+ items: Array of items for the current page
227
+ totalItems: Total number of items available in full dataset
228
+ currentPage: Current page index (1-based)
229
+ pageSize: Number of items per page
230
+ type: Logical resource type (e.g., "application", "environment")
231
+
232
+ Returns:
233
+ PaginatedListResponse object
234
+
235
+ Examples:
236
+ >>> items = [{'id': 1}, {'id': 2}]
237
+ >>> response = createPaginatedListResponse(items, 10, 1, 2, 'item')
238
+ >>> response.meta.totalItems
239
+ 10
240
+ >>> len(response.data)
241
+ 2
242
+ """
243
+ meta = createMetaObject(totalItems, currentPage, pageSize, type)
244
+ return PaginatedListResponse(meta=meta, data=items)
245
+
246
+
247
+ def create_paginated_list_response(
248
+ items: List[T],
249
+ total_items: int,
250
+ current_page: int,
251
+ page_size: int,
252
+ type: str,
253
+ ) -> PaginatedListResponse[T]:
254
+ """
255
+ Wrap array + meta into standard paginated response (legacy function).
256
+
257
+ Args:
258
+ items: Array of items for current page
259
+ total_items: Total number of items across all pages
260
+ current_page: Current page number (1-based)
261
+ page_size: Number of items per page
262
+ type: Resource type identifier (e.g., 'item', 'user', 'group')
263
+
264
+ Returns:
265
+ PaginatedListResponse with meta and data
266
+
267
+ Examples:
268
+ >>> items = [{'id': 1}, {'id': 2}]
269
+ >>> response = create_paginated_list_response(items, 10, 1, 2, 'item')
270
+ >>> response.meta.totalItems
271
+ 10
272
+ >>> len(response.data)
273
+ 2
274
+ """
275
+ return createPaginatedListResponse(items, total_items, current_page, page_size, type)