Skip to main content

auth_framework/sdks/
python.rs

1//! Python SDK generator for enhanced RBAC functionality
2//!
3//! This module provides Python SDK generation with comprehensive
4//! role-system integration and async/await support.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Python SDK configuration
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct PythonSdkConfig {
12    /// Base API URL
13    pub base_url: String,
14    /// API version
15    pub version: String,
16    /// Include RBAC functionality
17    pub include_rbac: bool,
18    /// Include conditional permissions
19    pub include_conditional_permissions: bool,
20    /// Include audit logging
21    pub include_audit: bool,
22    /// Client class name
23    pub client_name: String,
24    /// Enable async/await support
25    pub async_support: bool,
26    /// Include type hints
27    pub type_hints: bool,
28}
29
30impl Default for PythonSdkConfig {
31    fn default() -> Self {
32        Self {
33            base_url: "https://api.example.com".to_string(),
34            version: "v1".to_string(),
35            include_rbac: true,
36            include_conditional_permissions: true,
37            include_audit: true,
38            client_name: "AuthFrameworkClient".to_string(),
39            async_support: true,
40            type_hints: true,
41        }
42    }
43}
44
45/// Python SDK generator
46pub struct PythonSdkGenerator {
47    config: PythonSdkConfig,
48}
49
50impl PythonSdkGenerator {
51    /// Create new Python SDK generator
52    pub fn new(config: PythonSdkConfig) -> Self {
53        Self { config }
54    }
55
56    /// Generate complete Python SDK
57    pub fn generate_sdk(&self) -> Result<HashMap<String, String>, Box<dyn std::error::Error>> {
58        let mut files = HashMap::new();
59
60        // Generate main client
61        files.insert("client.py".to_string(), self.generate_base_client()?);
62
63        // Generate type definitions
64        if self.config.type_hints {
65            files.insert("types.py".to_string(), self.generate_types()?);
66        }
67
68        // Generate RBAC module
69        if self.config.include_rbac {
70            files.insert("rbac.py".to_string(), self.generate_rbac_module()?);
71        }
72
73        // Generate conditional permissions
74        if self.config.include_conditional_permissions {
75            files.insert(
76                "conditional.py".to_string(),
77                self.generate_conditional_module()?,
78            );
79        }
80
81        // Generate audit module
82        if self.config.include_audit {
83            files.insert("audit.py".to_string(), self.generate_audit_module()?);
84        }
85
86        // Generate utilities
87        files.insert("utils.py".to_string(), self.generate_utils()?);
88
89        // Generate main package
90        files.insert("__init__.py".to_string(), self.generate_init()?);
91
92        // Generate setup.py
93        files.insert("setup.py".to_string(), self.generate_setup()?);
94
95        // Generate requirements
96        files.insert(
97            "requirements.txt".to_string(),
98            self.generate_requirements()?,
99        );
100
101        // Generate README
102        files.insert("README.md".to_string(), self.generate_readme()?);
103
104        Ok(files)
105    }
106
107    /// Generate base HTTP client
108    fn generate_base_client(&self) -> Result<String, Box<dyn std::error::Error>> {
109        let client_code = format!(
110            r#"""
111Enhanced {} with RBAC Support
112Generated by AuthFramework SDK Generator
113"""
114
115import asyncio
116import json
117import time
118from typing import Dict, Any, Optional, Union
119{}
120import aiohttp
121import requests
122from urllib.parse import urljoin, urlencode
123
124
125class HttpError(Exception):
126    """HTTP error with status code and response data"""
127
128    def __init__(self, status: int, message: str, data: Optional[Dict[str, Any]] = None):
129        self.status = status
130        self.message = message
131        self.data = data
132        super().__init__(f"HTTP {{status}}: {{message}}")
133
134
135class ApiResponse:
136    """Wrapper for API responses"""
137
138    def __init__(self, success: bool, data: Optional[Any] = None,
139                 error: Optional[str] = None, message: Optional[str] = None):
140        self.success = success
141        self.data = data
142        self.error = error
143        self.message = message
144
145
146class {}:
147    """Enhanced AuthFramework client with comprehensive RBAC support"""
148
149    def __init__(self, base_url: str, access_token: Optional[str] = None,
150                 api_key: Optional[str] = None, timeout: int = 30,
151                 retry_attempts: int = 3):
152        self.base_url = base_url.rstrip('/')
153        self.timeout = timeout
154        self.retry_attempts = retry_attempts
155        self.headers = {{
156            'Content-Type': 'application/json',
157            'User-Agent': 'AuthFramework-Python-SDK/1.0.0'
158        }}
159
160        if access_token:
161            self.headers['Authorization'] = f'Bearer {{access_token}}'
162
163        if api_key:
164            self.headers['X-API-Key'] = api_key
165
166    def set_access_token(self, token: str) -> None:
167        """Set authentication token"""
168        self.headers['Authorization'] = f'Bearer {{token}}'
169
170    def clear_access_token(self) -> None:
171        """Clear authentication token"""
172        self.headers.pop('Authorization', None)
173
174    def set_api_key(self, api_key: str) -> None:
175        """Set API key"""
176        self.headers['X-API-Key'] = api_key
177
178    def _make_request_sync(self, method: str, path: str, data: Optional[Dict] = None,
179                          headers: Optional[Dict[str, str]] = None) -> ApiResponse:
180        """Make synchronous HTTP request with retry logic"""
181        url = urljoin(self.base_url, path)
182        request_headers = {{**self.headers, **(headers or {{}})}}
183
184        last_error = None
185
186        for attempt in range(self.retry_attempts + 1):
187            try:
188                response = requests.request(
189                    method=method,
190                    url=url,
191                    headers=request_headers,
192                    json=data,
193                    timeout=self.timeout
194                )
195
196                response_data = response.json() if response.content else {{}}
197
198                if not response.ok:
199                    raise HttpError(
200                        status=response.status_code,
201                        message=response.reason or 'Request failed',
202                        data=response_data
203                    )
204
205                return ApiResponse(
206                    success=response_data.get('success', True),
207                    data=response_data.get('data'),
208                    error=response_data.get('error'),
209                    message=response_data.get('message')
210                )
211
212            except (requests.RequestException, HttpError) as e:
213                last_error = e
214
215                # Don't retry on client errors (4xx)
216                if isinstance(e, HttpError) and 400 <= e.status < 500:
217                    raise e
218
219                # Wait before retry (exponential backoff)
220                if attempt < self.retry_attempts:
221                    time.sleep(2 ** attempt)
222
223        raise last_error or Exception('Request failed after all retries')
224
225    async def _make_request_async(self, method: str, path: str, data: Optional[Dict] = None,
226                                 headers: Optional[Dict[str, str]] = None) -> ApiResponse:
227        """Make asynchronous HTTP request with retry logic"""
228        url = urljoin(self.base_url, path)
229        request_headers = {{**self.headers, **(headers or {{}})}}
230
231        last_error = None
232
233        for attempt in range(self.retry_attempts + 1):
234            try:
235                timeout = aiohttp.ClientTimeout(total=self.timeout)
236
237                async with aiohttp.ClientSession(timeout=timeout) as session:
238                    async with session.request(
239                        method=method,
240                        url=url,
241                        headers=request_headers,
242                        json=data
243                    ) as response:
244                        response_data = await response.json() if response.content_length else {{}}
245
246                        if not response.ok:
247                            raise HttpError(
248                                status=response.status,
249                                message=response.reason or 'Request failed',
250                                data=response_data
251                            )
252
253                        return ApiResponse(
254                            success=response_data.get('success', True),
255                            data=response_data.get('data'),
256                            error=response_data.get('error'),
257                            message=response_data.get('message')
258                        )
259
260            except (aiohttp.ClientError, HttpError) as e:
261                last_error = e
262
263                # Don't retry on client errors (4xx)
264                if isinstance(e, HttpError) and 400 <= e.status < 500:
265                    raise e
266
267                # Wait before retry (exponential backoff)
268                if attempt < self.retry_attempts:
269                    await asyncio.sleep(2 ** attempt)
270
271        raise last_error or Exception('Request failed after all retries')
272
273    def get(self, path: str, headers: Optional[Dict[str, str]] = None) -> ApiResponse:
274        """Synchronous GET request"""
275        return self._make_request_sync('GET', path, headers=headers)
276
277    def post(self, path: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None) -> ApiResponse:
278        """Synchronous POST request"""
279        return self._make_request_sync('POST', path, data=data, headers=headers)
280
281    def put(self, path: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None) -> ApiResponse:
282        """Synchronous PUT request"""
283        return self._make_request_sync('PUT', path, data=data, headers=headers)
284
285    def delete(self, path: str, headers: Optional[Dict[str, str]] = None) -> ApiResponse:
286        """Synchronous DELETE request"""
287        return self._make_request_sync('DELETE', path, headers=headers)
288
289    async def get_async(self, path: str, headers: Optional[Dict[str, str]] = None) -> ApiResponse:
290        """Asynchronous GET request"""
291        return await self._make_request_async('GET', path, headers=headers)
292
293    async def post_async(self, path: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None) -> ApiResponse:
294        """Asynchronous POST request"""
295        return await self._make_request_async('POST', path, data=data, headers=headers)
296
297    async def put_async(self, path: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None) -> ApiResponse:
298        """Asynchronous PUT request"""
299        return await self._make_request_async('PUT', path, data=data, headers=headers)
300
301    async def delete_async(self, path: str, headers: Optional[Dict[str, str]] = None) -> ApiResponse:
302        """Asynchronous DELETE request"""
303        return await self._make_request_async('DELETE', path, headers=headers)
304"#,
305            self.config.client_name,
306            if self.config.type_hints { ", List" } else { "" },
307            self.config.client_name
308        );
309
310        Ok(client_code)
311    }
312
313    /// Generate Python type definitions
314    fn generate_types(&self) -> Result<String, Box<dyn std::error::Error>> {
315        let types_code = r#"""
316Type definitions for AuthFramework RBAC
317"""
318
319from typing import Dict, List, Optional, Union, Any
320from datetime import datetime
321from dataclasses import dataclass
322from enum import Enum
323
324
325class TimeOfDay(Enum):
326    """Time of day classification"""
327    BUSINESS_HOURS = "business_hours"
328    AFTER_HOURS = "after_hours"
329    WEEKEND = "weekend"
330    HOLIDAY = "holiday"
331
332
333class DayType(Enum):
334    """Day type classification"""
335    WEEKDAY = "weekday"
336    WEEKEND = "weekend"
337    HOLIDAY = "holiday"
338
339
340class DeviceType(Enum):
341    """Device type classification"""
342    DESKTOP = "desktop"
343    MOBILE = "mobile"
344    TABLET = "tablet"
345    UNKNOWN = "unknown"
346
347
348class ConnectionType(Enum):
349    """Connection type classification"""
350    DIRECT = "direct"
351    VPN = "vpn"
352    PROXY = "proxy"
353    TOR = "tor"
354    CORPORATE = "corporate"
355    UNKNOWN = "unknown"
356
357
358class SecurityLevel(Enum):
359    """Security level assessment"""
360    LOW = "low"
361    MEDIUM = "medium"
362    HIGH = "high"
363    CRITICAL = "critical"
364
365
366@dataclass
367class Role:
368    """Role definition"""
369    id: str
370    name: str
371    description: Optional[str] = None
372    parent_id: Optional[str] = None
373    permissions: List[str] = None
374    created_at: Optional[datetime] = None
375    updated_at: Optional[datetime] = None
376
377    def __post_init__(self):
378        if self.permissions is None:
379            self.permissions = []
380
381
382@dataclass
383class Permission:
384    """Permission definition"""
385    id: str
386    action: str
387    resource: str
388    conditions: Optional[Dict[str, str]] = None
389    created_at: Optional[datetime] = None
390
391
392@dataclass
393class UserRole:
394    """User role assignment"""
395    role_id: str
396    role_name: str
397    assigned_at: datetime
398    assigned_by: Optional[str] = None
399    expires_at: Optional[datetime] = None
400
401
402@dataclass
403class UserRolesResponse:
404    """User roles and effective permissions"""
405    user_id: str
406    roles: List[UserRole]
407    effective_permissions: List[str]
408
409
410@dataclass
411class CreateRoleRequest:
412    """Request to create a new role"""
413    name: str
414    description: Optional[str] = None
415    parent_id: Optional[str] = None
416    permissions: Optional[List[str]] = None
417
418
419@dataclass
420class UpdateRoleRequest:
421    """Request to update an existing role"""
422    name: Optional[str] = None
423    description: Optional[str] = None
424    parent_id: Optional[str] = None
425
426
427@dataclass
428class AssignRoleRequest:
429    """Request to assign a role to a user"""
430    role_id: str
431    expires_at: Optional[datetime] = None
432    reason: Optional[str] = None
433
434
435@dataclass
436class BulkAssignment:
437    """Single assignment in bulk operation"""
438    user_id: str
439    role_id: str
440    expires_at: Optional[datetime] = None
441
442
443@dataclass
444class BulkAssignRequest:
445    """Request for bulk role assignment"""
446    assignments: List[BulkAssignment]
447
448
449@dataclass
450class ElevateRoleRequest:
451    """Request for role elevation"""
452    target_role: str
453    duration_minutes: Optional[int] = None
454    justification: str = ""
455
456
457@dataclass
458class PermissionCheckRequest:
459    """Request to check permissions"""
460    action: str
461    resource: str
462    context: Optional[Dict[str, str]] = None
463
464
465@dataclass
466class PermissionCheckResponse:
467    """Response from permission check"""
468    granted: bool
469    reason: str
470    required_roles: List[str]
471    missing_permissions: List[str]
472
473
474@dataclass
475class AuditEntry:
476    """Audit log entry"""
477    id: str
478    user_id: Optional[str]
479    action: str
480    resource: Optional[str]
481    result: str
482    context: Dict[str, str]
483    timestamp: datetime
484
485
486@dataclass
487class AuditLogResponse:
488    """Response containing audit logs"""
489    entries: List[AuditEntry]
490    total_count: int
491    page: int
492    per_page: int
493
494
495@dataclass
496class AuditQuery:
497    """Query parameters for audit logs"""
498    user_id: Optional[str] = None
499    action: Optional[str] = None
500    resource: Optional[str] = None
501    start_time: Optional[datetime] = None
502    end_time: Optional[datetime] = None
503    page: Optional[int] = None
504    per_page: Optional[int] = None
505
506
507@dataclass
508class ConditionalContext:
509    """Context for conditional permissions"""
510    time_of_day: Optional[TimeOfDay] = None
511    day_type: Optional[DayType] = None
512    device_type: Optional[DeviceType] = None
513    connection_type: Optional[ConnectionType] = None
514    security_level: Optional[SecurityLevel] = None
515    risk_score: Optional[int] = None
516    ip_address: Optional[str] = None
517    user_agent: Optional[str] = None
518    custom_attributes: Optional[Dict[str, str]] = None
519
520
521@dataclass
522class RoleListQuery:
523    """Query parameters for listing roles"""
524    page: Optional[int] = None
525    per_page: Optional[int] = None
526    parent_id: Optional[str] = None
527    include_permissions: Optional[bool] = None
528"#;
529
530        Ok(types_code.to_string())
531    }
532
533    /// Generate RBAC module
534    fn generate_rbac_module(&self) -> Result<String, Box<dyn std::error::Error>> {
535        let rbac_code = format!(
536            r#"""
537RBAC (Role-Based Access Control) Module
538Provides comprehensive role and permission management
539"""
540
541from typing import Dict, List, Optional, Union
542{}
543from .types import (
544    Role, CreateRoleRequest, UpdateRoleRequest, UserRolesResponse,
545    AssignRoleRequest, BulkAssignRequest, ElevateRoleRequest,
546    PermissionCheckRequest, PermissionCheckResponse, RoleListQuery
547)
548from .client import ApiResponse
549
550
551class RbacManager:
552    """RBAC management client"""
553
554    def __init__(self, client):
555        self.client = client
556
557    # ============================================================================
558    # ROLE MANAGEMENT
559    # ============================================================================
560
561    def create_role(self, request: CreateRoleRequest) -> ApiResponse:
562        """Create a new role"""
563        data = {{
564            'name': request.name,
565            'description': request.description,
566            'parent_id': request.parent_id,
567            'permissions': request.permissions
568        }}
569        return self.client.post('/{}/rbac/roles', data)
570
571    def get_role(self, role_id: str) -> ApiResponse:
572        """Get role by ID"""
573        return self.client.get(f'/{}/rbac/roles/{{role_id}}')
574
575    def list_roles(self, query: Optional[RoleListQuery] = None) -> ApiResponse:
576        """List all roles with pagination"""
577        path = '/{}/rbac/roles'
578
579        if query:
580            params = []
581            if query.page is not None:
582                params.append(f'page={{query.page}}')
583            if query.per_page is not None:
584                params.append(f'per_page={{query.per_page}}')
585            if query.parent_id is not None:
586                params.append(f'parent_id={{query.parent_id}}')
587            if query.include_permissions is not None:
588                params.append(f'include_permissions={{str(query.include_permissions).lower()}}')
589
590            if params:
591                path += '?' + '&'.join(params)
592
593        return self.client.get(path)
594
595    def update_role(self, role_id: str, request: UpdateRoleRequest) -> ApiResponse:
596        """Update an existing role"""
597        data = {{}}
598        if request.name is not None:
599            data['name'] = request.name
600        if request.description is not None:
601            data['description'] = request.description
602        if request.parent_id is not None:
603            data['parent_id'] = request.parent_id
604
605        return self.client.put(f'/{}/rbac/roles/{{role_id}}', data)
606
607    def delete_role(self, role_id: str) -> ApiResponse:
608        """Delete a role"""
609        return self.client.delete(f'/{}/rbac/roles/{{role_id}}')
610
611    # ============================================================================
612    # USER ROLE ASSIGNMENTS
613    # ============================================================================
614
615    def assign_user_role(self, user_id: str, request: AssignRoleRequest) -> ApiResponse:
616        """Assign role to user"""
617        data = {{
618            'role_id': request.role_id,
619            'expires_at': request.expires_at.isoformat() if request.expires_at else None,
620            'reason': request.reason
621        }}
622        return self.client.post(f'/{}/rbac/users/{{user_id}}/roles', data)
623
624    def revoke_user_role(self, user_id: str, role_id: str) -> ApiResponse:
625        """Revoke role from user"""
626        return self.client.delete(f'/{}/rbac/users/{{user_id}}/roles/{{role_id}}')
627
628    def get_user_roles(self, user_id: str) -> ApiResponse:
629        """Get user's roles and effective permissions"""
630        return self.client.get(f'/{}/rbac/users/{{user_id}}/roles')
631
632    def bulk_assign_roles(self, request: BulkAssignRequest) -> ApiResponse:
633        """Bulk assign roles to multiple users"""
634        data = {{
635            'assignments': [
636                {{
637                    'user_id': assignment.user_id,
638                    'role_id': assignment.role_id,
639                    'expires_at': assignment.expires_at.isoformat() if assignment.expires_at else None
640                }}
641                for assignment in request.assignments
642            ]
643        }}
644        return self.client.post('/{}/rbac/bulk/assign', data)
645
646    # ============================================================================
647    # PERMISSION CHECKING
648    # ============================================================================
649
650    def check_permission(self, request: PermissionCheckRequest) -> ApiResponse:
651        """Check if current user has permission"""
652        data = {{
653            'action': request.action,
654            'resource': request.resource,
655            'context': request.context
656        }}
657        return self.client.post('/{}/rbac/check-permission', data)
658
659    def has_permission(self, action: str, resource: str, context: Optional[Dict[str, str]] = None) -> bool:
660        """Quick permission check (returns boolean)"""
661        try:
662            request = PermissionCheckRequest(action=action, resource=resource, context=context)
663            response = self.check_permission(request)
664            return response.data and response.data.get('granted', False)
665        except Exception:
666            return False
667
668    def elevate_role(self, request: ElevateRoleRequest) -> ApiResponse:
669        """Request role elevation"""
670        data = {{
671            'target_role': request.target_role,
672            'duration_minutes': request.duration_minutes,
673            'justification': request.justification
674        }}
675        return self.client.post('/{}/rbac/elevate', data)
676
677    # ============================================================================
678    # ASYNC METHODS
679    # ============================================================================
680
681    async def create_role_async(self, request: CreateRoleRequest) -> ApiResponse:
682        """Create a new role (async)"""
683        data = {{
684            'name': request.name,
685            'description': request.description,
686            'parent_id': request.parent_id,
687            'permissions': request.permissions
688        }}
689        return await self.client.post_async('/{}/rbac/roles', data)
690
691    async def get_role_async(self, role_id: str) -> ApiResponse:
692        """Get role by ID (async)"""
693        return await self.client.get_async(f'/{}/rbac/roles/{{role_id}}')
694
695    async def has_permission_async(self, action: str, resource: str, context: Optional[Dict[str, str]] = None) -> bool:
696        """Quick permission check (async, returns boolean)"""
697        try:
698            request = PermissionCheckRequest(action=action, resource=resource, context=context)
699            data = {{
700                'action': request.action,
701                'resource': request.resource,
702                'context': request.context
703            }}
704            response = await self.client.post_async('/{}/rbac/check-permission', data)
705            return response.data and response.data.get('granted', False)
706        except Exception:
707            return False
708
709    # ============================================================================
710    # CONVENIENCE METHODS
711    # ============================================================================
712
713    def user_has_any_role(self, user_id: str, role_names: List[str]) -> bool:
714        """Check if user has any of the specified roles"""
715        try:
716            response = self.get_user_roles(user_id)
717            if not response.data:
718                return False
719
720            user_role_names = [role['role_name'] for role in response.data.get('roles', [])]
721            return any(role in user_role_names for role in role_names)
722        except Exception:
723            return False
724
725    def user_has_all_roles(self, user_id: str, role_names: List[str]) -> bool:
726        """Check if user has all of the specified roles"""
727        try:
728            response = self.get_user_roles(user_id)
729            if not response.data:
730                return False
731
732            user_role_names = [role['role_name'] for role in response.data.get('roles', [])]
733            return all(role in user_role_names for role in role_names)
734        except Exception:
735            return False
736
737    def get_role_hierarchy(self) -> Dict[str, List[str]]:
738        """Get role hierarchy (parent-child relationships)"""
739        try:
740            response = self.list_roles(RoleListQuery(include_permissions=False))
741            if not response.data:
742                return {{}}
743
744            hierarchy = {{}}
745
746            for role in response.data:
747                parent_id = role.get('parent_id')
748                if parent_id:
749                    if parent_id not in hierarchy:
750                        hierarchy[parent_id] = []
751                    hierarchy[parent_id].append(role['id'])
752
753            return hierarchy
754        except Exception:
755            return {{}}
756
757    def get_child_roles(self, parent_role_id: str) -> List[Role]:
758        """Get all child roles for a given parent role"""
759        try:
760            response = self.list_roles(RoleListQuery(parent_id=parent_role_id))
761            return response.data or []
762        except Exception:
763            return []
764"#,
765            "", // No additional imports needed between typing and .types import groups
766            self.config.version,
767            self.config.version,
768            self.config.version,
769            self.config.version,
770            self.config.version,
771            self.config.version,
772            self.config.version,
773            self.config.version,
774            self.config.version,
775            self.config.version,
776            self.config.version,
777            self.config.version,
778            self.config.version,
779            self.config.version
780        );
781
782        Ok(rbac_code)
783    }
784
785    /// Generate other modules (conditional, audit, utils)
786    fn generate_conditional_module(&self) -> Result<String, Box<dyn std::error::Error>> {
787        Ok(format!(
788            r#""""Conditional permission helpers for auth-framework Python SDK v{version}."""
789
790from __future__ import annotations
791from typing import Any, Callable, List, Optional
792
793
794class ConditionalPermission:
795    """A permission that is only granted when a runtime condition is satisfied."""
796
797    def __init__(self, permission: str, condition: Callable[..., bool]) -> None:
798        self.permission = permission
799        self._condition = condition
800
801    def is_granted(self, **context: Any) -> bool:
802        """Return True only when the attached condition evaluates to True."""
803        return self._condition(**context)
804
805
806class PermissionGate:
807    """Evaluates a list of :class:`ConditionalPermission` items against a context."""
808
809    def __init__(self, permissions: Optional[List[ConditionalPermission]] = None) -> None:
810        self._permissions: List[ConditionalPermission] = permissions or []
811
812    def add(self, cp: ConditionalPermission) -> None:
813        self._permissions.append(cp)
814
815    def evaluate(self, **context: Any) -> List[str]:
816        """Return the names of all permissions granted in the given context."""
817        return [
818            cp.permission for cp in self._permissions if cp.is_granted(**context)
819        ]
820
821    def has(self, permission: str, **context: Any) -> bool:
822        """Return True if *permission* is granted in *context*."""
823        return permission in self.evaluate(**context)
824"#,
825            version = self.config.version
826        ))
827    }
828
829    fn generate_audit_module(&self) -> Result<String, Box<dyn std::error::Error>> {
830        Ok(format!(
831            r#"""Audit log client for auth-framework Python SDK v{version}."""
832
833from __future__ import annotations
834import time
835from dataclasses import dataclass, field
836from typing import Any, Dict, List, Optional
837
838
839@dataclass
840class AuditEvent:
841    """Represents a single audit log entry."""
842
843    event_type: str
844    user_id: str
845    resource: str
846    outcome: str
847    timestamp: float = field(default_factory=time.time)
848    metadata: Dict[str, Any] = field(default_factory=dict)
849
850
851class AuditLog:
852    """Lightweight in-process audit log buffer."""
853
854    def __init__(self, max_size: int = 10_000) -> None:
855        self._events: List[AuditEvent] = []
856        self._max_size = max_size
857
858    def record(self, event: AuditEvent) -> None:
859        if len(self._events) >= self._max_size:
860            self._events.pop(0)
861        self._events.append(event)
862
863    def query(
864        self,
865        *,
866        user_id: Optional[str] = None,
867        event_type: Optional[str] = None,
868        outcome: Optional[str] = None,
869    ) -> List[AuditEvent]:
870        results = self._events
871        if user_id is not None:
872            results = [e for e in results if e.user_id == user_id]
873        if event_type is not None:
874            results = [e for e in results if e.event_type == event_type]
875        if outcome is not None:
876            results = [e for e in results if e.outcome == outcome]
877        return results
878
879    def clear(self) -> None:
880        self._events.clear()
881"#,
882            version = self.config.version
883        ))
884    }
885
886    fn generate_utils(&self) -> Result<String, Box<dyn std::error::Error>> {
887        Ok(format!(
888            r#"""Utility helpers for auth-framework Python SDK v{version}."""
889
890from __future__ import annotations
891import base64
892import time
893from typing import Any, Dict, Optional
894
895
896def decode_jwt_payload(token: str) -> Dict[str, Any]:
897    """Decode JWT payload without verifying the signature.
898
899    .. warning::
900        This does **not** verify the token. Use the server-side validation
901        endpoints for security-sensitive operations.
902    """
903    try:
904        parts = token.split(".")
905        if len(parts) < 2:
906            raise ValueError("Not a valid JWT")
907        padding = "=" * (4 - len(parts[1]) % 4)
908        import json
909        return json.loads(base64.urlsafe_b64decode(parts[1] + padding))
910    except Exception as exc:
911        raise ValueError(f"Failed to decode JWT payload: {{exc}}") from exc
912
913
914def is_token_expired(token: str, leeway: int = 0) -> bool:
915    """Return True if the JWT *exp* claim is in the past."""
916    try:
917        payload = decode_jwt_payload(token)
918        exp: Optional[int] = payload.get("exp")
919        if exp is None:
920            return False
921        return time.time() > (exp + leeway)
922    except ValueError:
923        return True
924
925
926def constant_time_compare(a: str, b: str) -> bool:
927    """Compare two strings in constant time to prevent timing attacks."""
928    if len(a) != len(b):
929        return False
930    result = 0
931    for x, y in zip(a, b):
932        result |= ord(x) ^ ord(y)
933    return result == 0
934"#,
935            version = self.config.version
936        ))
937    }
938
939    fn generate_init(&self) -> Result<String, Box<dyn std::error::Error>> {
940        Ok(format!(
941            r#"""auth-framework Python SDK v{version}.
942
943Usage::
944
945    from auth_framework import AuthClient
946
947    client = AuthClient(base_url="https://auth.example.com", api_key="...")
948    token = client.authenticate(username="alice", password="secret")
949"""
950
951from .client import AuthClient
952from .models import AuthToken, UserInfo
953from .exceptions import AuthError, TokenExpiredError, PermissionDeniedError
954
955__all__ = [
956    "AuthClient",
957    "AuthToken",
958    "UserInfo",
959    "AuthError",
960    "TokenExpiredError",
961    "PermissionDeniedError",
962]
963
964__version__ = "{version}"
965"#,
966            version = self.config.version
967        ))
968    }
969
970    fn generate_setup(&self) -> Result<String, Box<dyn std::error::Error>> {
971        Ok(format!(
972            r#""""Setup configuration for auth-framework Python SDK."""
973from setuptools import setup, find_packages
974
975setup(
976    name="auth-framework-sdk",
977    version="{version}",
978    description="Python SDK for auth-framework authentication and authorization",
979    long_description=open("README.md").read(),
980    long_description_content_type="text/markdown",
981    python_requires=">=3.9",
982    packages=find_packages(),
983    install_requires=[
984        "aiohttp>=3.8.0",
985        "requests>=2.28.0",
986    ],
987    extras_require={{
988        "async": ["aiohttp>=3.8.0"],
989    }},
990    classifiers=[
991        "Programming Language :: Python :: 3",
992        "License :: OSI Approved :: MIT License",
993        "Topic :: Security",
994    ],
995)
996"#,
997            version = self.config.version,
998        ))
999    }
1000
1001    fn generate_requirements(&self) -> Result<String, Box<dyn std::error::Error>> {
1002        Ok("aiohttp>=3.8.0\nrequests>=2.28.0".to_string())
1003    }
1004
1005    fn generate_readme(&self) -> Result<String, Box<dyn std::error::Error>> {
1006        Ok(format!(
1007            r#"# auth-framework-sdk — AuthFramework Python SDK v{version}
1008
1009A Python SDK for integrating with the AuthFramework authentication and authorization server.
1010
1011## Installation
1012
1013```bash
1014pip install auth-framework-sdk
1015```
1016
1017## Quick Start
1018
1019```python
1020from auth_framework import {client_name}
1021
1022client = {client_name}(
1023    base_url="{base_url}",
1024    api_key="your-api-key",
1025)
1026
1027# Authenticate a user
1028token = client.authenticate(username="alice", password="secret")
1029print(token.access_token)
1030
1031# Validate a token
1032user = client.validate_token(token.access_token)
1033print(user.user_id)
1034```
1035
1036## Features
1037
1038- **Token Authentication**: Username/password, API keys, OAuth 2.0
1039- **RBAC**: Role-based access control with permission checks
1040- **MFA**: Multi-factor authentication (TOTP, Email, WebAuthn)
1041- **Audit Logging**: Built-in audit log client
1042- **Async Support**: Full async/await support via `aiohttp`
1043
1044## License
1045
1046MIT
1047"#,
1048            version = self.config.version,
1049            client_name = self.config.client_name,
1050            base_url = self.config.base_url,
1051        ))
1052    }
1053}
1054
1055#[cfg(test)]
1056mod tests {
1057    use super::*;
1058
1059    #[test]
1060    fn test_python_sdk_config_default() {
1061        let config = PythonSdkConfig::default();
1062        assert_eq!(config.base_url, "https://api.example.com");
1063        assert_eq!(config.version, "v1");
1064        assert!(config.include_rbac);
1065        assert!(config.include_conditional_permissions);
1066        assert!(config.include_audit);
1067        assert_eq!(config.client_name, "AuthFrameworkClient");
1068        assert!(config.async_support);
1069        assert!(config.type_hints);
1070    }
1071
1072    #[test]
1073    fn test_generate_sdk_default_config() {
1074        let generator = PythonSdkGenerator::new(PythonSdkConfig::default());
1075        let files = generator.generate_sdk().unwrap();
1076        // Default config enables all modules
1077        assert!(files.contains_key("client.py"));
1078        assert!(files.contains_key("types.py"));
1079        assert!(files.contains_key("rbac.py"));
1080        assert!(files.contains_key("conditional.py"));
1081        assert!(files.contains_key("audit.py"));
1082        assert!(files.contains_key("utils.py"));
1083        assert!(files.contains_key("__init__.py"));
1084        assert!(files.contains_key("setup.py"));
1085        assert!(files.contains_key("requirements.txt"));
1086        assert!(files.contains_key("README.md"));
1087    }
1088
1089    #[test]
1090    fn test_generate_sdk_without_optional_modules() {
1091        let config = PythonSdkConfig {
1092            include_rbac: false,
1093            include_conditional_permissions: false,
1094            include_audit: false,
1095            type_hints: false,
1096            ..PythonSdkConfig::default()
1097        };
1098        let generator = PythonSdkGenerator::new(config);
1099        let files = generator.generate_sdk().unwrap();
1100        assert!(files.contains_key("client.py"));
1101        assert!(!files.contains_key("types.py"));
1102        assert!(!files.contains_key("rbac.py"));
1103        assert!(!files.contains_key("conditional.py"));
1104        assert!(!files.contains_key("audit.py"));
1105    }
1106
1107    #[test]
1108    fn test_generated_client_contains_class_name() {
1109        let config = PythonSdkConfig {
1110            client_name: "MyClient".to_string(),
1111            ..PythonSdkConfig::default()
1112        };
1113        let generator = PythonSdkGenerator::new(config);
1114        let files = generator.generate_sdk().unwrap();
1115        let client_code = &files["client.py"];
1116        assert!(client_code.contains("MyClient"));
1117    }
1118}