Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/parser.py: 79%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

302 statements  

1"""This module parses and generates contentlines as defined in RFC 5545 

2(iCalendar), but will probably work for other MIME types with similar syntax. 

3Eg. RFC 2426 (vCard) 

4 

5It is stupid in the sense that it treats the content purely as strings. No type 

6conversion is attempted. 

7""" 

8 

9from __future__ import annotations 

10 

11import functools 

12import os 

13import re 

14from datetime import datetime, time 

15from typing import TYPE_CHECKING, Any, Callable 

16 

17from icalendar.caselessdict import CaselessDict 

18from icalendar.error import JCalParsingError 

19from icalendar.parser_tools import ( 

20 DEFAULT_ENCODING, 

21 ICAL_TYPE, 

22 SEQUENCE_TYPES, 

23 to_unicode, 

24) 

25from icalendar.timezone.tzid import tzid_from_dt 

26 

27if TYPE_CHECKING: 

28 from icalendar.enums import VALUE 

29 from icalendar.prop import VPROPERTY 

30 

31 

32def escape_char(text): 

33 """Format value according to iCalendar TEXT escaping rules.""" 

34 assert isinstance(text, (str, bytes)) 

35 # NOTE: ORDER MATTERS! 

36 return ( 

37 text.replace(r"\N", "\n") 

38 .replace("\\", "\\\\") 

39 .replace(";", r"\;") 

40 .replace(",", r"\,") 

41 .replace("\r\n", r"\n") 

42 .replace("\n", r"\n") 

43 ) 

44 

45 

46def unescape_char(text): 

47 assert isinstance(text, (str, bytes)) 

48 # NOTE: ORDER MATTERS! 

49 if isinstance(text, str): 

50 return ( 

51 text.replace("\\N", "\\n") 

52 .replace("\r\n", "\n") 

53 .replace("\\n", "\n") 

54 .replace("\\,", ",") 

55 .replace("\\;", ";") 

56 .replace("\\\\", "\\") 

57 ) 

58 if isinstance(text, bytes): 

59 return ( 

60 text.replace(b"\\N", b"\\n") 

61 .replace(b"\r\n", b"\n") 

62 .replace(b"\\n", b"\n") 

63 .replace(b"\\,", b",") 

64 .replace(b"\\;", b";") 

65 .replace(b"\\\\", b"\\") 

66 ) 

67 return None 

68 

69 

70def foldline(line, limit=75, fold_sep="\r\n "): 

71 """Make a string folded as defined in RFC5545 

72 Lines of text SHOULD NOT be longer than 75 octets, excluding the line 

73 break. Long content lines SHOULD be split into a multiple line 

74 representations using a line "folding" technique. That is, a long 

75 line can be split between any two characters by inserting a CRLF 

76 immediately followed by a single linear white-space character (i.e., 

77 SPACE or HTAB). 

78 """ 

79 assert isinstance(line, str) 

80 assert "\n" not in line 

81 

82 # Use a fast and simple variant for the common case that line is all ASCII. 

83 try: 

84 line.encode("ascii") 

85 except (UnicodeEncodeError, UnicodeDecodeError): 

86 pass 

87 else: 

88 return fold_sep.join( 

89 line[i : i + limit - 1] for i in range(0, len(line), limit - 1) 

90 ) 

91 

92 ret_chars = [] 

93 byte_count = 0 

94 for char in line: 

95 char_byte_len = len(char.encode(DEFAULT_ENCODING)) 

96 byte_count += char_byte_len 

97 if byte_count >= limit: 

98 ret_chars.append(fold_sep) 

99 byte_count = char_byte_len 

100 ret_chars.append(char) 

101 

102 return "".join(ret_chars) 

103 

104 

105################################################################# 

106# Property parameter stuff 

107 

108 

109def param_value(value, always_quote=False): 

110 """Returns a parameter value.""" 

111 if isinstance(value, SEQUENCE_TYPES): 

112 return q_join(map(rfc_6868_escape, value), always_quote=always_quote) 

113 if isinstance(value, str): 

114 return dquote(rfc_6868_escape(value), always_quote=always_quote) 

115 return dquote(rfc_6868_escape(value.to_ical().decode(DEFAULT_ENCODING))) 

116 

117 

118# Could be improved 

119 

120# [\w-] because of the iCalendar RFC 

121# . because of the vCard RFC 

122NAME = re.compile(r"[\w.-]+") 

123 

124UNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7f",:;]') 

125QUNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7f"]') 

126FOLD = re.compile(b"(\r?\n)+[ \t]") 

127UFOLD = re.compile("(\r?\n)+[ \t]") 

128NEWLINE = re.compile(r"\r?\n") 

129 

130 

131def validate_token(name): 

132 match = NAME.findall(name) 

133 if len(match) == 1 and name == match[0]: 

134 return 

135 raise ValueError(name) 

136 

137 

138def validate_param_value(value, quoted=True): 

139 validator = QUNSAFE_CHAR if quoted else UNSAFE_CHAR 

140 if validator.findall(value): 

141 raise ValueError(value) 

142 

143 

144# chars presence of which in parameter value will be cause the value 

145# to be enclosed in double-quotes 

146QUOTABLE = re.compile("[,;:’]") # noqa: RUF001 

147 

148 

149def dquote(val, always_quote=False): 

150 """Enclose parameter values containing [,;:] in double quotes.""" 

151 # a double-quote character is forbidden to appear in a parameter value 

152 # so replace it with a single-quote character 

153 val = val.replace('"', "'") 

154 if QUOTABLE.search(val) or always_quote: 

155 return f'"{val}"' 

156 return val 

157 

158 

159# parsing helper 

160def q_split(st, sep=",", maxsplit=-1): 

161 """Splits a string on char, taking double (q)uotes into considderation.""" 

162 if maxsplit == 0: 

163 return [st] 

164 

165 result = [] 

166 cursor = 0 

167 length = len(st) 

168 inquote = 0 

169 splits = 0 

170 for i, ch in enumerate(st): 

171 if ch == '"': 

172 inquote = not inquote 

173 if not inquote and ch == sep: 

174 result.append(st[cursor:i]) 

175 cursor = i + 1 

176 splits += 1 

177 if i + 1 == length or splits == maxsplit: 

178 result.append(st[cursor:]) 

179 break 

180 return result 

181 

182 

183def q_join(lst, sep=",", always_quote=False): 

184 """Joins a list on sep, quoting strings with QUOTABLE chars.""" 

185 return sep.join(dquote(itm, always_quote=always_quote) for itm in lst) 

186 

187 

188def single_string_parameter(func: Callable | None = None, upper=False): 

189 """Create a parameter getter/setter for a single string parameter. 

190 

191 Args: 

192 upper: Convert the value to uppercase 

193 func: The function to decorate. 

194 

195 Returns: 

196 The property for the parameter or a decorator for the parameter 

197 if func is ``None``. 

198 """ 

199 

200 def decorator(func): 

201 name = func.__name__ 

202 

203 @functools.wraps(func) 

204 def fget(self: Parameters): 

205 """Get the value.""" 

206 value = self.get(name) 

207 if value is not None and upper: 

208 value = value.upper() 

209 return value 

210 

211 def fset(self: Parameters, value: str | None): 

212 """Set the value""" 

213 if value is None: 

214 fdel(self) 

215 else: 

216 if upper: 

217 value = value.upper() 

218 self[name] = value 

219 

220 def fdel(self: Parameters): 

221 """Delete the value.""" 

222 self.pop(name, None) 

223 

224 return property(fget, fset, fdel, doc=func.__doc__) 

225 

226 if func is None: 

227 return decorator 

228 return decorator(func) 

229 

230 

231class Parameters(CaselessDict): 

232 """Parser and generator of Property parameter strings. 

233 

234 It knows nothing of datatypes. 

235 Its main concern is textual structure. 

236 

237 Examples: 

238 

239 Modify parameters: 

240 

241 .. code-block:: pycon 

242 

243 >>> from icalendar import Parameters 

244 >>> params = Parameters() 

245 >>> params['VALUE'] = 'TEXT' 

246 >>> params.value 

247 'TEXT' 

248 >>> params 

249 Parameters({'VALUE': 'TEXT'}) 

250 

251 Create new parameters: 

252 

253 .. code-block:: pycon 

254 

255 >>> params = Parameters(value="BINARY") 

256 >>> params.value 

257 'BINARY' 

258 

259 Set a default: 

260 

261 .. code-block:: pycon 

262 

263 >>> params = Parameters(value="BINARY", default_value="TEXT") 

264 >>> params 

265 Parameters({'VALUE': 'BINARY'}) 

266 

267 """ 

268 

269 def __init__(self, *args, **kwargs): 

270 """Create new parameters.""" 

271 if args and args[0] is None: 

272 # allow passing None 

273 args = args[1:] 

274 defaults = { 

275 key[8:]: kwargs.pop(key) 

276 for key in list(kwargs.keys()) 

277 if key.lower().startswith("default_") 

278 } 

279 super().__init__(*args, **kwargs) 

280 for key, value in defaults.items(): 

281 self.setdefault(key, value) 

282 

283 # The following paremeters must always be enclosed in double quotes 

284 always_quoted = ( 

285 "ALTREP", 

286 "DELEGATED-FROM", 

287 "DELEGATED-TO", 

288 "DIR", 

289 "MEMBER", 

290 "SENT-BY", 

291 # Part of X-APPLE-STRUCTURED-LOCATION 

292 "X-ADDRESS", 

293 "X-TITLE", 

294 # RFC 9253 

295 "LINKREL", 

296 ) 

297 # this is quoted should one of the values be present 

298 quote_also = { 

299 # This is escaped in the RFC 

300 "CN": " '", 

301 } 

302 

303 def params(self): 

304 """In RFC 5545 keys are called parameters, so this is to be consitent 

305 with the naming conventions. 

306 """ 

307 return self.keys() 

308 

309 def to_ical(self, sorted: bool = True): # noqa: A002, FBT001 

310 """Returns an :rfc:`5545` representation of the parameters. 

311 

312 Args: 

313 sorted (bool): Sort the parameters before encoding. 

314 exclude_utc (bool): Exclude TZID if it is set to ``"UTC"`` 

315 """ 

316 result = [] 

317 items = list(self.items()) 

318 if sorted: 

319 items.sort() 

320 

321 for key, value in items: 

322 if key == "TZID" and value == "UTC": 

323 # The "TZID" property parameter MUST NOT be applied to DATE-TIME 

324 # properties whose time values are specified in UTC. 

325 continue 

326 upper_key = key.upper() 

327 check_quoteable_characters = self.quote_also.get(key.upper()) 

328 always_quote = upper_key in self.always_quoted or ( 

329 check_quoteable_characters 

330 and any(c in value for c in check_quoteable_characters) 

331 ) 

332 quoted_value = param_value(value, always_quote=always_quote) 

333 if isinstance(quoted_value, str): 

334 quoted_value = quoted_value.encode(DEFAULT_ENCODING) 

335 # CaselessDict keys are always unicode 

336 result.append(upper_key.encode(DEFAULT_ENCODING) + b"=" + quoted_value) 

337 return b";".join(result) 

338 

339 @classmethod 

340 def from_ical(cls, st, strict=False): 

341 """Parses the parameter format from ical text format.""" 

342 

343 # parse into strings 

344 result = cls() 

345 for param in q_split(st, ";"): 

346 try: 

347 key, val = q_split(param, "=", maxsplit=1) 

348 validate_token(key) 

349 # Property parameter values that are not in quoted 

350 # strings are case insensitive. 

351 vals = [] 

352 for v in q_split(val, ","): 

353 if v.startswith('"') and v.endswith('"'): 

354 v2 = v.strip('"') 

355 validate_param_value(v2, quoted=True) 

356 vals.append(rfc_6868_unescape(v2)) 

357 else: 

358 validate_param_value(v, quoted=False) 

359 if strict: 

360 vals.append(rfc_6868_unescape(v.upper())) 

361 else: 

362 vals.append(rfc_6868_unescape(v)) 

363 if not vals: 

364 result[key] = val 

365 elif len(vals) == 1: 

366 result[key] = vals[0] 

367 else: 

368 result[key] = vals 

369 except ValueError as exc: # noqa: PERF203 

370 raise ValueError( 

371 f"{param!r} is not a valid parameter string: {exc}" 

372 ) from exc 

373 return result 

374 

375 @single_string_parameter(upper=True) 

376 def value(self) -> VALUE | str | None: 

377 """The VALUE parameter from :rfc:`5545`. 

378 

379 Description: 

380 This parameter specifies the value type and format of 

381 the property value. The property values MUST be of a single value 

382 type. For example, a "RDATE" property cannot have a combination 

383 of DATE-TIME and TIME value types. 

384 

385 If the property's value is the default value type, then this 

386 parameter need not be specified. However, if the property's 

387 default value type is overridden by some other allowable value 

388 type, then this parameter MUST be specified. 

389 

390 Applications MUST preserve the value data for x-name and iana- 

391 token values that they don't recognize without attempting to 

392 interpret or parse the value data. 

393 

394 For convenience, using this property, the value will be converted to 

395 an uppercase string. 

396 

397 .. code-block:: pycon 

398 

399 >>> from icalendar import Parameters 

400 >>> params = Parameters() 

401 >>> params.value = "unknown" 

402 >>> params 

403 Parameters({'VALUE': 'UNKNOWN'}) 

404 

405 """ 

406 

407 def _parameter_value_to_jcal( 

408 self, value: str | float | list | VPROPERTY 

409 ) -> str | int | float | list[str] | list[int] | list[float]: 

410 """Convert a parameter value to jCal format. 

411 

412 Args: 

413 value: The parameter value 

414 

415 Returns: 

416 The jCal representation of the parameter value 

417 """ 

418 if isinstance(value, list): 

419 return [self._parameter_value_to_jcal(v) for v in value] 

420 if hasattr(value, "to_jcal"): 

421 # proprty values respond to this 

422 jcal = value.to_jcal() 

423 # we only need the value part 

424 if len(jcal) == 4: 

425 return jcal[3] 

426 return jcal[3:] 

427 for t in (int, float, str): 

428 if isinstance(value, t): 

429 return t(value) 

430 raise TypeError( 

431 "Unsupported parameter value type for jCal conversion: " 

432 f"{type(value)} {value!r}" 

433 ) 

434 

435 def to_jcal(self, exclude_utc=False) -> dict[str, str]: 

436 """Return the jCal representation of the parameters. 

437 

438 Args: 

439 exclude_utc (bool): Exclude the TZID parameter if it is UTC 

440 """ 

441 jcal = { 

442 k.lower(): self._parameter_value_to_jcal(v) 

443 for k, v in self.items() 

444 if k.lower() != "value" 

445 } 

446 if exclude_utc and jcal.get("tzid") == "UTC": 

447 del jcal["tzid"] 

448 return jcal 

449 

450 @single_string_parameter 

451 def tzid(self) -> str | None: 

452 """The TZID parameter from :rfc:`5545`.""" 

453 

454 def is_utc(self): 

455 """Whether the TZID parameter is UTC.""" 

456 return self.tzid == "UTC" 

457 

458 def update_tzid_from(self, dt: datetime | time | Any) -> None: 

459 """Update the TZID parameter from a datetime object. 

460 

461 This sets the TZID parameter or deletes it according to the datetime. 

462 """ 

463 if isinstance(dt, (datetime, time)): 

464 self.tzid = tzid_from_dt(dt) 

465 

466 @classmethod 

467 def from_jcal(cls, jcal: dict[str : str | list[str]]): 

468 """Parse jCal parameters.""" 

469 if not isinstance(jcal, dict): 

470 raise JCalParsingError("The parameters must be a mapping.", cls) 

471 for name, value in jcal.items(): 

472 if not isinstance(name, str): 

473 raise JCalParsingError( 

474 "All parameter names must be strings.", cls, value=name 

475 ) 

476 if not ( 

477 ( 

478 isinstance(value, list) 

479 and all(isinstance(v, (str, int, float)) for v in value) 

480 and value 

481 ) 

482 or isinstance(value, (str, int, float)) 

483 ): 

484 raise JCalParsingError( 

485 "Parameter values must be a string, integer or " 

486 "float or a list of those.", 

487 cls, 

488 name, 

489 value=value, 

490 ) 

491 return cls(jcal) 

492 

493 @classmethod 

494 def from_jcal_property(cls, jcal_property: list): 

495 """Create the parameters for a jCal property. 

496 

497 Args: 

498 jcal_property (list): The jCal property [name, params, value, ...] 

499 default_value (str, optional): The default value of the property. 

500 If this is given, the default value will not be set. 

501 """ 

502 if not isinstance(jcal_property, list) or len(jcal_property) < 4: 

503 raise JCalParsingError( 

504 "The property must be a list with at least 4 items.", cls 

505 ) 

506 jcal_params = jcal_property[1] 

507 with JCalParsingError.reraise_with_path_added(1): 

508 self = cls.from_jcal(jcal_params) 

509 if self.is_utc(): 

510 del self.tzid # we do not want this parameter 

511 return self 

512 

513 

514def escape_string(val): 

515 # f'{i:02X}' 

516 return ( 

517 val.replace(r"\,", "%2C") 

518 .replace(r"\:", "%3A") 

519 .replace(r"\;", "%3B") 

520 .replace(r"\\", "%5C") 

521 ) 

522 

523 

524def unescape_string(val): 

525 return ( 

526 val.replace("%2C", ",") 

527 .replace("%3A", ":") 

528 .replace("%3B", ";") 

529 .replace("%5C", "\\") 

530 ) 

531 

532 

533_unescape_backslash_regex = re.compile(r"\\([\\,;:nN])") 

534 

535 

536def unescape_backslash(val: str): 

537 r"""Unescape backslash sequences in iCalendar text. 

538 

539 Unlike :py:meth:`unescape_string`, this only handles actual backslash escapes 

540 per :rfc:`5545`, not URL encoding. This preserves URL-encoded values 

541 like ``%3A`` in URLs. 

542 

543 Processes backslash escape sequences in a single pass using regex matching. 

544 """ 

545 return _unescape_backslash_regex.sub( 

546 lambda m: "\n" if m.group(1) in "nN" else m.group(1), val 

547 ) 

548 

549 

550RFC_6868_UNESCAPE_REGEX = re.compile(r"\^\^|\^n|\^'") 

551 

552 

553def rfc_6868_unescape(param_value: str) -> str: 

554 """Take care of :rfc:`6868` unescaping. 

555 

556 - ^^ -> ^ 

557 - ^n -> system specific newline 

558 - ^' -> " 

559 - ^ with others stay intact 

560 """ 

561 replacements = { 

562 "^^": "^", 

563 "^n": os.linesep, 

564 "^'": '"', 

565 } 

566 return RFC_6868_UNESCAPE_REGEX.sub( 

567 lambda m: replacements.get(m.group(0), m.group(0)), param_value 

568 ) 

569 

570 

571RFC_6868_ESCAPE_REGEX = re.compile(r'\^|\r\n|\r|\n|"') 

572 

573 

574def rfc_6868_escape(param_value: str) -> str: 

575 """Take care of :rfc:`6868` escaping. 

576 

577 - ^ -> ^^ 

578 - " -> ^' 

579 - newline -> ^n 

580 """ 

581 replacements = { 

582 "^": "^^", 

583 "\n": "^n", 

584 "\r": "^n", 

585 "\r\n": "^n", 

586 '"': "^'", 

587 } 

588 return RFC_6868_ESCAPE_REGEX.sub( 

589 lambda m: replacements.get(m.group(0), m.group(0)), param_value 

590 ) 

591 

592 

593def unescape_list_or_string(val): 

594 if isinstance(val, list): 

595 return [unescape_string(s) for s in val] 

596 return unescape_string(val) 

597 

598 

599######################################### 

600# parsing and generation of content lines 

601 

602 

603class Contentline(str): 

604 """A content line is basically a string that can be folded and parsed into 

605 parts. 

606 """ 

607 

608 __slots__ = ("strict",) 

609 

610 def __new__(cls, value, strict=False, encoding=DEFAULT_ENCODING): 

611 value = to_unicode(value, encoding=encoding) 

612 assert "\n" not in value, ( 

613 "Content line can not contain unescaped new line characters." 

614 ) 

615 self = super().__new__(cls, value) 

616 self.strict = strict 

617 return self 

618 

619 @classmethod 

620 def from_parts( 

621 cls, 

622 name: ICAL_TYPE, 

623 params: Parameters, 

624 values, 

625 sorted: bool = True, # noqa: A002, FBT001 

626 ): 

627 """Turn a parts into a content line.""" 

628 assert isinstance(params, Parameters) 

629 if hasattr(values, "to_ical"): 

630 values = values.to_ical() 

631 else: 

632 from icalendar.prop import vText 

633 

634 values = vText(values).to_ical() 

635 # elif isinstance(values, basestring): 

636 # values = escape_char(values) 

637 

638 # TODO: after unicode only, remove this 

639 # Convert back to unicode, after to_ical encoded it. 

640 name = to_unicode(name) 

641 values = to_unicode(values) 

642 if params: 

643 params = to_unicode(params.to_ical(sorted=sorted)) 

644 if params: 

645 # some parameter values can be skipped during serialization 

646 return cls(f"{name};{params}:{values}") 

647 return cls(f"{name}:{values}") 

648 

649 def parts(self) -> tuple[str, Parameters, str]: 

650 """Split the content line into ``name``, ``parameters``, and ``values`` parts. 

651 

652 Properly handles escaping with backslashes and double-quote sections 

653 to avoid corrupting URL-encoded characters in values. 

654 

655 Example with parameter: 

656 

657 .. code-block:: text 

658 

659 DESCRIPTION;ALTREP="cid:part1.0001@example.org":The Fall'98 Wild 

660 

661 Example without parameters: 

662 

663 .. code-block:: text 

664 

665 DESCRIPTION:The Fall'98 Wild 

666 """ 

667 try: 

668 name_split: int | None = None 

669 value_split: int | None = None 

670 in_quotes: bool = False 

671 escaped: bool = False 

672 

673 for i, ch in enumerate(self): 

674 if ch == '"' and not escaped: 

675 in_quotes = not in_quotes 

676 elif ch == "\\" and not in_quotes: 

677 escaped = True 

678 continue 

679 elif not in_quotes and not escaped: 

680 # Find first delimiter for name 

681 if ch in ":;" and name_split is None: 

682 name_split = i 

683 # Find value delimiter (first colon) 

684 if ch == ":" and value_split is None: 

685 value_split = i 

686 

687 escaped = False 

688 

689 # Validate parsing results 

690 if not value_split: 

691 # No colon found - value is empty, use end of string 

692 value_split = len(self) 

693 

694 # Extract name - if no delimiter, 

695 # take whole string for validate_token to reject 

696 name = self[:name_split] if name_split else self 

697 validate_token(name) 

698 

699 if not name_split or name_split + 1 == value_split: 

700 # No delimiter or empty parameter section 

701 raise ValueError("Invalid content line") # noqa: TRY301 

702 # Parse parameters - they still need to be escaped/unescaped 

703 # for proper handling of commas, semicolons, etc. in parameter values 

704 param_str = escape_string(self[name_split + 1 : value_split]) 

705 params = Parameters.from_ical(param_str, strict=self.strict) 

706 params = Parameters( 

707 (unescape_string(key), unescape_list_or_string(value)) 

708 for key, value in iter(params.items()) 

709 ) 

710 # Unescape backslash sequences in values but preserve URL encoding 

711 values = unescape_backslash(self[value_split + 1 :]) 

712 except ValueError as exc: 

713 raise ValueError( 

714 f"Content line could not be parsed into parts: '{self}': {exc}" 

715 ) from exc 

716 return (name, params, values) 

717 

718 @classmethod 

719 def from_ical(cls, ical, strict=False): 

720 """Unfold the content lines in an iCalendar into long content lines.""" 

721 ical = to_unicode(ical) 

722 # a fold is carriage return followed by either a space or a tab 

723 return cls(UFOLD.sub("", ical), strict=strict) 

724 

725 def to_ical(self): 

726 """Long content lines are folded so they are less than 75 characters 

727 wide. 

728 """ 

729 return foldline(self).encode(DEFAULT_ENCODING) 

730 

731 

732class Contentlines(list): 

733 """I assume that iCalendar files generally are a few kilobytes in size. 

734 Then this should be efficient. for Huge files, an iterator should probably 

735 be used instead. 

736 """ 

737 

738 def to_ical(self): 

739 """Simply join self.""" 

740 return b"\r\n".join(line.to_ical() for line in self if line) + b"\r\n" 

741 

742 @classmethod 

743 def from_ical(cls, st): 

744 """Parses a string into content lines.""" 

745 st = to_unicode(st) 

746 try: 

747 # a fold is carriage return followed by either a space or a tab 

748 unfolded = UFOLD.sub("", st) 

749 lines = cls(Contentline(line) for line in NEWLINE.split(unfolded) if line) 

750 lines.append("") # '\r\n' at the end of every content line 

751 except Exception as e: 

752 raise ValueError("Expected StringType with content lines") from e 

753 return lines 

754 

755 

756__all__ = [ 

757 "FOLD", 

758 "NAME", 

759 "NEWLINE", 

760 "QUNSAFE_CHAR", 

761 "QUOTABLE", 

762 "UFOLD", 

763 "UNSAFE_CHAR", 

764 "Contentline", 

765 "Contentlines", 

766 "Parameters", 

767 "dquote", 

768 "escape_char", 

769 "escape_string", 

770 "foldline", 

771 "param_value", 

772 "q_join", 

773 "q_split", 

774 "rfc_6868_escape", 

775 "rfc_6868_unescape", 

776 "unescape_backslash", 

777 "unescape_char", 

778 "unescape_list_or_string", 

779 "unescape_string", 

780 "validate_param_value", 

781 "validate_token", 

782]