ud-resolver 1.1.0__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 (52) hide show
  1. backend/__init__.py +20 -0
  2. backend/api/__init__.py +110 -0
  3. backend/api/auth.py +433 -0
  4. backend/api/dependencies.py +40 -0
  5. backend/api/exceptions.py +80 -0
  6. backend/api/main.py +434 -0
  7. backend/api/middleware.py +597 -0
  8. backend/api/routes/__init__.py +45 -0
  9. backend/api/routes/auth.py +297 -0
  10. backend/api/routes/packages.py +1145 -0
  11. backend/api/routes/scan.py +184 -0
  12. backend/api/routes/system.py +1692 -0
  13. backend/api/schemas.py +37 -0
  14. backend/cli.py +657 -0
  15. backend/core/__init__.py +5 -0
  16. backend/core/cache.py +385 -0
  17. backend/core/conflict_resolver.py +1185 -0
  18. backend/core/data_aggregator.py +1221 -0
  19. backend/core/export_generator.py +510 -0
  20. backend/core/system_scanner.py +2641 -0
  21. backend/core/utils.py +162 -0
  22. backend/data_sources/__init__.py +35 -0
  23. backend/data_sources/apk_client.py +354 -0
  24. backend/data_sources/apt_client.py +338 -0
  25. backend/data_sources/base_client.py +212 -0
  26. backend/data_sources/cocoapods_client.py +263 -0
  27. backend/data_sources/conda_client.py +627 -0
  28. backend/data_sources/crates_client.py +752 -0
  29. backend/data_sources/documentation_scraper.py +1044 -0
  30. backend/data_sources/gomodules_client.py +297 -0
  31. backend/data_sources/homebrew_client.py +532 -0
  32. backend/data_sources/maven_client.py +1561 -0
  33. backend/data_sources/npm_client.py +1099 -0
  34. backend/data_sources/nuget_client.py +666 -0
  35. backend/data_sources/packagist_client.py +534 -0
  36. backend/data_sources/pypi_client.py +877 -0
  37. backend/data_sources/rubygems_client.py +527 -0
  38. backend/data_sources/utils.py +14 -0
  39. backend/database/__init__.py +28 -0
  40. backend/database/compatibility_db.py +874 -0
  41. backend/database/models.py +440 -0
  42. backend/logging_config.py +60 -0
  43. backend/manifest_detector.py +401 -0
  44. backend/settings.py +831 -0
  45. backend/tracing_config.py +192 -0
  46. backend/utils/errors.py +173 -0
  47. ud_resolver-1.1.0.dist-info/METADATA +346 -0
  48. ud_resolver-1.1.0.dist-info/RECORD +52 -0
  49. ud_resolver-1.1.0.dist-info/WHEEL +5 -0
  50. ud_resolver-1.1.0.dist-info/entry_points.txt +2 -0
  51. ud_resolver-1.1.0.dist-info/licenses/LICENSE +21 -0
  52. ud_resolver-1.1.0.dist-info/top_level.txt +1 -0
backend/__init__.py ADDED
@@ -0,0 +1,20 @@
1
+ # backend/__init__.py
2
+ """
3
+ Universal Dependency Resolver Backend Package
4
+ """
5
+
6
+ from .settings import get_ecosystem_config
7
+ from .core import DataAggregator, ConflictResolver, SystemScanner, ExportGenerator
8
+ from .manifest_detector import ManifestDetector
9
+
10
+ __all__ = [
11
+ "get_ecosystem_config",
12
+ "DataAggregator",
13
+ "ConflictResolver",
14
+ "SystemScanner",
15
+ "ExportGenerator",
16
+ "ManifestDetector",
17
+ ]
18
+
19
+ import logging
20
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
@@ -0,0 +1,110 @@
1
+ # backend/api/__init__.py
2
+ """
3
+ Universal Dependency Resolver API Package
4
+
5
+ This package contains all API-related components including routes,
6
+ middleware, authentication, and exception handling.
7
+ """
8
+
9
+ # Import main app instance
10
+ from .main import app
11
+
12
+ # Import routers
13
+ from .routes import packages, system
14
+
15
+ # Import middleware
16
+ from .middleware import (
17
+ CorrelationIDMiddleware,
18
+ LoggingMiddleware,
19
+ PerformanceMiddleware,
20
+ CompressionMiddleware,
21
+ SecurityHeadersMiddleware,
22
+ RequestSizeLimitMiddleware,
23
+ CacheMiddleware,
24
+ MetricsMiddleware,
25
+ MaintenanceModeMiddleware,
26
+ AuditLogMiddleware,
27
+ CSRFProtectionMiddleware,
28
+ setup_middleware,
29
+ get_client_ip,
30
+ get_user_agent,
31
+ )
32
+
33
+ # Import exceptions
34
+ from .exceptions import (
35
+ DependencyResolverError,
36
+ ValidationError,
37
+ PackageNotFoundError,
38
+ EcosystemNotSupportedError,
39
+ ConflictResolutionError,
40
+ RateLimitExceededError,
41
+ )
42
+
43
+ # Import auth components
44
+ from .auth import (
45
+ # Models
46
+ Token,
47
+ TokenData,
48
+ UserCreate,
49
+ UserLogin,
50
+ APIKeyCreate,
51
+ # Functions
52
+ verify_password,
53
+ get_password_hash,
54
+ create_access_token,
55
+ create_refresh_token,
56
+ generate_api_key,
57
+ get_current_user,
58
+ get_current_active_user,
59
+ require_scopes,
60
+ # Service
61
+ AuthService,
62
+ # OAuth2
63
+ oauth2_scheme,
64
+ login_for_access_token,
65
+ )
66
+
67
+ # Version info
68
+ __version__ = "1.0.0"
69
+ __author__ = "Universal Dependency Resolver Team"
70
+
71
+ # Export main components
72
+ __all__ = [
73
+ # App
74
+ "app",
75
+ # Routers
76
+ "packages",
77
+ "system",
78
+ # Middleware
79
+ "setup_middleware",
80
+ "CorrelationIDMiddleware",
81
+ "LoggingMiddleware",
82
+ "PerformanceMiddleware",
83
+ "CompressionMiddleware",
84
+ "SecurityHeadersMiddleware",
85
+ "RequestSizeLimitMiddleware",
86
+ "CacheMiddleware",
87
+ "MetricsMiddleware",
88
+ "MaintenanceModeMiddleware",
89
+ "AuditLogMiddleware",
90
+ "CSRFProtectionMiddleware",
91
+ # Exceptions
92
+ "DependencyResolverError",
93
+ "ValidationError",
94
+ "PackageNotFoundError",
95
+ "EcosystemNotSupportedError",
96
+ "ConflictResolutionError",
97
+ "RateLimitExceededError",
98
+ # Auth
99
+ "Token",
100
+ "UserCreate",
101
+ "UserLogin",
102
+ "APIKeyCreate",
103
+ "AuthService",
104
+ "get_current_user",
105
+ "get_current_active_user",
106
+ "require_scopes",
107
+ # Utilities
108
+ "get_client_ip",
109
+ "get_user_agent",
110
+ ]
backend/api/auth.py ADDED
@@ -0,0 +1,433 @@
1
+ # backend/api/auth.py
2
+ from datetime import datetime, timedelta
3
+ from typing import Optional, Dict, List, Any
4
+ from fastapi import Depends, HTTPException, status, Request
5
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials, APIKeyHeader
6
+ from jose import JWTError, jwt
7
+ from passlib.context import CryptContext
8
+ from pydantic import BaseModel, EmailStr, Field
9
+ import secrets
10
+ import logging
11
+ from typing import Optional
12
+ from datetime import datetime
13
+
14
+ from backend.settings import (
15
+ SECRET_KEY,
16
+ ALGORITHM,
17
+ ACCESS_TOKEN_EXPIRE_MINUTES,
18
+ REFRESH_TOKEN_EXPIRE_DAYS,
19
+ API_KEY_HEADER,
20
+ ENABLE_API_KEY_AUTH,
21
+ FEATURES,
22
+ )
23
+ from backend.database.models import User, APIKey, db_session
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # Password hashing
28
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
29
+
30
+ # Security schemes
31
+ bearer_scheme = HTTPBearer(auto_error=False)
32
+ api_key_header = APIKeyHeader(name=API_KEY_HEADER, auto_error=False)
33
+
34
+
35
+ # Pydantic models
36
+ class Token(BaseModel):
37
+ access_token: str
38
+ refresh_token: Optional[str] = None
39
+ token_type: str = "bearer"
40
+ expires_in: int = Field(default=ACCESS_TOKEN_EXPIRE_MINUTES * 60)
41
+
42
+
43
+ class TokenData(BaseModel):
44
+ username: Optional[str] = None
45
+ user_id: Optional[int] = None
46
+ scopes: List[str] = []
47
+
48
+
49
+ class UserCreate(BaseModel):
50
+ username: str = Field(..., min_length=3, max_length=50)
51
+ email: EmailStr
52
+ password: str = Field(..., min_length=8)
53
+ full_name: Optional[str] = None
54
+
55
+
56
+ class UserLogin(BaseModel):
57
+ username: str
58
+ password: str
59
+
60
+
61
+ class APIKeyCreate(BaseModel):
62
+ name: str = Field(..., min_length=3, max_length=100)
63
+ description: Optional[str] = None
64
+ scopes: List[str] = Field(default_factory=list)
65
+ expires_at: Optional[datetime] = None
66
+
67
+
68
+ # Authentication functions
69
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
70
+ """Verify a password against its hash"""
71
+ return pwd_context.verify(plain_password, hashed_password)
72
+
73
+
74
+ def get_password_hash(password: str) -> str:
75
+ """Hash a password"""
76
+ return pwd_context.hash(password)
77
+
78
+
79
+ def create_access_token(
80
+ data: Dict[str, Any], expires_delta: Optional[timedelta] = None
81
+ ) -> str:
82
+ """Create a JWT access token"""
83
+ to_encode = data.copy()
84
+ if expires_delta:
85
+ expire = datetime.utcnow() + expires_delta
86
+ else:
87
+ expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
88
+
89
+ to_encode.update({"exp": expire, "type": "access"})
90
+ encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
91
+ return encoded_jwt
92
+
93
+
94
+ def create_refresh_token(data: Dict[str, Any]) -> str:
95
+ """Create a JWT refresh token"""
96
+ to_encode = data.copy()
97
+ expire = datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
98
+ to_encode.update({"exp": expire, "type": "refresh"})
99
+ encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
100
+ return encoded_jwt
101
+
102
+
103
+ def generate_api_key() -> str:
104
+ """Generate a secure API key"""
105
+ return f"udr_{secrets.token_urlsafe(32)}"
106
+
107
+
108
+ async def get_current_user_from_token(token: str) -> Optional[User]:
109
+ """Extract and validate user from JWT token"""
110
+ credentials_exception = HTTPException(
111
+ status_code=status.HTTP_401_UNAUTHORIZED,
112
+ detail="Could not validate credentials",
113
+ headers={"WWW-Authenticate": "Bearer"},
114
+ )
115
+
116
+ try:
117
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
118
+ username: str = payload.get("sub")
119
+ token_type: str = payload.get("type")
120
+
121
+ if username is None or token_type != "access":
122
+ raise credentials_exception
123
+
124
+ token_data = TokenData(username=username, scopes=payload.get("scopes", []))
125
+ except JWTError:
126
+ raise credentials_exception
127
+
128
+ # Get user from database
129
+ with db_session() as db:
130
+ user = db.query(User).filter(User.username == token_data.username).first()
131
+ if user is None:
132
+ raise credentials_exception
133
+
134
+ return user
135
+
136
+
137
+ async def get_current_user_from_api_key(api_key: str) -> Optional[User]:
138
+ """Validate API key and return associated user"""
139
+ if not api_key:
140
+ return None
141
+
142
+ with db_session() as db:
143
+ key_record = (
144
+ db.query(APIKey)
145
+ .filter(APIKey.key == api_key, APIKey.is_active == True)
146
+ .first()
147
+ )
148
+
149
+ if not key_record:
150
+ return None
151
+
152
+ # Check expiration
153
+ if key_record.expires_at and key_record.expires_at < datetime.utcnow():
154
+ return None
155
+
156
+ # Update last used timestamp
157
+ key_record.last_used_at = datetime.utcnow()
158
+ key_record.usage_count += 1
159
+ db.commit()
160
+
161
+ return key_record.user
162
+
163
+
164
+ # Dependency functions
165
+ async def get_current_user(
166
+ request: Request,
167
+ bearer_token: Optional[HTTPAuthorizationCredentials] = Depends(bearer_scheme),
168
+ api_key: Optional[str] = Depends(api_key_header),
169
+ ) -> User:
170
+ """Get current user from either JWT token or API key"""
171
+ if not FEATURES.get("ENABLE_AUTH", False):
172
+ # Return a mock user if auth is disabled
173
+ return User(id=1, username="anonymous", email="anonymous@example.com")
174
+
175
+ user = None
176
+
177
+ # Try JWT token first
178
+ if bearer_token and bearer_token.credentials:
179
+ try:
180
+ user = await get_current_user_from_token(bearer_token.credentials)
181
+ except HTTPException:
182
+ pass
183
+
184
+ # Try API key if enabled and no user from JWT
185
+ if not user and ENABLE_API_KEY_AUTH and api_key:
186
+ user = await get_current_user_from_api_key(api_key)
187
+
188
+ if not user:
189
+ raise HTTPException(
190
+ status_code=status.HTTP_401_UNAUTHORIZED,
191
+ detail="Not authenticated",
192
+ headers={"WWW-Authenticate": "Bearer"},
193
+ )
194
+
195
+ # Check if user is active
196
+ if not user.is_active:
197
+ raise HTTPException(
198
+ status_code=status.HTTP_403_FORBIDDEN, detail="Inactive user"
199
+ )
200
+
201
+ return user
202
+
203
+
204
+ async def get_current_active_user(
205
+ current_user: User = Depends(get_current_user),
206
+ ) -> User:
207
+ """Ensure the current user is active"""
208
+ if not current_user.is_active:
209
+ raise HTTPException(status_code=400, detail="Inactive user")
210
+ return current_user
211
+
212
+
213
+ def require_scopes(*required_scopes: str):
214
+ """Dependency to require specific scopes"""
215
+
216
+ async def scope_checker(current_user: User = Depends(get_current_user)):
217
+ if not FEATURES.get("ENABLE_AUTH", False):
218
+ return current_user
219
+
220
+ user_scopes = set(current_user.scopes or [])
221
+ required = set(required_scopes)
222
+
223
+ if not required.issubset(user_scopes):
224
+ raise HTTPException(
225
+ status_code=status.HTTP_403_FORBIDDEN,
226
+ detail=f"Not enough permissions. Required scopes: {', '.join(required_scopes)}",
227
+ )
228
+
229
+ return current_user
230
+
231
+ return scope_checker
232
+
233
+
234
+ # Authentication service class
235
+ class AuthService:
236
+ """Service for authentication operations"""
237
+
238
+ @staticmethod
239
+ async def register_user(user_data: UserCreate) -> User:
240
+ """Register a new user"""
241
+ with db_session() as db:
242
+ # Check if user exists
243
+ if (
244
+ db.query(User)
245
+ .filter(
246
+ (User.username == user_data.username)
247
+ | (User.email == user_data.email)
248
+ )
249
+ .first()
250
+ ):
251
+ raise HTTPException(
252
+ status_code=status.HTTP_400_BAD_REQUEST,
253
+ detail="Username or email already registered",
254
+ )
255
+
256
+ # Create new user
257
+ hashed_password = get_password_hash(user_data.password)
258
+ user = User(
259
+ username=user_data.username,
260
+ email=user_data.email,
261
+ hashed_password=hashed_password,
262
+ full_name=user_data.full_name,
263
+ is_active=True,
264
+ created_at=datetime.utcnow(),
265
+ )
266
+
267
+ db.add(user)
268
+ db.commit()
269
+ db.refresh(user)
270
+
271
+ logger.info(f"New user registered: {user.username}")
272
+ return user
273
+
274
+ @staticmethod
275
+ async def authenticate_user(username: str, password: str) -> Optional[User]:
276
+ """Authenticate a user with username and password"""
277
+ with db_session() as db:
278
+ user = db.query(User).filter(User.username == username).first()
279
+
280
+ if not user or not verify_password(password, user.hashed_password):
281
+ return None
282
+
283
+ # Update last login
284
+ user.last_login = datetime.utcnow()
285
+ db.commit()
286
+
287
+ return user
288
+
289
+ @staticmethod
290
+ async def login(user_data: UserLogin) -> Token:
291
+ """Login user and return tokens"""
292
+ user = await AuthService.authenticate_user(
293
+ user_data.username, user_data.password
294
+ )
295
+
296
+ if not user:
297
+ raise HTTPException(
298
+ status_code=status.HTTP_401_UNAUTHORIZED,
299
+ detail="Incorrect username or password",
300
+ headers={"WWW-Authenticate": "Bearer"},
301
+ )
302
+
303
+ # Create tokens
304
+ access_token_data = {
305
+ "sub": user.username,
306
+ "user_id": user.id,
307
+ "scopes": user.scopes or [],
308
+ }
309
+
310
+ access_token = create_access_token(access_token_data)
311
+ refresh_token = create_refresh_token({"sub": user.username})
312
+
313
+ return Token(
314
+ access_token=access_token,
315
+ refresh_token=refresh_token,
316
+ token_type="bearer",
317
+ expires_in=ACCESS_TOKEN_EXPIRE_MINUTES * 60,
318
+ )
319
+
320
+ @staticmethod
321
+ async def refresh_token(refresh_token: str) -> Token:
322
+ """Refresh access token using refresh token"""
323
+ try:
324
+ payload = jwt.decode(refresh_token, SECRET_KEY, algorithms=[ALGORITHM])
325
+ username: str = payload.get("sub")
326
+ token_type: str = payload.get("type")
327
+
328
+ if username is None or token_type != "refresh":
329
+ raise HTTPException(
330
+ status_code=status.HTTP_401_UNAUTHORIZED,
331
+ detail="Invalid refresh token",
332
+ )
333
+
334
+ # Get user
335
+ with db_session() as db:
336
+ user = db.query(User).filter(User.username == username).first()
337
+ if not user:
338
+ raise HTTPException(
339
+ status_code=status.HTTP_401_UNAUTHORIZED,
340
+ detail="User not found",
341
+ )
342
+
343
+ # Create new access token
344
+ access_token_data = {
345
+ "sub": user.username,
346
+ "user_id": user.id,
347
+ "scopes": user.scopes or [],
348
+ }
349
+
350
+ access_token = create_access_token(access_token_data)
351
+
352
+ return Token(
353
+ access_token=access_token,
354
+ token_type="bearer",
355
+ expires_in=ACCESS_TOKEN_EXPIRE_MINUTES * 60,
356
+ )
357
+
358
+ except JWTError:
359
+ raise HTTPException(
360
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token"
361
+ )
362
+
363
+ @staticmethod
364
+ async def create_api_key(user: User, key_data: APIKeyCreate) -> APIKey:
365
+ """Create a new API key for a user"""
366
+ with db_session() as db:
367
+ api_key = APIKey(
368
+ key=generate_api_key(),
369
+ name=key_data.name,
370
+ description=key_data.description,
371
+ user_id=user.id,
372
+ scopes=key_data.scopes,
373
+ expires_at=key_data.expires_at,
374
+ created_at=datetime.utcnow(),
375
+ is_active=True,
376
+ )
377
+
378
+ db.add(api_key)
379
+ db.commit()
380
+ db.refresh(api_key)
381
+
382
+ logger.info(f"API key created for user {user.username}: {api_key.name}")
383
+ return api_key
384
+
385
+ @staticmethod
386
+ async def revoke_api_key(user: User, key_id: int) -> bool:
387
+ """Revoke an API key"""
388
+ with db_session() as db:
389
+ api_key = (
390
+ db.query(APIKey)
391
+ .filter(APIKey.id == key_id, APIKey.user_id == user.id)
392
+ .first()
393
+ )
394
+
395
+ if not api_key:
396
+ return False
397
+
398
+ api_key.is_active = False
399
+ api_key.revoked_at = datetime.utcnow()
400
+ db.commit()
401
+
402
+ logger.info(f"API key revoked: {api_key.name}")
403
+ return True
404
+
405
+
406
+ # Optional: OAuth2 password flow for testing
407
+ from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
408
+
409
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/token", auto_error=False)
410
+
411
+
412
+ async def login_for_access_token(
413
+ form_data: OAuth2PasswordRequestForm = Depends(),
414
+ ) -> Token:
415
+ """OAuth2 compatible token endpoint"""
416
+ user = await AuthService.authenticate_user(form_data.username, form_data.password)
417
+
418
+ if not user:
419
+ raise HTTPException(
420
+ status_code=status.HTTP_401_UNAUTHORIZED,
421
+ detail="Incorrect username or password",
422
+ headers={"WWW-Authenticate": "Bearer"},
423
+ )
424
+
425
+ access_token_data = {
426
+ "sub": user.username,
427
+ "user_id": user.id,
428
+ "scopes": form_data.scopes,
429
+ }
430
+
431
+ access_token = create_access_token(access_token_data)
432
+
433
+ return {"access_token": access_token, "token_type": "bearer"}
@@ -0,0 +1,40 @@
1
+ import os
2
+ import logging
3
+
4
+ from slowapi import Limiter, _rate_limit_exceeded_handler
5
+ from slowapi.util import get_remote_address
6
+ from slowapi.middleware import SlowAPIMiddleware
7
+
8
+ from backend.core.system_scanner import SystemScanner
9
+ from backend.core.data_aggregator import DataAggregator
10
+ from backend.core.conflict_resolver import ConflictResolver
11
+ from backend.core.export_generator import ExportGenerator
12
+ from backend.database.compatibility_db import CompatibilityDB
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ redis_url = os.getenv("REDIS_URL")
17
+ if redis_url:
18
+ limiter = Limiter(key_func=get_remote_address, storage_uri=redis_url)
19
+ else:
20
+ limiter = Limiter(key_func=get_remote_address)
21
+
22
+
23
+ def get_system_scanner() -> SystemScanner:
24
+ return SystemScanner()
25
+
26
+
27
+ def get_data_aggregator() -> DataAggregator:
28
+ return DataAggregator()
29
+
30
+
31
+ def get_conflict_resolver() -> ConflictResolver:
32
+ return ConflictResolver()
33
+
34
+
35
+ def get_export_generator() -> ExportGenerator:
36
+ return ExportGenerator()
37
+
38
+
39
+ def get_compatibility_db() -> CompatibilityDB:
40
+ return CompatibilityDB()
@@ -0,0 +1,80 @@
1
+ """Custom exception classes for structured error handling"""
2
+
3
+ from typing import Dict, Any, Optional, List
4
+
5
+
6
+ class DependencyResolverError(Exception):
7
+ """Base exception for dependency resolver errors"""
8
+
9
+ def __init__(
10
+ self,
11
+ message: str,
12
+ error_code: str,
13
+ status_code: int = 500,
14
+ details: Optional[Dict[str, Any]] = None,
15
+ ):
16
+ self.message = message
17
+ self.error_code = error_code
18
+ self.status_code = status_code
19
+ self.details = details or {}
20
+ super().__init__(self.message)
21
+
22
+
23
+ class ValidationError(DependencyResolverError):
24
+ """Raised when input validation fails"""
25
+
26
+ def __init__(self, message: str, field: Optional[str] = None):
27
+ super().__init__(
28
+ message=message,
29
+ error_code="VALIDATION_ERROR",
30
+ status_code=400,
31
+ details={"field": field} if field else {},
32
+ )
33
+
34
+
35
+ class PackageNotFoundError(DependencyResolverError):
36
+ """Raised when a package cannot be found"""
37
+
38
+ def __init__(self, package_name: str, ecosystem: Optional[str] = None):
39
+ super().__init__(
40
+ message=f"Package '{package_name}' not found",
41
+ error_code="PACKAGE_NOT_FOUND",
42
+ status_code=404,
43
+ details={"package_name": package_name, "ecosystem": ecosystem},
44
+ )
45
+
46
+
47
+ class EcosystemNotSupportedError(DependencyResolverError):
48
+ """Raised when an ecosystem is not supported"""
49
+
50
+ def __init__(self, ecosystem: str):
51
+ super().__init__(
52
+ message=f"Ecosystem '{ecosystem}' is not supported",
53
+ error_code="ECOSYSTEM_NOT_SUPPORTED",
54
+ status_code=400,
55
+ details={"ecosystem": ecosystem},
56
+ )
57
+
58
+
59
+ class ConflictResolutionError(DependencyResolverError):
60
+ """Raised when dependency conflicts cannot be resolved"""
61
+
62
+ def __init__(self, message: str, conflicts: Optional[List[Dict]] = None):
63
+ super().__init__(
64
+ message=message,
65
+ error_code="CONFLICT_RESOLUTION_FAILED",
66
+ status_code=409,
67
+ details={"conflicts": conflicts or []},
68
+ )
69
+
70
+
71
+ class RateLimitExceededError(DependencyResolverError):
72
+ """Raised when rate limit is exceeded"""
73
+
74
+ def __init__(self, retry_after: Optional[int] = None):
75
+ super().__init__(
76
+ message="Rate limit exceeded",
77
+ error_code="RATE_LIMIT_EXCEEDED",
78
+ status_code=429,
79
+ details={"retry_after": retry_after},
80
+ )