add proxy for the first part and try to develop the second part
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user