diff --git a/izzylib/http_server_async/misc.py b/izzylib/http_server_async/misc.py new file mode 100644 index 0000000..b06e83f --- /dev/null +++ b/izzylib/http_server_async/misc.py @@ -0,0 +1,300 @@ +from datetime import datetime, timezone, timedelta + +from .. import logging, boolean +from ..dotdict import DotDict +from ..path import Path + + +UtcTime = timezone.utc +LocalTime = datetime.now(UtcTime).astimezone().tzinfo + +cookie_params = { + 'Expires': 'expires', + 'Max-Age': 'maxage', + 'Domain': 'domain', + 'Path': 'path', + 'SameSite': 'samesite', + 'Secure': 'secure', + 'HttpOnly': 'httponly' +} + + +class Cookies(DotDict): + def __setitem__(self, key, value): + if type(value) != CookieItem: + value = CookieItem(key, value) + + super().__setitem__(key, value) + + +class Headers(DotDict): + def __getattr__(self, key): + return self[key.replace('_', '-')] + + + def __setattr__(self, key, value): + self[key.replace('_', '-')] = value + + + def __getitem__(self, key): + return super().__getitem__(key.title()) + + + def __setitem__(self, key, value): + key = key.title() + + if key in ['Cookie', 'Set-Cookie']: + logging.warning('Do not set the "Cookie" or "Set-Cookie" headers') + return + + try: + self[key].append(value) + + except KeyError: + super().__setitem__(key, HeaderItem(key, value)) + + + def getone(self, key, default=None): + try: + return self[key].one() + + except (KeyError, IndexError): + return default + + + def setall(self, key, value): + self[key].set(value) + + + #def update(self, data): + #for k,v in data.items(): + #self.__setitem__(k,v) + + +class CookieItem: + def __init__(self, key, value, **kwargs): + self.key = key + self.value = value + self.args = DotDict() + + for k,v in kwargs.items(): + if k not in cookie_params.values(): + raise AttributeError(f'Not a valid cookie parameter: {key}') + + setattr(self, k, v) + + + def __str__(self): + text = f'{self.key}={self.value}' + + if self.expires: + text += f'; Expires={self.expires.strftime("%a, %d %b %Y %H:%M:%S GMT")}' + + if self.maxage != None: + text += f'; Max-Age={self.maxage}' + + if self.domain: + text += f'; Domain={self.domain}' + + if self.path: + text += f'; Path={self.path}' + + if self.samesite: + text += f'; SameSite={self.samesite}' + + if self.secure: + text += f'; Secure' + + if self.httponly: + text += f'; HttpOnly' + + return text + + + @classmethod + def from_string(cls, data): + kwargs = {} + + for idx, pairs in enumerate(data.split(';')): + try: + k, v = pairs.split('=', 1) + v = v.strip() + + except: + k, v = pairs, True + + k = k.replace(' ', '') + + if isinstance(v, str) and v.startswith('"') and v.endswith('"'): + v = v[1:-1] + + if idx == 0: + key = k + value = v + + elif k in cookie_params.keys(): + kwargs[cookie_params[k]] = v + + else: + logging.info(f'Invalid key/value pair for cookie: {k} = {v}') + + return cls(key, value, **kwargs) + + + @property + def expires(self): + return self.args.get('Expires') + + + @expires.setter + def expires(self, data): + if isinstance(data, str): + data = datetime.strptime(data, '%a, %d %b %Y %H:%M:%S GMT').replace(tzinfo=UtcTime) + + elif isinstance(data, int) or isinstance(data, float): + data = datetime.fromtimestamp(data).replace(tzinfo=UtcTime) + + elif isinstance(data, datetime): + if not data.tzinfo: + data = data.replace(tzinfo=UtcTime) + + elif isinstance(data, timedelta): + data = datetime.now(UtcTime) + data + + else: + raise TypeError(f'Expires must be a http date string, timestamp, or datetime object, not {data.__class__.__name__}') + + self.args['Expires'] = data + + + @property + def maxage(self): + return self.args.get('Max-Age') + + + @maxage.setter + def maxage(self, data): + if isinstance(data, int): + pass + + elif isinstance(date, timedelta): + data = data.seconds + + elif isinstance(date, datetime): + data = (datetime.now() - date).seconds + + else: + raise TypeError(f'Max-Age must be an integer, timedelta object, or datetime object, not {data.__class__.__name__}') + + self.args['Max-Age'] = data + + + @property + def domain(self): + return self.args.get('Domain') + + + @domain.setter + def domain(self, data): + if not isinstance(data, str): + raise ValueError(f'Domain must be a string, not {data.__class__.__name__}') + + self.args['Domain'] = data + + + @property + def path(self): + return self.args.get('Path') + + + @path.setter + def path(self, data): + if not isinstance(data, str): + raise ValueError(f'Path must be a string or izzylib.Path object, not {data.__class__.__name__}') + + self.args['Path'] = Path(data) + + + @property + def secure(self): + return self.args.get('Secure') + + + @secure.setter + def secure(self, data): + self.args['Secure'] = boolean(data) + + + @property + def httponly(self): + return self.args.get('HttpOnly') + + + @httponly.setter + def httponly(self, data): + self.args['HttpOnly'] = boolean(data) + + + @property + def samesite(self): + return self.args.get('SameSite') + + + @samesite.setter + def samesite(self, data): + if isinstance(data, bool): + data = 'Strict' if data else 'None' + + elif isinstance(data, str) and data.title() in ['Strict', 'Lax', 'None']: + data = data.title() + + else: + raise TypeError(f'SameSite must be a boolean or one of Strict, Lax, or None, not {data.__class__.__name__}') + + self.args['SameSite'] = data + self.args['Secure'] = True + + + def as_dict(self): + data = DotDict({self.key: self.value}) + data.update(self.args) + + return data + + + def set_defaults(self): + for key in list(self.args.keys()): + del self.args[key] + + + def set_delete(self): + self.args.pop('Expires', None) + self.maxage = 0 + + return self + + +class HeaderItem(list): + def __init__(self, key, values): + super().__init__() + self.update(values) + + self.key = key + + + def __str__(self): + return ','.join(str(v) for v in self) + + + def set(self, *value): + self.clear() + self.append(value) + + + def one(self): + return self[0] + + + def update(self, *items): + for item in items: + self.append(item)