many changes
This commit is contained in:
parent
72a3073a5b
commit
1206aaab74
|
@ -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
|
||||
})
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
107
izzylib/misc.py
107
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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
125
izzylib/sql/table.py
Normal 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]
|
Loading…
Reference in a new issue