4 Commits
v1.0.2 ... main

Author SHA1 Message Date
relikd
d342f42290 fix: extension + file permissions 2025-10-15 18:20:11 +02:00
relikd
4f160cefcd feat: allow Log.error to pass exception 2023-03-19 13:51:23 +01:00
relikd
24aa71c8bc feat: Log to file error.log 2022-10-14 00:46:12 +02:00
relikd
9b4440c700 feat: add Curl.post() 2022-09-28 22:35:21 +02:00
7 changed files with 89 additions and 26 deletions

0
botlib/__init__.py Executable file → Normal file
View File

0
botlib/cli.py Executable file → Normal file
View File

0
botlib/cron.py Executable file → Normal file
View File

87
botlib/curl.py Executable file → Normal file
View File

@@ -48,14 +48,40 @@ class Curl:
md5(url.encode()).hexdigest()) md5(url.encode()).hexdigest())
@staticmethod @staticmethod
def open(url: str, *, headers: Optional[Dict[str, str]] = None) \ def _cached_is_recent(fname: str, *, maxAge: int) -> bool:
-> Optional[HTTPResponse]: fname = os.path.join(Curl.CACHE_DIR, fname)
return os.path.isfile(fname) and FileTime.get(fname) < maxAge
@staticmethod
def _cached_read(
conn: Optional[HTTPResponse], fname_data: str, fname_head: str
) -> Optional[TextIO]:
fname_data = os.path.join(Curl.CACHE_DIR, fname_data)
if conn:
os.makedirs(Curl.CACHE_DIR, exist_ok=True)
with open(os.path.join(Curl.CACHE_DIR, fname_head), 'w') as fp:
fp.write(str(conn.info()).strip())
with open(fname_data, 'wb') as fpb:
while True:
data = conn.read(8192) # 1024 Bytes
if not data:
break
fpb.write(data)
return open(fname_data) if os.path.isfile(fname_data) else None
@staticmethod
def open(
url: str,
*,
post: Optional[bytes] = None,
headers: Optional[Dict[str, str]] = None,
) -> Optional[HTTPResponse]:
''' Open a network connection, returl urlopen() result or None. ''' ''' Open a network connection, returl urlopen() result or None. '''
try: try:
head = {'User-Agent': 'Mozilla/5.0'} head = {'User-Agent': 'Mozilla/5.0'}
if headers: if headers:
head.update(headers) head.update(headers)
return urlopen(Request(url, headers=head)) return urlopen(Request(url, data=post, headers=head))
except Exception as e: except Exception as e:
if isinstance(e, HTTPError) and e.getcode() == 304: if isinstance(e, HTTPError) and e.getcode() == 304:
# print('Not-Modified: {}'.format(url), file=stderr) # print('Not-Modified: {}'.format(url), file=stderr)
@@ -64,31 +90,48 @@ class Curl:
return None return None
@staticmethod @staticmethod
def get(url: str, *, cache_only: bool = False) -> Optional[TextIO]: def get(
url: str,
*,
cache_only: bool = False,
headers: Optional[Dict[str, str]] = None,
) -> Optional[TextIO]:
''' '''
Returns an already open file pointer. Returns an already open file pointer.
You are responsible for closing the file. You are responsible for closing the file.
NOTE: `HTML2List.parse` and `Feed2List.parse` will close it for you. NOTE: `HTML2List.parse` and `Feed2List.parse` will close it for you.
''' '''
fname = '{}/curl-{}.data'.format(Curl.CACHE_DIR, Curl.url_hash(url)) fname = 'curl-{}.data'.format(Curl.url_hash(url))
fname_head = fname[:-5] + '.head'
# If file was created less than 45 sec ago, reuse cached value # If file was created less than 45 sec ago, reuse cached value
if cache_only or (os.path.isfile(fname) and FileTime.get(fname) < 45): if cache_only or Curl._cached_is_recent(fname, maxAge=45):
return open(fname) return Curl._cached_read(None, fname, '')
os.makedirs(Curl.CACHE_DIR, exist_ok=True) fname_head = fname[:-5] + '.head'
conn = Curl.open(url, headers=_read_modified_header(fname_head)) head = _read_modified_header(fname_head)
if conn: if headers:
with open(fname_head, 'w') as fp: head.update(headers)
fp.write(str(conn.info()).strip()) conn = Curl.open(url, headers=head)
with open(fname, 'wb') as fpb: return Curl._cached_read(conn, fname, fname_head)
while True:
data = conn.read(8192) # 1024 Bytes
if not data:
break
fpb.write(data)
return open(fname) if os.path.isfile(fname) else None @staticmethod
def post(
url: str,
data: bytes,
*,
cache_only: bool = False,
headers: Optional[Dict[str, str]] = None,
) -> Optional[TextIO]:
'''
Perform POST operation.
Returns an already open file pointer.
You are responsible for closing the file.
'''
fname = 'curl-{}.post.data'.format(Curl.url_hash(url))
if cache_only:
return Curl._cached_read(None, fname, '')
conn = Curl.open(url, post=data, headers=headers)
return Curl._cached_read(conn, fname, fname[:-5] + '.head')
@staticmethod @staticmethod
def json(url: str, fallback: Any = None, *, cache_only: bool = False) \ def json(url: str, fallback: Any = None, *, cache_only: bool = False) \
@@ -141,7 +184,9 @@ class Curl:
if not parts: if not parts:
raise URLError('URL not valid: "{}"'.format(url_str)) raise URLError('URL not valid: "{}"'.format(url_str))
ext = parts.path.split('.')[-1] or 'unknown' ext = parts.path.split('.')[-1]
if not ext or len(ext) > 4:
ext = 'unknown'
file_path = os.path.join(dest_dir, fname + '.' + ext) file_path = os.path.join(dest_dir, fname + '.' + ext)
if override or not os.path.isfile(file_path): if override or not os.path.isfile(file_path):
url = parts.geturl() url = parts.geturl()

24
botlib/helper.py Executable file → Normal file
View File

@@ -2,6 +2,7 @@
import re import re
import os # utime, getmtime import os # utime, getmtime
import time # mktime, time import time # mktime, time
import traceback # format_exc
from sys import stderr from sys import stderr
from html import unescape from html import unescape
from datetime import datetime from datetime import datetime
@@ -11,15 +12,32 @@ from typing import Optional, Callable, Union
class Log: class Log:
FILE = 'error.log'
LEVEL = 0 # -1: disabled, 0: error, 1: warn, 2: info, 4: debug
@staticmethod @staticmethod
def error(e: str) -> None: def _log_if(level: int, msg: str) -> None:
''' Log to file if LOG_LEVEL >= level. '''
if Log.LEVEL >= level:
with open(Log.FILE, 'a') as fp:
fp.write(msg + '\n')
@staticmethod
def error(e: Union[str, Exception]) -> None:
''' Log error message (incl. current timestamp) ''' ''' Log error message (incl. current timestamp) '''
print('{} [ERROR] {}'.format(datetime.now(), e), file=stderr) msg = '{} [ERROR] {}'.format(
datetime.now(), e if isinstance(e, str) else repr(e))
print(msg, file=stderr)
Log._log_if(0, msg)
if isinstance(e, Exception):
Log._log_if(0, traceback.format_exc())
@staticmethod @staticmethod
def info(m: str) -> None: def info(m: str) -> None:
''' Log info message (incl. current timestamp) ''' ''' Log info message (incl. current timestamp) '''
print('{} {}'.format(datetime.now(), m)) msg = '{} {}'.format(datetime.now(), m)
print(msg)
Log._log_if(2, msg)
class FileTime: class FileTime:

0
botlib/oncedb.py Executable file → Normal file
View File

4
botlib/tgclient.py Executable file → Normal file
View File

@@ -42,7 +42,7 @@ class TGClient(telebot.TeleBot):
self.onKillCallback() self.onKillCallback()
return return
except Exception as e: except Exception as e:
Log.error(repr(e)) Log.error(e)
Log.info('Auto-restart in 15 sec ...') Log.info('Auto-restart in 15 sec ...')
sleep(15) sleep(15)
_fn() _fn()
@@ -91,7 +91,7 @@ class TGClient(telebot.TeleBot):
try: try:
return self.send_message(chat_id, msg, **kwargs) return self.send_message(chat_id, msg, **kwargs)
except Exception as e: except Exception as e:
Log.error(repr(e)) Log.error(e)
sleep(45) sleep(45)
return None return None