From 1206aaab74a4ddbed975d02d61109d90d3e1a527 Mon Sep 17 00:00:00 2001 From: Izalia Mae Date: Mon, 15 Nov 2021 03:01:42 -0500 Subject: [PATCH] many changes --- izzylib/activitypub.py | 140 +++++++++++++++++++++-- izzylib/http_server_async/application.py | 13 ++- izzylib/misc.py | 107 ++++++++++++++++- izzylib/path.py | 45 ++++++-- izzylib/sql/__init__.py | 2 +- izzylib/sql/column.py | 54 --------- izzylib/sql/database.py | 7 +- izzylib/sql/table.py | 125 ++++++++++++++++++++ 8 files changed, 412 insertions(+), 81 deletions(-) delete mode 100644 izzylib/sql/column.py create mode 100644 izzylib/sql/table.py diff --git a/izzylib/activitypub.py b/izzylib/activitypub.py index 1ced8bc..4a6e918 100644 --- a/izzylib/activitypub.py +++ b/izzylib/activitypub.py @@ -5,7 +5,7 @@ from functools import partial from typing import Union from .dotdict import DotDict -from .misc import DateString, Url +from .misc import DateString, Url, boolean pubstr = 'https://www.w3.org/ns/activitystreams#Public' @@ -233,7 +233,7 @@ class Object(DotDict): data.attachment.append(PropertyValue(key, value)) if kwargs.get('avatar_url'): - data.icon = Object.new_icon(kwargs.get('avatar_url'), kwargs.get('avatar_type')) + data.icon = Object.new_image(kwargs.get('avatar_url'), kwargs.get('avatar_type')) # need to add data when "full" is true if not full: @@ -247,7 +247,6 @@ class Object(DotDict): return data - @classmethod def new_activity(cls, id: str, type: str, actor_src: Union[str, dict], object: Union[str, dict], to: list=[pubstr], cc: list=[]): assert type in activity_types @@ -274,12 +273,49 @@ class Object(DotDict): }) +class Media(Object): @classmethod - def new_image(cls, url, type=None): - return cls({ - 'type': 'Image', - 'mediaType': type or mimetypes.guess_type(url)[0] or 'image/png', - 'url': url + def new(cls, type, url, mime=None): + return cls( + type = 'Image', + mediaType = mime or mimetypes.guess_type(url)[0] or 'image/png', + url = url + ) + + + @classmethod + def new_image(cls, url, mime=None): + return cls.new('Image', url, mime) + + + @classmethod + def new_video(cls, url, mime=None): + return cls.new('Video', url, mime) + + + @classmethod + def new_audio(cls, url, mime=None): + return cls.new('Audio', url, mime) + + +class Activity(DotDict): + @property + def privacy_level(self): + return parse_privacy_level(self.get('to', []), self.get('cc', [])) + + + @classmethod + def new_activity(cls, id: str, type: str, actor_src: Union[str, dict], object: Union[str, dict], to: list=[pubstr], cc: list=[]): + assert type in activity_types + + cls({ + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': id, + 'object': object, + 'type': type, + 'to': to, + 'cc': cc, + 'actor': actor_src }) @@ -319,3 +355,91 @@ class PropertyValue(DotDict): def set_pair(self, key, value): self.name = key self.value = value + + +class Nodeinfo(DotDict): + @classmethod + def new_20(cls, name, version, **metadata): + return cls.new(name, version, '2.0', **metadata) + + + @classmethod + def new_21(cls, name, version, **metadata): + return cls.new(name, version, '2.1', **metadata) + + + @classmethod + def new(cls, name, version, niversion='2.1', **kwargs): + print(name, version, niversion) + assert niversion in ['2.0', '2.1'] + + open_regs = boolean(kwargs.pop('open_regs', True)) + posts = int(kwargs.pop('posts', 0)) + users = int(kwargs.pop('users', 0)) + users_halfyear = int(kwargs.pop('halfyear', 0)) + users_month = int(kwargs.pop('month', 0)) + comments = int(kwargs.pop('comments', 0)) + repository = kwargs.pop('repository', None) + homepage = kwargs.pop('homepage', None) + + data = cls( + version = niversion, + openRegistrations = open_regs, + software = { + 'name': name.lower().replace(' ', '-'), + 'version': version + }, + usage = { + 'users': { + 'total': users + } + }, + protocols = [ + 'activitypub' + ], + services = { + 'inbound': kwargs.pop('inbound', []), + 'outbound': kwargs.pop('outbound', []) + }, + metadata = kwargs + ) + + if data.version == '2.1': + if repository: + data.software.repository = repository + + if homepage: + data.software.homepage = homepage + + if users_halfyear: + data.users.activeHalfyear = halfyear + + if users_month: + data.users.activeMonth = month + + if posts: + data.usage.localPosts = posts + + if comments: + data.usage.localComments = comments + + return data + + +class WellknownNodeinfo(DotDict): + @classmethod + def new(cls, path, version='2.1'): + data = cls(links=[]) + data.append(path, version) + + return data + + + def append(self, path, version='2.1'): + assert version in ['2.0', '2.1'] + + self.links.append({ + 'rel': f'http://nodeinfo.dispora.software/ns/schema/{version}', + 'href': path + }) + diff --git a/izzylib/http_server_async/application.py b/izzylib/http_server_async/application.py index c532b9b..5f5d81a 100644 --- a/izzylib/http_server_async/application.py +++ b/izzylib/http_server_async/application.py @@ -17,6 +17,8 @@ from ..misc import signal_handler from ..path import Path from ..template import Template +try: from ..sql import Database +except ImportError: Database = None frontend = Path(__file__).resolve().parent.parent.join('http_frontend') @@ -24,8 +26,9 @@ frontend = Path(__file__).resolve().parent.parent.join('http_frontend') class ApplicationBase: ctx = DotDict() - def __init__(self, views=[], middleware=[], **kwargs): + def __init__(self, views=[], middleware=[], dbtype=None, dbargs={}, **kwargs): self.cfg = Config(**kwargs) + self.db = None self.router = Router(trim_last_slash=True) self.middleware = DotDict({'request': [], 'response': []}) @@ -35,6 +38,12 @@ class ApplicationBase: for mw in middleware: self.add_middleware(mw) + if dbtype or dbargs: + if not Database: + raise NotImplementedError('Failed to import SQL database class') + + self.db = Database(dbtype, **dbargs) + def __getitem__(self, key): return self.ctx[key] @@ -210,7 +219,7 @@ class Application(ApplicationBase): task.cancel() self._tasks.remove(task) - signal_handler() + signal_handler(None) def start(self, *tasks, log=True): diff --git a/izzylib/misc.py b/izzylib/misc.py index 1204fe9..d939dc8 100644 --- a/izzylib/misc.py +++ b/izzylib/misc.py @@ -1,4 +1,16 @@ -import grp, hashlib, os, platform, random, shlex, signal, socket, statistics, string, time, timeit +import argparse +import grp +import hashlib +import os +import platform +import random +import shlex +import signal +import socket +import statistics +import string +import time +import timeit from datetime import datetime, timezone from getpass import getpass, getuser @@ -30,6 +42,7 @@ __all__ = [ 'time_function_pprint', 'timestamp', 'var_name', + 'ArgParser', 'DateString', 'Url' ] @@ -42,13 +55,37 @@ datetime_formats = { } +def app_data_dirs(author, name): + if platform.system() == 'Linux': + config = Path('~/.config') + cache = Path('~/.cache') + + elif platform.system() == 'Darwin': + config = Path('~/Application Support') + cache = Path('~/Library/Caches') + + elif platform.system() == 'Windows': + return DotDict( + config = Path('~/Application Data/Local Settings').join(author).join(name), + cache = Path('~/Application Data').join(author).join(name).join('Cache') + ) + + else: + raise TypeError(f'Unknown system: {platform.system()}') + + return DotDict( + config = config.join(author).join(name), + cache = cache.join(author).join(name) + ) + + def boolean(value, return_value=False): ''' Convert a str, bool, int or None object into a boolean. Arguments: value (str, bool, int, None): The value to be checked - return_value (bool): If True, return v instead of True if it can't be converted + return_value (bool): If True, return value instead of True if it can't be converted Return: various: A boolean or the value itself @@ -469,6 +506,72 @@ def var_name(single=True, **kwargs): return key[0] if single else keys +class ArgParser(argparse.ArgumentParser): + def __init__(self, *args): + super().__init__() + + self._args = None + self._arguments = DotDict() + + + def __getitem__(self, key): + return self._arguments[key] + + + def __setitem__(self, key, value): + if not isinstance(value, dict): + raise TypeError(f'Value must be a dict, not {type(value).__name__}') + + self._arguments[key] = Argument(**value) + + + def add_positional(self, name, **kwargs): + self[name] = Argument(name=name, **kwargs) + + + def add_optional(self, name, short_name=None, **kwargs): + self[name] = Argument(name=f'--{name}', short_name=f'-{short_name}', **kwargs) + + + def add_action(self, name, callback, *args, argument=None, **kwargs): + if argument: + self[name] = argument + + self[name].set_callback(callback, *args, **kwargs) + + + @property + def args(self): + if not self._args: + self._args = self.parse_args() + + return self._args + + +class Argument(DotDict): + valid_keys = ['name', 'short_name', 'nargs', 'default', 'type', 'help', 'metavar'] + callback = None + + def __setitem__(self, key, value): + if key not in self.valid_keys: + raise KeyError(f'Not a valid argument option: {key}') + + if not value: + return + + if key == 'name': + if not self.metavar: + super().__setitem__('metavar', value) + + value = value.replace(' ', '-', '_', '-') + + super().__setitem__(key, value) + + + def set_callback(self, callback, *args, **kwargs): + self.callback = lambda *cliargs: callback(*cliargs, *args, **kwargs) + + class DateString(str): tz_utc = timezone.utc tz_local = datetime.now(tz_utc).astimezone().tzinfo diff --git a/izzylib/path.py b/izzylib/path.py index 27e66b3..d584d2e 100644 --- a/izzylib/path.py +++ b/izzylib/path.py @@ -1,4 +1,4 @@ -import json, os, shutil +import json, os, shutil, sys from datetime import datetime from functools import cached_property @@ -16,6 +16,14 @@ class PathMeta(type): return cls(os.getcwd()).resolve() + @property + def script(cls): + try: path = sys.modules['__main__'].__file__ + except: path = sys.argv[0] + + return Path(path).parent + + class Path(str, metaclass=PathMeta): def __init__(self, path=os.getcwd(), exist=True, missing=True, parents=True): #if str(path).startswith('~'): @@ -54,13 +62,28 @@ class Path(str, metaclass=PathMeta): def __check_dir(self, path=None): target = self if not path else Path(path) - if not self.config['parents'] and not target.parent.exists: + if not self.parents and not target.parent.exists: raise FileNotFoundError('Parent directories do not exist:', target) - if not self.config['exist'] and target.exists: + if not self.exist and target.exists: raise FileExistsError('File or directory already exists:', target) + @property + def missing(self): + return self.config.missing + + + @property + def exist(self): + return self.config.exist + + + @property + def parents(self): + return self.config.parents + + @cached_property def isdir(self): return os.path.isdir(self) @@ -137,7 +160,7 @@ class Path(str, metaclass=PathMeta): except FileNotFoundError: pass - elif not self.config.exist: + elif not self.exist: raise FileExistsError(target) shutil.copy2(self, target) @@ -145,7 +168,7 @@ class Path(str, metaclass=PathMeta): def delete(self): - if not self.exists() and not self.config.exist: + if not self.exists() and not self.exist: raise FileNotFoundError(self) if self.isdir: @@ -170,8 +193,8 @@ class Path(str, metaclass=PathMeta): return tuple(sorted(self.join(path) for path in paths)) - def join(self, path): - return Path(os.path.join(self, path)) + def join(self, *paths): + return Path(os.path.join(self, *paths)) def json_load(self): @@ -190,7 +213,7 @@ class Path(str, metaclass=PathMeta): self.__check_dir(path) if target.exists(): - if not self.config.exist: + if not self.exist: raise FileExistsError(target) target.delete() @@ -207,11 +230,11 @@ class Path(str, metaclass=PathMeta): def mkdir(self, mode=0o755): - if self.config.parents: - os.makedirs(self, mode, exist_ok=self.config.exist) + if self.parents: + os.makedirs(self, mode, exist_ok=self.exist) else: - os.makedir(self, mode, exist_ok=self.config.exist) + os.makedir(self, mode, exist_ok=self.exist) return self.exists diff --git a/izzylib/sql/__init__.py b/izzylib/sql/__init__.py index 3591eac..2bd4438 100644 --- a/izzylib/sql/__init__.py +++ b/izzylib/sql/__init__.py @@ -1,7 +1,7 @@ ## Normal SQL client from .database import Database, OperationalError, ProgrammingError from .session import Session -from .column import Column +from .table import Column, Table, Tables from .rows import Row, RowClasses ## Sqlite server diff --git a/izzylib/sql/column.py b/izzylib/sql/column.py deleted file mode 100644 index cab364d..0000000 --- a/izzylib/sql/column.py +++ /dev/null @@ -1,54 +0,0 @@ -from sqlalchemy import ForeignKey -from sqlalchemy import ( - Column as sqlalchemy_column, - types as Types -) - - -SqlTypes = {t.lower(): getattr(Types, t) for t in dir(Types) if not t.startswith('_')} - - -class Column(sqlalchemy_column): - def __init__(self, name, stype=None, fkey=None, **kwargs): - if not stype and not kwargs: - if name == 'id': - stype = 'integer' - kwargs['primary_key'] = True - kwargs['autoincrement'] = True - - elif name == 'timestamp': - stype = 'datetime' - - else: - raise ValueError('Missing column type and options') - - stype = (stype.lower() if type(stype) == str else stype) or 'string' - - if type(stype) == str: - try: - stype = SqlTypes[stype.lower()] - - except KeyError: - raise KeyError(f'Invalid SQL data type: {stype}') - - options = [name, stype] - - if fkey: - options.append(ForeignKey(fkey)) - - super().__init__(*options, **kwargs) - - - def compile(self): - sql = f'{self.name} {self.type}' - - if not self.nullable: - sql += ' NOT NULL' - - if self.primary_key: - sql += ' PRIMARY KEY' - - if self.unique: - sql += ' UNIQUE' - - return sql diff --git a/izzylib/sql/database.py b/izzylib/sql/database.py index 7febbc8..6bea356 100644 --- a/izzylib/sql/database.py +++ b/izzylib/sql/database.py @@ -22,7 +22,7 @@ modules = dict( class Database: - def __init__(self, dbtype='sqlite', **kwargs): + def __init__(self, dbtype='sqlite', open_now=True, **kwargs): self._connect_args = [dbtype, kwargs] self.db = None self.cache = None @@ -34,7 +34,8 @@ class Database: self.session_class = kwargs.get('session_class', Session) self.sessions = {} - self.open() + if open_now: + self.open() def _setup_cache(self): @@ -53,7 +54,7 @@ class Database: @property def table(self): - return DotDict(self.meta.tables) + return self.meta.tables def get_tables(self): diff --git a/izzylib/sql/table.py b/izzylib/sql/table.py new file mode 100644 index 0000000..d7c5e5a --- /dev/null +++ b/izzylib/sql/table.py @@ -0,0 +1,125 @@ +from sqlalchemy import ForeignKey +from sqlalchemy import ( + Column as sqlalchemy_column, + types as Types +) + +from ..dotdict import DotDict + + +ptype = type +SqlTypes = {t.lower(): getattr(Types, t) for t in dir(Types) if not t.startswith('_')} + + +class Column(sqlalchemy_column): + def __init__(self, name, type=None, fkey=None, **kwargs): + if not type and not kwargs: + if name == 'id': + type = 'integer' + kwargs['primary_key'] = True + kwargs['autoincrement'] = True + + elif name in ['timestamp', 'created', 'modified', 'accessed']: + type = 'datetime' + + else: + raise ValueError('Missing column type and options') + + type = (type.lower() if ptype(type) == str else type) or 'string' + + if ptype(type) == str: + try: + type = SqlTypes[type.lower()] + + except KeyError: + raise KeyError(f'Invalid SQL data type: {type}') + + options = [name, type] + + if fkey: + options.append(ForeignKey(fkey)) + + super().__init__(*options, **kwargs) + + + def compile(self): + sql = f'{self.name} {self.type}' + + if not self.nullable: + sql += ' NOT NULL' + + if self.primary_key: + sql += ' PRIMARY KEY' + + if self.unique: + sql += ' UNIQUE' + + return sql + + +class Table(list): + __table_name__ = None + + def __init__(self, name, *columns, **kwcolumns): + super().__init__() + self.__table_name__ = name + self.columns = [] + + self.new('id') + + for column in columns: + self.add(column) + + + def new(self, name, *args, **kwargs): + self.add(Column(name, *args, **kwargs)) + + + def add(self, column: Column): + if column.name == 'id': + self.remove('id') + + elif column.name in self.columns: + # This needs to be a custom exception. Probably ColumnExistsError + raise ValueError(f'Column already exists: {column.name}') + + self.append(column) + self.columns.append(column.name) + + + def remove(self, name): + for col in self: + if col.name == name: + super().remove(name) + self.columns.remove(name) + + +class Tables(DotDict): + def __init__(self, *tables, **kwtables): + super().__init__() + + for table in tables: + self.add(table) + self[table.__table_name__] = table + + for name, table in kwtables.items(): + if isinstance(table, list): + self.new(name, *table) + + elif isinstance(table, Table): + self.add(table) + + else: + raise TypeError(f'Invalid table type for {name}: {type(table).__name__}') + + + def new(self, name, *columns): + self[name] = Table(name, *columns) + + + def add(self, table: Table): + self[table.__table_name__] = table + + + def remove(self, name): + del self[name]