Files
Django_kube/Webcal-python/venv/lib/python3.11/site-packages/ics/grammar/parse.py
T

228 lines
6.6 KiB
Python

import collections
import re
from typing import Dict, List
import attr
import tatsu
from ics.grammar.contentline import contentlineParser
# grammar_path = Path(__file__).parent.joinpath('contentline.ebnf')
#
# with open(grammar_path) as fd:
# GRAMMAR = tatsu.compile(fd.read())
GRAMMAR = contentlineParser()
@attr.s(auto_exc=True)
class ParseError(Exception):
line: str = attr.ib(default="")
nr: int = attr.ib(default=-1)
class ContentLine:
"""
Represents one property line.
For example:
``FOO;BAR=1:YOLO`` is represented by
``ContentLine('FOO', {'BAR': ['1']}, 'YOLO'))``
Args:
name: The name of the property (uppercased for consistency and
easier comparison)
params: A map name:list of values
value: The value of the property
"""
def __eq__(self, other):
ret = (self.name == other.name and
self.params == other.params and
self.value == other.value)
return ret
def __ne__(self, other):
return not self.__eq__(other)
def __init__(self, name: str, params: Dict[str, List[str]] = {}, value: str = ''):
self.name = name.upper()
self.params = params
self.value = value
def __str__(self):
params_str = ''
for pname in self.params:
params_str += ';{}={}'.format(pname, ','.join(self.params[pname]))
return "{}{}:{}".format(self.name, params_str, self.value)
def __repr__(self):
return "<ContentLine '{}' with {} parameter{}. Value='{}'>".format(
self.name,
len(self.params),
"s" if len(self.params) > 1 else "",
self.value,
)
def __getitem__(self, item):
return self.params[item]
def __setitem__(self, item, *values):
self.params[item] = [val for val in values]
@classmethod
def parse(cls, line, nr=-1):
"""Parse a single iCalendar-formatted line into a ContentLine"""
if "\n" in line or "\r" in line:
raise ValueError("ContentLine can only contain escaped newlines")
try:
ast = GRAMMAR.parse(line)
except tatsu.exceptions.FailedToken:
raise ParseError(line, nr)
name = ''.join(ast['name'])
value = ''.join(ast['value'])
params = {}
for param_ast in ast.get('params', []):
param_name = ''.join(param_ast["name"])
param_values = [''.join(x["value"]) for x in param_ast["values"]]
params[param_name] = param_values
return cls(name, params, value)
def clone(self):
"""Makes a copy of itself"""
# dict(self.params) -> Make a copy of the dict
return self.__class__(self.name, dict(self.params), self.value)
class Container(list):
"""Represents an iCalendar object.
Contains a list of ContentLines or Containers.
Args:
name: the name of the object (VCALENDAR, VEVENT etc.)
items: Containers or ContentLines
"""
def __init__(self, name: str, *items: List):
super(Container, self).__init__(items)
self.name = name
def __str__(self):
name = self.name
ret = ['BEGIN:' + name]
for line in self:
ret.append(str(line))
ret.append('END:' + name)
return "\r\n".join(ret)
def __repr__(self):
return "<Container '{}' with {} element{}>" \
.format(self.name, len(self), "s" if len(self) > 1 else "")
@classmethod
def parse(cls, name, tokenized_lines):
items = []
for line in tokenized_lines:
if line.name == 'BEGIN':
items.append(Container.parse(line.value, tokenized_lines))
elif line.name == 'END':
if line.value != name:
raise ParseError(
"Expected END:{}, got END:{}".format(name, line.value))
break
else:
items.append(line)
return cls(name, *items)
def clone(self):
"""Makes a copy of itself"""
c = self.__class__(self.name)
for elem in self:
c.append(elem.clone())
return c
def unfold_lines(physical_lines, with_linenr=False):
if not isinstance(physical_lines, collections.abc.Iterable):
raise ParseError('Parameter `physical_lines` must be an iterable')
current_nr = -1
current_line = ''
for nr, line in enumerate(physical_lines):
if len(line.strip()) == 0:
continue
elif not current_line:
current_nr = nr
current_line = line.strip('\r')
elif line[0] in (' ', '\t'):
current_line += line[1:].strip('\r')
else:
if with_linenr:
yield current_nr, current_line
else:
yield current_line
current_nr = nr
current_line = line.strip('\r')
if current_line:
if with_linenr:
yield current_nr, current_line
else:
yield current_line
def tokenize_line(unfolded_lines):
for line in unfolded_lines:
if isinstance(line, tuple):
yield ContentLine.parse(line[1], line[0])
else:
yield ContentLine.parse(line)
def parse(tokenized_lines, block_name=None):
res = []
for line in tokenized_lines:
if line.name == 'BEGIN':
res.append(Container.parse(line.value, tokenized_lines))
else:
res.append(line)
return res
def lines_to_container(lines, linewise=True):
if linewise:
return parse(tokenize_line(unfold_lines(lines, with_linenr=True))) # linewise
else:
return string_to_container("\r\n".join(lines), linewise) # full-string
def string_to_container(txt, linewise=True):
if linewise:
return lines_to_container(txt.split("\n"), linewise) # linewise
else:
return parse(string_to_content_lines(txt)) # full-string
def string_to_content_lines(txt):
txt = re.sub("\r?\n[ \t]", "", txt)
ast = GRAMMAR.parse(txt, rule_name='full')
for line in ast:
line = line[0]
name = ''.join(line['name'])
value = ''.join(line['value'])
params = {}
for param_ast in line.get('params', []):
param_name = ''.join(param_ast["name"])
param_values = [''.join(x["value"]) for x in param_ast["values"]]
params[param_name] = param_values
yield ContentLine(name, params, value)
def calendar_string_to_containers(string):
if not isinstance(string, str):
raise TypeError("Expecting a string")
return string_to_container(string)