a bunch
This commit is contained in:
parent
00a5b03995
commit
b4615af139
|
@ -17,7 +17,7 @@ izzylog = logging.logger['IzzyLib']
|
|||
from .path import Path
|
||||
from .dotdict import DotDict, LowerDotDict, DefaultDotDict, MultiDotDict, JsonEncoder
|
||||
from .misc import *
|
||||
from .cache import LruCache, TtlCache
|
||||
from .cache import CacheDecorator, LruCache, TtlCache
|
||||
from .connection import Connection
|
||||
|
||||
from .http_urllib_client import HttpUrllibClient, HttpUrllibResponse
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import re
|
||||
import json, re
|
||||
|
||||
from datetime import datetime
|
||||
from collections import OrderedDict
|
||||
from functools import wraps
|
||||
from functools import update_wrapper
|
||||
from hashlib import sha1
|
||||
|
||||
from . import DotDict
|
||||
|
||||
|
@ -43,17 +44,24 @@ def parse_ttl(ttl):
|
|||
|
||||
class BaseCache(OrderedDict):
|
||||
_get = OrderedDict.get
|
||||
_items = OrderedDict.items
|
||||
def __init__(self, maxsize=1024, ttl=None):
|
||||
self.ttl = parse_ttl(ttl)
|
||||
self.maxsize = maxsize
|
||||
self.set = self.store
|
||||
|
||||
self.deco = lambda *args: CacheDecorator(self, *args)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
data = ', '.join([f'{k}="{v["data"]}"' for k,v in self.items()])
|
||||
data = ', '.join([f'{k}="{v}"' for k,v in self.items()])
|
||||
return f'BaseCache({data})'
|
||||
|
||||
|
||||
def items(self):
|
||||
return [[k, v.data] for k,v in self._items()]
|
||||
|
||||
|
||||
def get(self, key):
|
||||
while len(self) >= self.maxsize and self.maxsize > 0:
|
||||
self.popitem(last=False)
|
||||
|
@ -109,24 +117,29 @@ class BaseCache(OrderedDict):
|
|||
self[key]['timestamp'] = timestamp + self.ttl
|
||||
|
||||
self.move_to_end(key)
|
||||
return self[key].data
|
||||
return item.data
|
||||
|
||||
|
||||
## Was gonna use this for db stuff, but I need to plan it out better
|
||||
def decorator(function, key, arg=0):
|
||||
@wraps(function)
|
||||
def wrapper(*args, **kwargs):
|
||||
cached = self.fetch(key)
|
||||
## This doesn't work for some reason
|
||||
def CacheDecorator(cache):
|
||||
def decorator(func):
|
||||
def wrapper(cls, *args, **kwargs):
|
||||
key = sha1(json.dumps(args).encode() + json.dumps(kwargs).encode()).hexdigest()
|
||||
cached = cache.fetch(key)
|
||||
print(cached)
|
||||
|
||||
if cached:
|
||||
if cached != None:
|
||||
print('Returning cached value:', cache)
|
||||
return cached
|
||||
|
||||
result = function(*args, **kwargs)
|
||||
result = func(cls, *args, **kwargs)
|
||||
|
||||
self.store(key, args[arg] if type(arg) == int else kwargs[arg])
|
||||
cache.store(key, result)
|
||||
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
class TtlCache(BaseCache):
|
||||
|
|
|
@ -95,7 +95,7 @@ def catch_kb_interrupt(function, *args, **kwargs):
|
|||
'''
|
||||
|
||||
try:
|
||||
function(*args, **kwargs)
|
||||
return function(*args, **kwargs)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
izzylog.verbose('Bye! UvU')
|
||||
|
|
1
hasher/izzylib/hasher/__init__.py
Normal file
1
hasher/izzylib/hasher/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
from .hasher import PasswordHasher
|
|
@ -54,7 +54,7 @@ class PasswordHasher:
|
|||
|
||||
|
||||
def hash(self, password: str):
|
||||
return super().hash(password)
|
||||
return self.hasher.hash(password)
|
||||
|
||||
|
||||
def verify(self, passhash: str, password: str):
|
|
@ -3,18 +3,14 @@ from setuptools import setup, find_namespace_packages
|
|||
|
||||
|
||||
requires = [
|
||||
'argon2-cffi==20.1.0',
|
||||
'pillow==8.2.0',
|
||||
'pycryptodome==3.10.1',
|
||||
'sanic==21.2.4',
|
||||
'Sanic-Cors==1.0.0'
|
||||
'argon2-cffi==20.1.0'
|
||||
]
|
||||
|
||||
|
||||
setup(
|
||||
name="IzzyLib HTTP Server",
|
||||
name="IzzyLib Password Hasher",
|
||||
version='0.6.0',
|
||||
packages=find_namespace_packages(include=['izzylib.http_server']),
|
||||
packages=find_namespace_packages(include=['izzylib.hasher']),
|
||||
python_requires='>=3.7.0',
|
||||
install_requires=requires,
|
||||
include_package_data=False,
|
|
@ -1,2 +0,0 @@
|
|||
from .hasher import PasswordHasher
|
||||
from .server import HttpServer, HttpServerRequest, HttpServerResponse
|
|
@ -1,331 +0,0 @@
|
|||
# probably gonna remove this since I'll be using my asgi framework
|
||||
import multiprocessing, sanic, signal, traceback
|
||||
import logging as pylog
|
||||
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
from multidict import CIMultiDict
|
||||
from multiprocessing import cpu_count, current_process
|
||||
from sanic.views import HTTPMethodView
|
||||
from urllib.parse import parse_qsl, urlparse
|
||||
|
||||
from . import http, izzylog
|
||||
from .misc import DotDict, DefaultDict, LowerDotDict
|
||||
from .template import Template
|
||||
|
||||
|
||||
log_path_ignore = [
|
||||
'/media',
|
||||
'/static'
|
||||
]
|
||||
|
||||
log_ext_ignore = [
|
||||
'js', 'ttf', 'woff2',
|
||||
'ac3', 'aiff', 'flac', 'm4a', 'mp3', 'ogg', 'wav', 'wma',
|
||||
'apng', 'ico', 'jpeg', 'jpg', 'png', 'svg',
|
||||
'divx', 'mov', 'mp4', 'webm', 'wmv'
|
||||
]
|
||||
|
||||
|
||||
class HttpServer(sanic.Sanic):
|
||||
def __init__(self, name='sanic', host='0.0.0.0', port='4080', **kwargs):
|
||||
self.host = host
|
||||
self.port = int(port)
|
||||
self.workers = int(kwargs.get('workers', cpu_count()))
|
||||
self.sig_handler = kwargs.get('sig_handler')
|
||||
|
||||
super().__init__(name, request_class=kwargs.get('request_class', HttpServerRequest))
|
||||
|
||||
for log in ['sanic.root', 'sanic.access']:
|
||||
pylog.getLogger(log).setLevel(pylog.ERROR)
|
||||
|
||||
self.template = Template(
|
||||
kwargs.get('tpl_search', []),
|
||||
kwargs.get('tpl_globals', {}),
|
||||
kwargs.get('tpl_context'),
|
||||
kwargs.get('tpl_autoescape', True)
|
||||
)
|
||||
|
||||
self.template.addEnv('app', self)
|
||||
|
||||
self.error_handler.add(TemplateNotFound, NoTemplateError)
|
||||
self.error_handler.add(Exception, kwargs.get('error_handler', GenericError))
|
||||
self.register_middleware(MiddlewareAccessLog, attach_to='response')
|
||||
|
||||
signal.signal(signal.SIGHUP, self.finish)
|
||||
signal.signal(signal.SIGINT, self.finish)
|
||||
signal.signal(signal.SIGQUIT, self.finish)
|
||||
signal.signal(signal.SIGTERM, self.finish)
|
||||
|
||||
|
||||
## Sanic spits out a warning, so this is the workaround to stop it
|
||||
def __setattr__(self, key, value):
|
||||
object.__setattr__(self, key, value)
|
||||
|
||||
|
||||
def add_method_route(self, method, *routes):
|
||||
for route in routes:
|
||||
self.add_route(method.as_view(), route)
|
||||
|
||||
|
||||
def add_method_routes(self, routes: list):
|
||||
for route in routes:
|
||||
self.add_method_route(*route)
|
||||
|
||||
|
||||
def start(self):
|
||||
options = {
|
||||
'host': self.host,
|
||||
'port': self.port,
|
||||
'workers': self.workers,
|
||||
'access_log': False,
|
||||
'debug': False
|
||||
}
|
||||
|
||||
msg = f'Starting {self.name} at {self.host}:{self.port}'
|
||||
|
||||
if self.workers > 1:
|
||||
msg += f' with {self.workers} workers'
|
||||
|
||||
izzylog.info(msg)
|
||||
self.run(**options)
|
||||
|
||||
|
||||
def finish(self):
|
||||
if self.sig_handler:
|
||||
self.sig_handler()
|
||||
|
||||
print('stopping.....')
|
||||
self.stop()
|
||||
izzylog.info('Bye! :3')
|
||||
sys.exit()
|
||||
|
||||
|
||||
class HttpServerRequest(sanic.request.Request):
|
||||
def __init__(self, url_bytes, headers, version, method, transport, app):
|
||||
super().__init__(url_bytes, headers, version, method, transport, app)
|
||||
|
||||
self.Headers = HttpHeaders(headers)
|
||||
self.Data = HttpData(self)
|
||||
self.template = self.app.template
|
||||
self.__setup_defaults()
|
||||
self.__parse_path()
|
||||
|
||||
#if self.paths.media:
|
||||
#return
|
||||
|
||||
self.__parse_signature()
|
||||
self.Run()
|
||||
|
||||
|
||||
def Run(self):
|
||||
pass
|
||||
|
||||
|
||||
def response(self, tpl, *args, **kwargs):
|
||||
return self.template.response(self, tpl, *args, **kwargs)
|
||||
|
||||
|
||||
def alldata(self):
|
||||
return self.__combine_dicts(self.content.json, self.data.query, self.data.form)
|
||||
|
||||
|
||||
def verify(self, actor=None):
|
||||
self.ap.valid = http.VerifyHeaders(self.headers, self.method, self.path, actor, self.body)
|
||||
return self.ap.valid
|
||||
|
||||
|
||||
def __combine_dicts(self, *dicts):
|
||||
data = DotDict()
|
||||
|
||||
for item in dicts:
|
||||
data.update(item)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def __setup_defaults(self):
|
||||
self.paths = DotDict({'media': False, 'json': False, 'ap': False, 'cookie': False})
|
||||
self.ap = DotDict({'valid': False, 'signature': {}, 'actor': None, 'inbox': None, 'domain': None})
|
||||
|
||||
|
||||
def __parse_path(self):
|
||||
self.paths.media = any(map(self.path.startswith, log_path_ignore)) or any(map(self.path.startswith, log_ext_ignore))
|
||||
self.paths.json = self.__json_check()
|
||||
|
||||
|
||||
def __parse_signature(self):
|
||||
sig = self.headers.getone('signature', None)
|
||||
|
||||
if sig:
|
||||
self.ap.signature = http.ParseSig(sig)
|
||||
|
||||
if self.ap.signature:
|
||||
self.ap.actor = self.ap.signature.get('keyid', '').split('#', 1)[0]
|
||||
self.ap.domain = urlparse(self.ap.actor).netloc
|
||||
|
||||
|
||||
def __json_check(self):
|
||||
if self.path.endswith('.json'):
|
||||
return True
|
||||
|
||||
accept = self.headers.getone('Accept', None)
|
||||
|
||||
if accept:
|
||||
mimes = [v.strip() for v in accept.split(',')]
|
||||
|
||||
if any(mime in ['application/json', 'application/activity+json'] for mime in mimes):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class HttpServerResponse:
|
||||
Text = sanic.response.text
|
||||
Html = sanic.response.html
|
||||
Json = sanic.response.json
|
||||
Redir = sanic.response.redirect
|
||||
|
||||
|
||||
def Css(*args, headers={}, **kwargs):
|
||||
ReplaceHeader(headers, 'content-type', 'text/css')
|
||||
return sanic.response.text(*args, headers=headers, **kwargs)
|
||||
|
||||
|
||||
def Js(*args, headers={}, **kwargs):
|
||||
ReplaceHeader(headers, 'content-type', 'application/javascript')
|
||||
return sanic.response.text(*args, headers=headers, **kwargs)
|
||||
|
||||
|
||||
def Ap(*args, headers={}, **kwargs):
|
||||
ReplaceHeader(headers, 'content-type', 'application/activity+json')
|
||||
return sanic.response.json(*args, headers=headers, **kwargs)
|
||||
|
||||
|
||||
def Jrd(*args, headers={}, **kwargs):
|
||||
ReplaceHeader(headers, 'content-type', 'application/jrd+json')
|
||||
return sanic.response.json(*args, headers=headers, **kwargs)
|
||||
|
||||
|
||||
class HttpHeaders(LowerDotDict):
|
||||
def __init__(self, headers):
|
||||
super().__init__()
|
||||
|
||||
for k,v in headers.items():
|
||||
if not self.get(k):
|
||||
self[k] = []
|
||||
|
||||
self[k].append(v)
|
||||
|
||||
|
||||
def getone(self, key, default=None):
|
||||
value = self.get(key)
|
||||
|
||||
if not value:
|
||||
return default
|
||||
|
||||
return value[0]
|
||||
|
||||
|
||||
def getall(self, key, default=[]):
|
||||
return self.get(key.lower(), default)
|
||||
|
||||
|
||||
class HttpData(object):
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
|
||||
|
||||
@property
|
||||
def combined(self):
|
||||
return DotDict(**self.form.asDict(), **self.query.asDict(), **self.json.asDict())
|
||||
|
||||
|
||||
@property
|
||||
def query(self):
|
||||
data = {k: v for k,v in parse_qsl(self.request.query_string)}
|
||||
return DotDict(data)
|
||||
|
||||
|
||||
@property
|
||||
def form(self):
|
||||
data = {k: v[0] for k,v in self.request.form.items()}
|
||||
return DotDict(data)
|
||||
|
||||
|
||||
@property
|
||||
def files(self):
|
||||
return DotDict({k:v[0] for k,v in self.request.files.items()})
|
||||
|
||||
|
||||
### body functions
|
||||
@property
|
||||
def raw(self):
|
||||
try:
|
||||
return self.request.body
|
||||
except Exception as e:
|
||||
izzylog.verbose('IzzyLib.http_server.Data.raw: failed to get body')
|
||||
izzylog.debug(f'{e.__class__.__name__}: {e}')
|
||||
return b''
|
||||
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
try:
|
||||
return self.raw.decode()
|
||||
except Exception as e:
|
||||
izzylog.verbose('IzzyLib.http_server.Data.text: failed to get body')
|
||||
izzylog.debug(f'{e.__class__.__name__}: {e}')
|
||||
return ''
|
||||
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
try:
|
||||
return DotDict(self.text)
|
||||
except Exception as e:
|
||||
izzylog.verbose('IzzyLib.http_server.Data.json: failed to get body')
|
||||
izzylog.debug(f'{e.__class__.__name__}: {e}')
|
||||
data = '{}'
|
||||
return {}
|
||||
|
||||
|
||||
async def MiddlewareAccessLog(request, response):
|
||||
if request.paths.media:
|
||||
return
|
||||
|
||||
uagent = request.headers.get('user-agent')
|
||||
address = request.headers.get('x-real-ip', request.forwarded.get('for', request.remote_addr))
|
||||
|
||||
izzylog.info(f'({multiprocessing.current_process().name}) {address} {request.method} {request.path} {response.status} "{uagent}"')
|
||||
|
||||
|
||||
def GenericError(request, exception):
|
||||
try:
|
||||
status = exception.status_code
|
||||
except:
|
||||
status = 500
|
||||
|
||||
if status not in range(200, 499):
|
||||
traceback.print_exc()
|
||||
|
||||
msg = f'{exception.__class__.__name__}: {str(exception)}'
|
||||
|
||||
if request.paths.json:
|
||||
return sanic.response.json({'error': {'status': status, 'message': msg}})
|
||||
|
||||
try:
|
||||
return request.response('server_error.haml', status=status, context={'status': str(status), 'error': msg})
|
||||
|
||||
except TemplateNotFound:
|
||||
return sanic.response.text(f'Error {status}: {msg}')
|
||||
|
||||
|
||||
def NoTemplateError(request, exception):
|
||||
izzylog.error('TEMPLATE_ERROR:', f'{exception.__class__.__name__}: {str(exception)}')
|
||||
return sanic.response.html('I\'m a dumbass and forgot to create a template for this page', 500)
|
||||
|
||||
|
||||
def ReplaceHeader(headers, key, value):
|
||||
for k,v in headers.items():
|
||||
if k.lower() == header.lower():
|
||||
del headers[k]
|
|
@ -7,8 +7,9 @@ from .client import (
|
|||
verify_headers,
|
||||
parse_signature,
|
||||
fetch_actor,
|
||||
fetch_webfinger_account,
|
||||
fetch_instance,
|
||||
fetch_nodeinfo,
|
||||
fetch_webfinger_account,
|
||||
set_requests_client,
|
||||
generate_rsa_key
|
||||
)
|
||||
|
@ -26,8 +27,9 @@ __all__ = [
|
|||
'HttpRequestsResponse',
|
||||
'SigningError',
|
||||
'fetch_actor',
|
||||
'fetch_webfinger_account',
|
||||
'fetch_instance',
|
||||
'fetch_nodeinfo',
|
||||
'fetch_webfinger_account',
|
||||
'generate_rsa_key',
|
||||
'parse_signature',
|
||||
'set_requests_client',
|
||||
|
|
|
@ -46,10 +46,6 @@ class HttpRequestsClient(object):
|
|||
|
||||
|
||||
def __sign_request(self, request, privkey, keyid):
|
||||
if not crypto_enabled:
|
||||
izzylog.error('Crypto functions disabled')
|
||||
return
|
||||
|
||||
request.add_header('(request-target)', f'{request.method.lower()} {request.path}')
|
||||
request.add_header('host', request.host)
|
||||
request.add_header('date', datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT'))
|
||||
|
@ -63,7 +59,7 @@ class HttpRequestsClient(object):
|
|||
'keyId': keyid,
|
||||
'algorithm': 'rsa-sha256',
|
||||
'headers': ' '.join([k.lower() for k in request.headers.keys()]),
|
||||
'signature': b64encode(PkcsHeaders(privkey, request.headers)).decode('UTF-8')
|
||||
'signature': b64encode(sign_pkcs_headers(privkey, request.headers)).decode('UTF-8')
|
||||
}
|
||||
|
||||
sig_items = [f'{k}="{v}"' for k,v in sig.items()]
|
||||
|
@ -83,6 +79,12 @@ class HttpRequestsClient(object):
|
|||
return HttpRequestsResponse(request.send())
|
||||
|
||||
|
||||
def signed_request(self, privkey, keyid, *args, **kwargs):
|
||||
request = HttpRequestsRequest(self, *args, **kwargs)
|
||||
self.__sign_request(request, privkey, keyid)
|
||||
return HttpRequestsResponse(request.send())
|
||||
|
||||
|
||||
def download(self, url, filepath, *args, filename=None, **kwargs):
|
||||
resp = self.request(url, *args, **kwargs)
|
||||
|
||||
|
@ -130,18 +132,15 @@ class HttpRequestsClient(object):
|
|||
return self.request(*args, headers=headers, **kwargs)
|
||||
|
||||
|
||||
def signed_request(self, privkey, keyid, *args, **kwargs):
|
||||
request = HttpRequestsRequest(self, *args, **kwargs)
|
||||
self.__sign_request(request, privkey, keyid)
|
||||
return HttpRequestsResponse(request.send())
|
||||
|
||||
|
||||
class HttpRequestsRequest(object):
|
||||
def __init__(self, client, url, data=None, headers={}, query={}, method='get'):
|
||||
parsed = urlparse(url)
|
||||
self.args = [url]
|
||||
self.kwargs = {'params': query}
|
||||
self.kwargs = DotDict({'params': query})
|
||||
self.method = method.lower()
|
||||
self.client = client
|
||||
self.path = parsed.path
|
||||
self.host = parsed.netloc
|
||||
|
||||
new_headers = client.headers.copy()
|
||||
new_headers.update(headers)
|
||||
|
@ -151,11 +150,34 @@ class HttpRequestsRequest(object):
|
|||
if not parsed_headers.get('user-agent'):
|
||||
parsed_headers['user-agent'] = client.agent
|
||||
|
||||
self.kwargs['headers'] = new_headers
|
||||
self.kwargs['headers'] = DotDict(new_headers)
|
||||
self.kwargs['data'] = data
|
||||
|
||||
if client.proxy.enabled:
|
||||
self.kwargs['proxies'] = {self.proxy.ptype: f'{self.proxy.ptype}://{self.proxy.host}:{self.proxy.port}'}
|
||||
self.kwargs['proxies'] = DotDict({self.proxy.ptype: f'{self.proxy.ptype}://{self.proxy.host}:{self.proxy.port}'})
|
||||
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
return self.kwargs.data
|
||||
|
||||
|
||||
@body.setter
|
||||
def body(self, data):
|
||||
self.kwargs.data = data
|
||||
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
return self.kwargs.headers
|
||||
|
||||
|
||||
def add_header(self, key, value):
|
||||
self.kwargs.headers[key] = value
|
||||
|
||||
|
||||
def remove_header(self, key):
|
||||
self.kwargs.headers.pop(key, None)
|
||||
|
||||
|
||||
def send(self):
|
||||
|
@ -191,6 +213,10 @@ class HttpRequestsResponse(object):
|
|||
|
||||
@cached_property
|
||||
def json(self):
|
||||
try:
|
||||
return DotDict(self.text)
|
||||
|
||||
except:
|
||||
return DotDict(self.body)
|
||||
|
||||
|
||||
|
@ -371,6 +397,25 @@ def fetch_actor(url):
|
|||
return actor
|
||||
|
||||
|
||||
@lru_cache(maxsize=512)
|
||||
def fetch_instance(domain):
|
||||
if not Client:
|
||||
raise ValueError('Please set global client with "SetRequestsClient(client)"')
|
||||
|
||||
headers = {'Accept': 'application/json'}
|
||||
resp = Client.request(f'https://{domain}/api/v1/instance', headers=headers)
|
||||
|
||||
try:
|
||||
return resp.json
|
||||
|
||||
except json.decoder.JSONDecodeError:
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
izzylog.debug(f'HTTP {resp.status}: {resp.body}')
|
||||
raise e from None
|
||||
|
||||
|
||||
@lru_cache(maxsize=512)
|
||||
def fetch_webfinger_account(handle, domain):
|
||||
if not Client:
|
||||
|
|
|
@ -220,7 +220,7 @@ class SqlSession(object):
|
|||
return self.fetch(*args, single=False, **kwargs)
|
||||
|
||||
|
||||
def insert(self, table_name, **kwargs):
|
||||
def insert(self, table_name, return_row=False, **kwargs):
|
||||
row = self.fetch(table_name, **kwargs)
|
||||
|
||||
if row:
|
||||
|
@ -232,11 +232,13 @@ class SqlSession(object):
|
|||
if getattr(table, 'timestamp', None) and not kwargs.get('timestamp'):
|
||||
kwargs['timestamp'] = datetime.now()
|
||||
|
||||
res = self.execute(table.insert().values(**kwargs))
|
||||
#return self.fetch(table_name, **kwargs)
|
||||
self.execute(table.insert().values(**kwargs))
|
||||
|
||||
if return_row:
|
||||
return self.fetch(table_name, **kwargs)
|
||||
|
||||
|
||||
def update(self, table=None, rowid=None, row=None, **data):
|
||||
def update(self, table=None, rowid=None, row=None, return_row=False, **data):
|
||||
if row:
|
||||
rowid = row.id
|
||||
table = row._table_name
|
||||
|
@ -247,6 +249,9 @@ class SqlSession(object):
|
|||
tclass = self.table[table]
|
||||
self.execute(tclass.update().where(tclass.c.id == rowid).values(**data))
|
||||
|
||||
if return_row:
|
||||
return self.fetch(table, id=rowid)
|
||||
|
||||
|
||||
def remove(self, table=None, rowid=None, row=None):
|
||||
if row:
|
||||
|
|
Loading…
Reference in a new issue