many changes

This commit is contained in:
Izalia Mae 2021-11-15 03:01:42 -05:00
parent 72a3073a5b
commit 1206aaab74
8 changed files with 412 additions and 81 deletions

View file

@ -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
})

View file

@ -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):

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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):

125
izzylib/sql/table.py Normal file
View file

@ -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]