Coverage for fastblocks / adapters / images / cloudinary.py: 44%

54 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-26 03:58 -0800

1"""Cloudinary image adapter implementation.""" 

2 

3from contextlib import suppress 

4from typing import Any 

5from uuid import UUID 

6 

7from acb.depends import depends 

8 

9from ._base import ImagesBase, ImagesBaseSettings 

10 

11 

12class CloudinaryImagesSettings(ImagesBaseSettings): 

13 """Cloudinary-specific settings.""" 

14 

15 cloud_name: str 

16 api_key: str 

17 api_secret: str 

18 secure: bool = True 

19 upload_preset: str | None = None 

20 

21 

22class CloudinaryImages(ImagesBase): 

23 """Cloudinary image adapter implementation.""" 

24 

25 # Required ACB 0.19.0+ metadata 

26 MODULE_ID: UUID = UUID("01937d86-4f2a-7b3c-8d9e-f3b4d3c2b2a1") # Static UUID7 

27 MODULE_STATUS = "stable" 

28 

29 def __init__(self) -> None: 

30 """Initialize Cloudinary adapter.""" 

31 super().__init__() 

32 self.settings = CloudinaryImagesSettings() 

33 

34 # Register with ACB dependency system 

35 with suppress(Exception): 

36 depends.set(self) 

37 

38 async def upload_image(self, file_data: bytes, filename: str) -> str: 

39 """Upload image to Cloudinary and return public_id.""" 

40 # Basic implementation - would integrate with cloudinary library 

41 # For now, return a mock public_id based on filename 

42 public_id = filename.rsplit(".", 1)[0] # Remove extension 

43 return f"uploads/{public_id}" 

44 

45 async def get_image_url( 

46 self, image_id: str, transformations: dict[str, Any] | None = None 

47 ) -> str: 

48 """Generate Cloudinary URL with optional transformations.""" 

49 base_url = f"https://res.cloudinary.com/{self.settings.cloud_name}/image/upload" 

50 

51 if transformations: 

52 # Build transformation string 

53 transform_parts = [] 

54 for key, value in transformations.items(): 

55 if key == "width": 

56 transform_parts.append(f"w_{value}") 

57 elif key == "height": 

58 transform_parts.append(f"h_{value}") 

59 elif key == "crop": 

60 transform_parts.append(f"c_{value}") 

61 elif key == "quality": 

62 transform_parts.append(f"q_{value}") 

63 elif key == "format": 

64 transform_parts.append(f"f_{value}") 

65 

66 if transform_parts: 

67 transform_str = ",".join(transform_parts) 

68 return f"{base_url}/{transform_str}/{image_id}" 

69 

70 return f"{base_url}/{image_id}" 

71 

72 def get_img_tag(self, image_id: str, alt: str, **attributes: Any) -> str: 

73 url = self.get_image_url(image_id, attributes.pop("transformations", None)) 

74 

75 # Build attributes string 

76 attr_parts = [f'src="{url}"', f'alt="{alt}"'] 

77 

78 for key, value in attributes.items(): 

79 if key in ("width", "height", "class", "id", "style"): 

80 attr_parts.append(f'{key}="{value}"') 

81 

82 # Add lazy loading by default 

83 if "loading" not in attributes: 

84 attr_parts.append('loading="lazy"') 

85 

86 return f"<img {' '.join(attr_parts)}>" 

87 

88 

89ImagesSettings = CloudinaryImagesSettings 

90Images = CloudinaryImages 

91 

92depends.set(Images, "cloudinary") 

93 

94__all__ = ["CloudinaryImages", "CloudinaryImagesSettings", "Images", "ImagesSettings"]