kopia lustrzana https://github.com/collective/icalendar
				
				
				
			Add Image class as read attribute
							rodzic
							
								
									b0d98b004f
								
							
						
					
					
						commit
						140e50746d
					
				|  | @ -51,7 +51,6 @@ from icalendar.parser import ( | |||
| 
 | ||||
| # Property Data Value Types | ||||
| from icalendar.prop import ( | ||||
|     Image, | ||||
|     TypesFactory, | ||||
|     vBinary, | ||||
|     vBoolean, | ||||
|  | @ -75,6 +74,7 @@ from icalendar.prop import ( | |||
|     vUTCOffset, | ||||
|     vWeekday, | ||||
| ) | ||||
| from icalendar.prop.image import Image | ||||
| 
 | ||||
| # Switching the timezone provider | ||||
| from icalendar.timezone import use_pytz, use_zoneinfo | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ from icalendar.enums import BUSYTYPE, CLASS, STATUS, TRANSP, StrEnum | |||
| from icalendar.error import IncompleteComponent, InvalidCalendar | ||||
| from icalendar.parser_tools import SEQUENCE_TYPES | ||||
| from icalendar.prop import vCalAddress, vCategory, vDDDTypes, vDuration, vRecur, vText | ||||
| from icalendar.prop.image import Image | ||||
| from icalendar.timezone import tzp | ||||
| from icalendar.tools import is_date | ||||
| 
 | ||||
|  | @ -1585,6 +1586,37 @@ def set_duration_with_locking(component, duration, locked, end_property): | |||
|         raise ValueError(f"locked must be 'start' or 'end', not {locked!r}") | ||||
| 
 | ||||
| 
 | ||||
| def _get_images(self: Component) -> list[Image]: | ||||
|     """IMAGE specifies an image associated with the calendar or a calendar component. | ||||
| 
 | ||||
|     Description: | ||||
|         This property specifies an image for an iCalendar | ||||
|         object or a calendar component via a URI or directly with inline | ||||
|         data that can be used by calendar user agents when presenting the | ||||
|         calendar data to a user.  Multiple properties MAY be used to | ||||
|         specify alternative sets of images with, for example, varying | ||||
|         media subtypes, resolutions, or sizes.  When multiple properties | ||||
|         are present, calendar user agents SHOULD display only one of them, | ||||
|         picking one that provides the most appropriate image quality, or | ||||
|         display none.  The "DISPLAY" parameter is used to indicate the | ||||
|         intended display mode for the image.  The "ALTREP" parameter, | ||||
|         defined in :rfc:`5545`, can be used to provide a "clickable" image | ||||
|         where the URI in the parameter value can be "launched" by a click | ||||
|         on the image in the calendar user agent. | ||||
| 
 | ||||
|     Conformance: | ||||
|         This property can be specified multiple times in an | ||||
|         iCalendar object or in "VEVENT", "VTODO", or "VJOURNAL" calendar | ||||
|         components. | ||||
|     """ | ||||
|     images = self.get("IMAGE", []) | ||||
|     if not isinstance(images, SEQUENCE_TYPES): | ||||
|         images = [images] | ||||
|     return [Image.from_property_value(img) for img in images] | ||||
| 
 | ||||
| 
 | ||||
| images_property = property(_get_images) | ||||
| 
 | ||||
| __all__ = [ | ||||
|     "attendees_property", | ||||
|     "busy_type_property", | ||||
|  | @ -1598,6 +1630,7 @@ __all__ = [ | |||
|     "descriptions_property", | ||||
|     "duration_property", | ||||
|     "exdates_property", | ||||
|     "images_property", | ||||
|     "location_property", | ||||
|     "multi_language_text_property", | ||||
|     "organizer_property", | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Sequence | |||
| 
 | ||||
| from icalendar.attr import ( | ||||
|     categories_property, | ||||
|     images_property, | ||||
|     multi_language_text_property, | ||||
|     single_string_property, | ||||
|     source_property, | ||||
|  | @ -495,6 +496,8 @@ Description: | |||
|         """Delete REFRESH-INTERVAL.""" | ||||
|         self.pop("REFRESH-INTERVAL") | ||||
| 
 | ||||
|     images = images_property | ||||
| 
 | ||||
|     @classmethod | ||||
|     def new( | ||||
|         cls, | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ from icalendar.attr import ( | |||
|     create_single_property, | ||||
|     description_property, | ||||
|     exdates_property, | ||||
|     images_property, | ||||
|     location_property, | ||||
|     organizer_property, | ||||
|     priority_property, | ||||
|  | @ -483,6 +484,7 @@ class Event(Component): | |||
|     transparency = transparency_property | ||||
|     status = status_property | ||||
|     attendees = attendees_property | ||||
|     images = images_property | ||||
| 
 | ||||
|     @classmethod | ||||
|     def new( | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ from icalendar.attr import ( | |||
|     create_single_property, | ||||
|     descriptions_property, | ||||
|     exdates_property, | ||||
|     images_property, | ||||
|     organizer_property, | ||||
|     rdates_property, | ||||
|     rrules_property, | ||||
|  | @ -167,6 +168,8 @@ class Journal(Component): | |||
|         """Delete all descriptions.""" | ||||
|         del self.descriptions | ||||
| 
 | ||||
|     images = images_property | ||||
| 
 | ||||
|     @classmethod | ||||
|     def new( | ||||
|         cls, | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ from icalendar.attr import ( | |||
|     create_single_property, | ||||
|     description_property, | ||||
|     exdates_property, | ||||
|     images_property, | ||||
|     location_property, | ||||
|     organizer_property, | ||||
|     priority_property, | ||||
|  | @ -351,6 +352,7 @@ class Todo(Component): | |||
|     contacts = contacts_property | ||||
|     status = status_property | ||||
|     attendees = attendees_property | ||||
|     images = images_property | ||||
| 
 | ||||
|     @classmethod | ||||
|     def new( | ||||
|  |  | |||
|  | @ -63,8 +63,6 @@ from icalendar.parser_tools import ( | |||
| from icalendar.timezone import tzid_from_dt, tzid_from_tzinfo, tzp | ||||
| from icalendar.tools import to_datetime | ||||
| 
 | ||||
| from .image import Image | ||||
| 
 | ||||
| DURATION_REGEX = re.compile( | ||||
|     r"([-+]?)P(?:(\d+)W)?(?:(\d+)D)?" r"(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$" | ||||
| ) | ||||
|  | @ -78,10 +76,13 @@ class vBinary: | |||
|     """Binary property values are base 64 encoded.""" | ||||
| 
 | ||||
|     params: Parameters | ||||
|     obj: str | ||||
| 
 | ||||
|     def __init__(self, obj): | ||||
|     def __init__(self, obj, params: dict[str:str] | None = None): | ||||
|         self.obj = to_unicode(obj) | ||||
|         self.params = Parameters(encoding="BASE64", value="BINARY") | ||||
|         if params: | ||||
|             self.params.update(params) | ||||
| 
 | ||||
|     def __repr__(self): | ||||
|         return f"vBinary({self.to_ical()})" | ||||
|  | @ -2209,7 +2210,6 @@ class TypesFactory(CaselessDict): | |||
| __all__ = [ | ||||
|     "DURATION_REGEX", | ||||
|     "WEEKDAY_RULE", | ||||
|     "Image", | ||||
|     "TimeBase", | ||||
|     "TypesFactory", | ||||
|     "tzid_from_dt", | ||||
|  |  | |||
|  | @ -1,8 +1,61 @@ | |||
| """This contains the IMAGE property from :rfc:`7986`.""" | ||||
| 
 | ||||
| from __future__ import annotations | ||||
| 
 | ||||
| import base64 | ||||
| 
 | ||||
| from icalendar.prop import vBinary, vText, vUri | ||||
| 
 | ||||
| 
 | ||||
| class Image: | ||||
|     """An image as URI or BINARY according to :rfc:`7986`.""" | ||||
| 
 | ||||
|     @classmethod | ||||
|     def from_property_value(cls, value: vUri | vBinary | vText): | ||||
|         """Create an Image from a property value.""" | ||||
|         params: dict[str, str] = {} | ||||
|         if not hasattr(value, "params"): | ||||
|             raise TypeError("Value must be URI or BINARY.") | ||||
|         value_type = value.params.get("VALUE", "").upper() | ||||
|         if value_type == "URI" or isinstance(value, vUri): | ||||
|             params["uri"] = str(value) | ||||
|         elif isinstance(value, vBinary): | ||||
|             params["b64data"] = value.obj | ||||
|         elif value_type == "BINARY": | ||||
|             params["b64data"] = str(value) | ||||
|         else: | ||||
|             raise TypeError( | ||||
|                 f"The VALUE parameter must be URI or BINARY, not {value_type!r}." | ||||
|             ) | ||||
|         params.update( | ||||
|             { | ||||
|                 "fmttype": value.params.get("FMTTYPE"), | ||||
|                 "altrep": value.params.get("ALTREP"), | ||||
|                 "display": value.params.get("DISPLAY"), | ||||
|             } | ||||
|         ) | ||||
|         return cls(**params) | ||||
| 
 | ||||
|     def __init__( | ||||
|         self, | ||||
|         uri: str | None = None, | ||||
|         b64data: str | None = None, | ||||
|         fmttype: str | None = None, | ||||
|         altrep: str | None = None, | ||||
|         display: str | None = None, | ||||
|     ): | ||||
|         self.uri = uri | ||||
|         self.b64data = b64data | ||||
|         self.fmttype = fmttype | ||||
|         self.altrep = altrep | ||||
|         self.display = display | ||||
| 
 | ||||
|     @property | ||||
|     def data(self) -> bytes | None: | ||||
|         """Return the binary data, if available.""" | ||||
|         if self.b64data is None: | ||||
|             return None | ||||
|         return base64.b64decode(self.b64data) | ||||
| 
 | ||||
| 
 | ||||
| __all__ = ["Image"] | ||||
|  |  | |||
|  | @ -5,10 +5,10 @@ IMAGE;VALUE=URI;DISPLAY=BADGE;FMTTYPE=image/png:http://example.com/images/party. | |||
| END:VEVENT | ||||
| BEGIN:VTODO | ||||
| UID:uri-todo | ||||
| IMAGE;VALUE=URI;DISPLAY=BADGE;FMTTYPE=image/jpg:http://example.com/images/party.jpg | ||||
| IMAGE;VALUE=URI;DISPLAY=BADGE:http://example.com/images/party.jpg | ||||
| END:VTODO | ||||
| BEGIN:VJOURNAL | ||||
| UID:uri-todo | ||||
| UID:uri-journal | ||||
| IMAGE;VALUE=URI;DISPLAY=ICON;FMTTYPE=image/jpg:http://example.com/images/party.jpg | ||||
| END:VJOURNAL | ||||
| BEGIN:VEVENT | ||||
|  | @ -17,7 +17,7 @@ IMAGE;ENCODING=BASE64;VALUE=BINARY;FMTTYPE=image/png:iVBORw0KGgoAAAANSUhEUgAAAAE | |||
| END:VEVENT | ||||
| BEGIN:VTODO | ||||
| UID:data-todo | ||||
| IMAGE;ENCODING=BASE64;VALUE=BINARY;FMTTYPE=image/png;ALTREP=http://example.com/images/party.jpg:iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACXBIWXMAAAAnAAAAJwEqCZFPAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAA1JREFUCJlj+P//PwMACPwC/oXNqzQAAAAASUVORK5CYII= | ||||
| IMAGE;ENCODING=BASE64;VALUE=BINARY;FMTTYPE=image/png;ALTREP="http://example.com/images/party.jpg":iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACXBIWXMAAAAnAAAAJwEqCZFPAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAA1JREFUCJlj+P//PwMACPwC/oXNqzQAAAAASUVORK5CYII= | ||||
| END:VTODO | ||||
| BEGIN:VJOURNAL | ||||
| UID:data-journal | ||||
|  |  | |||
|  | @ -1,8 +1,159 @@ | |||
| """Test the image class to convert from and to binary data.""" | ||||
| 
 | ||||
| from __future__ import annotations | ||||
| 
 | ||||
| import base64 | ||||
| 
 | ||||
| import pytest | ||||
| 
 | ||||
| from icalendar import Calendar, Image, vBinary, vUri | ||||
| from icalendar.prop import vText | ||||
| 
 | ||||
| TRANSPARENT_PIXEL = base64.b64decode("""iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCA | ||||
| YAAAAfFcSJAAAACXBIWXMAAAAnAAAAJwEqCZFPAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jn | ||||
| m+48GgAAAA1JREFUCJlj+P//PwMACPwC/oXNqzQAAAAASUVORK5CYII=""") | ||||
| 
 | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def images(calendars): | ||||
| def images(calendars) -> dict[str, Image]: | ||||
|     """Return the images we get from the example calendars.""" | ||||
|     calendar: Calendar = calendars.rfc_7986_image | ||||
|     images: dict[str, Image] = {} | ||||
|     for component in calendar.subcomponents: | ||||
|         img = component.images[0] | ||||
|         images[component.uid] = img | ||||
|     return images | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     ("uid", "uri", "data", "fmt_type", "altrep", "display"), | ||||
|     [ | ||||
|         ( | ||||
|             "uri-event", | ||||
|             "http://example.com/images/party.png", | ||||
|             None, | ||||
|             "image/png", | ||||
|             None, | ||||
|             "BADGE", | ||||
|         ), | ||||
|         ( | ||||
|             "uri-todo", | ||||
|             "http://example.com/images/party.jpg", | ||||
|             None, | ||||
|             None, | ||||
|             None, | ||||
|             "BADGE", | ||||
|         ), | ||||
|         ( | ||||
|             "uri-journal", | ||||
|             "http://example.com/images/party.jpg", | ||||
|             None, | ||||
|             "image/jpg", | ||||
|             None, | ||||
|             "ICON", | ||||
|         ), | ||||
|         ( | ||||
|             "data-event", | ||||
|             None, | ||||
|             TRANSPARENT_PIXEL, | ||||
|             "image/png", | ||||
|             None, | ||||
|             None, | ||||
|         ), | ||||
|         ( | ||||
|             "data-todo", | ||||
|             None, | ||||
|             TRANSPARENT_PIXEL, | ||||
|             "image/png", | ||||
|             "http://example.com/images/party.jpg", | ||||
|             None, | ||||
|         ), | ||||
|         ( | ||||
|             "data-journal", | ||||
|             None, | ||||
|             TRANSPARENT_PIXEL, | ||||
|             "image/png", | ||||
|             None, | ||||
|             "BADGE", | ||||
|         ), | ||||
|     ], | ||||
| ) | ||||
| def test_image_parsing( | ||||
|     images, | ||||
|     uid, | ||||
|     uri, | ||||
|     data, | ||||
|     fmt_type, | ||||
|     altrep, | ||||
|     display, | ||||
| ): | ||||
|     """Test that the image property is parsed correctly.""" | ||||
|     image = images[uid] | ||||
|     assert image.uri == uri | ||||
|     assert image.data == data | ||||
|     assert image.fmttype == fmt_type | ||||
|     assert image.altrep == altrep | ||||
|     assert image.display == display | ||||
| 
 | ||||
| 
 | ||||
| def test_no_images(): | ||||
|     """Test that an empty calendar has no images.""" | ||||
|     calendar = Calendar() | ||||
|     assert len(calendar.images) == 0 | ||||
| 
 | ||||
| 
 | ||||
| def test_create_image_invalid_type(): | ||||
|     """Test that creating an image with invalid type raises TypeError.""" | ||||
|     with pytest.raises(TypeError): | ||||
|         Image.from_property_value("not a valid type") | ||||
|     with pytest.raises(TypeError): | ||||
|         Image.from_property_value(vText("not a valid type", params={"VALUE": "TEXT"})) | ||||
|     with pytest.raises(TypeError): | ||||
|         Image.from_property_value(vText("http://example.com/image.png")) | ||||
| 
 | ||||
| 
 | ||||
| def test_create_with_vBinary(): | ||||
|     """Test creating an Image from a vBinary property.""" | ||||
|     b64data = base64.b64encode(TRANSPARENT_PIXEL).decode("ascii") | ||||
|     vbin = vBinary(b64data, params={"FMTTYPE": "image/png"}) | ||||
|     image = Image.from_property_value(vbin) | ||||
|     assert image.uri is None | ||||
|     assert image.data == TRANSPARENT_PIXEL | ||||
|     assert image.fmttype == "image/png" | ||||
|     assert image.altrep is None | ||||
|     assert image.display is None | ||||
| 
 | ||||
| 
 | ||||
| def test_create_with_vUri(): | ||||
|     """Test creating an Image from a vUri property.""" | ||||
|     uri = "http://example.com/image.png" | ||||
|     vuri = vUri(uri, params={"FMTTYPE": "image/png", "DISPLAY": "BADGE"}) | ||||
|     image = Image.from_property_value(vuri) | ||||
|     assert image.uri == uri | ||||
|     assert image.data is None | ||||
|     assert image.fmttype == "image/png" | ||||
|     assert image.altrep is None | ||||
|     assert image.display == "BADGE" | ||||
| 
 | ||||
| 
 | ||||
| def test_create_image_with_vText_as_uri(): | ||||
|     """Test that creating an image with vText but VALUE URI or BINARY raises TypeError.""" | ||||
|     img = Image.from_property_value( | ||||
|         vText("http://example.com/image.png", params={"VALUE": "URI"}) | ||||
|     ) | ||||
|     assert img.uri == "http://example.com/image.png" | ||||
|     assert img.data is None | ||||
|     assert img.fmttype is None | ||||
|     assert img.altrep is None | ||||
|     assert img.display is None | ||||
| 
 | ||||
| 
 | ||||
| def test_create_image_with_vText_as_binary(): | ||||
|     """Test that creating an image with vText but VALUE URI or BINARY raises TypeError.""" | ||||
|     b64data = base64.b64encode(TRANSPARENT_PIXEL).decode("ascii") | ||||
|     img = Image.from_property_value(vText(b64data, params={"VALUE": "BINARY"})) | ||||
|     assert img.uri is None | ||||
|     assert img.data == TRANSPARENT_PIXEL | ||||
|     assert img.fmttype is None | ||||
|     assert img.altrep is None | ||||
|     assert img.display is None | ||||
|  |  | |||
		Ładowanie…
	
		Reference in New Issue
	
	 Nicco Kunzmann
						Nicco Kunzmann