KaikOut/kaikout/database.py

514 lines
16 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from asyncio import Lock
from kaikout.log import Logger
from sqlite3 import connect, Error, IntegrityError
import os
import sys
import time
import tomli_w
import tomllib
# from eliot import start_action, to_file
# # with start_action(action_type="list_feeds()", db=db_file):
# # with start_action(action_type="last_entries()", num=num):
# # with start_action(action_type="get_feeds()"):
# # with start_action(action_type="remove_entry()", source=source):
# # with start_action(action_type="search_entries()", query=query):
# # with start_action(action_type="check_entry()", link=link):
CURSORS = {}
# aiosqlite
DBLOCK = Lock()
logger = Logger(__name__)
class SQLite:
def create_connection(db_file):
"""
Create a database connection to the SQLite database
specified by db_file.
Parameters
----------
db_file : str
Path to database file.
Returns
-------
conn : object
Connection object or None.
"""
time_begin = time.time()
function_name = sys._getframe().f_code.co_name
message_log = '{}'
logger.debug(message_log.format(function_name))
conn = None
try:
conn = connect(db_file)
conn.execute("PRAGMA foreign_keys = ON")
# return conn
except Error as e:
logger.warning('Error creating a connection to database {}.'.format(db_file))
logger.error(e)
time_end = time.time()
difference = time_end - time_begin
if difference > 1: logger.warning('{} (time: {})'.format(function_name,
difference))
return conn
def create_tables(db_file):
"""
Create SQLite tables.
Parameters
----------
db_file : str
Path to database file.
"""
function_name = sys._getframe().f_code.co_name
logger.debug('{}: db_file: {}'
.format(function_name, db_file))
with SQLite.create_connection(db_file) as conn:
activity_table_sql = (
"""
CREATE TABLE IF NOT EXISTS activity (
id INTEGER NOT NULL,
stanza_id TEXT,
alias TEXT,
jid TEXT,
body TEXT,
thread TEXT,
PRIMARY KEY ("id")
);
"""
)
filters_table_sql = (
"""
CREATE TABLE IF NOT EXISTS filters (
id INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY ("id")
);
"""
)
outcast_table_sql = (
"""
CREATE TABLE IF NOT EXISTS outcast (
id INTEGER NOT NULL,
alias TEXT,
jid TEXT,
reason TEXT,
PRIMARY KEY ("id")
);
"""
)
settings_table_sql = (
"""
CREATE TABLE IF NOT EXISTS settings (
id INTEGER NOT NULL,
key TEXT NOT NULL,
value INTEGER,
PRIMARY KEY ("id")
);
"""
)
cur = conn.cursor()
# cur = get_cursor(db_file)
cur.execute(activity_table_sql)
cur.execute(filters_table_sql)
cur.execute(outcast_table_sql)
cur.execute(settings_table_sql)
def get_cursor(db_file):
"""
Allocate a cursor to connection per database.
Parameters
----------
db_file : str
Path to database file.
Returns
-------
CURSORS[db_file] : object
Cursor.
"""
function_name = sys._getframe().f_code.co_name
logger.debug('{}: db_file: {}'
.format(function_name, db_file))
if db_file in CURSORS:
return CURSORS[db_file]
else:
with SQLite.create_connection(db_file) as conn:
cur = conn.cursor()
CURSORS[db_file] = cur
return CURSORS[db_file]
async def import_feeds(db_file, feeds):
"""
Insert a new feed into the feeds table.
Parameters
----------
db_file : str
Path to database file.
feeds : list
Set of feeds (Title and URL).
"""
function_name = sys._getframe().f_code.co_name
logger.debug('{}: db_file: {}'
.format(function_name, db_file))
async with DBLOCK:
with SQLite.create_connection(db_file) as conn:
cur = conn.cursor()
for feed in feeds:
logger.debug('{}: feed: {}'
.format(function_name, feed))
url = feed['url']
title = feed['title']
sql = (
"""
INSERT
INTO feeds_properties(
title, url)
VALUES(
?, ?)
"""
)
par = (title, url)
try:
cur.execute(sql, par)
except IntegrityError as e:
logger.warning("Skipping: " + str(url))
logger.error(e)
async def add_metadata(db_file):
"""
Insert a new feed into the feeds table.
Parameters
----------
db_file : str
Path to database file.
"""
function_name = sys._getframe().f_code.co_name
logger.debug('{}: db_file: {}'
.format(function_name, db_file))
async with DBLOCK:
with SQLite.create_connection(db_file) as conn:
cur = conn.cursor()
sql = (
"""
SELECT id
FROM feeds_properties
ORDER BY id ASC
"""
)
ixs = cur.execute(sql).fetchall()
for ix in ixs:
feed_id = ix[0]
# Set feed status
sql = (
"""
INSERT
INTO feeds_state(
feed_id)
VALUES(
?)
"""
)
par = (feed_id,)
try:
cur.execute(sql, par)
except IntegrityError as e:
logger.warning(
"Skipping feed_id {} for table feeds_state".format(feed_id))
logger.error(e)
# Set feed preferences.
sql = (
"""
INSERT
INTO feeds_preferences(
feed_id)
VALUES(
?)
"""
)
par = (feed_id,)
try:
cur.execute(sql, par)
except IntegrityError as e:
logger.warning(
"Skipping feed_id {} for table feeds_preferences".format(feed_id))
logger.error(e)
async def insert_feed(db_file, url, title, identifier, entries=None, version=None,
encoding=None, language=None, status_code=None,
updated=None):
"""
Insert a new feed into the feeds table.
Parameters
----------
db_file : str
Path to database file.
url : str
URL.
title : str
Feed title.
identifier : str
Feed identifier.
entries : int, optional
Number of entries. The default is None.
version : str, optional
Type of feed. The default is None.
encoding : str, optional
Encoding of feed. The default is None.
language : str, optional
Language code of feed. The default is None.
status : str, optional
HTTP status code. The default is None.
updated : ???, optional
Date feed was last updated. The default is None.
"""
function_name = sys._getframe().f_code.co_name
logger.debug('{}: db_file: {} url: {}'
.format(function_name, db_file, url))
async with DBLOCK:
with SQLite.create_connection(db_file) as conn:
cur = conn.cursor()
sql = (
"""
INSERT
INTO feeds_properties(
url, title, identifier, entries, version, encoding, language, updated)
VALUES(
?, ?, ?, ?, ?, ?, ?, ?)
"""
)
par = (url, title, identifier, entries, version, encoding, language, updated)
cur.execute(sql, par)
sql = (
"""
SELECT id
FROM feeds_properties
WHERE url = :url
"""
)
par = (url,)
feed_id = cur.execute(sql, par).fetchone()[0]
sql = (
"""
INSERT
INTO feeds_state(
feed_id, status_code, valid)
VALUES(
?, ?, ?)
"""
)
par = (feed_id, status_code, 1)
cur.execute(sql, par)
sql = (
"""
INSERT
INTO feeds_preferences(
feed_id)
VALUES(
?)
"""
)
par = (feed_id,)
cur.execute(sql, par)
async def remove_feed_by_url(db_file, url):
"""
Delete a feed by feed URL.
Parameters
----------
db_file : str
Path to database file.
url : str
URL of feed.
"""
function_name = sys._getframe().f_code.co_name
logger.debug('{}: db_file: {} url: {}'
.format(function_name, db_file, url))
with SQLite.create_connection(db_file) as conn:
async with DBLOCK:
cur = conn.cursor()
sql = (
"""
DELETE
FROM feeds_properties
WHERE url = ?
"""
)
par = (url,)
cur.execute(sql, par)
async def remove_feed_by_index(db_file, ix):
"""
Delete a feed by feed ID.
Parameters
----------
db_file : str
Path to database file.
ix : str
Index of feed.
"""
function_name = sys._getframe().f_code.co_name
logger.debug('{}: db_file: {} ix: {}'
.format(function_name, db_file, ix))
with SQLite.create_connection(db_file) as conn:
async with DBLOCK:
cur = conn.cursor()
# # NOTE Should we move DBLOCK to this line? 2022-12-23
# sql = (
# "DELETE "
# "FROM entries "
# "WHERE feed_id = ?"
# )
# par = (url,)
# cur.execute(sql, par) # Error? 2024-01-05
# sql = (
# "DELETE "
# "FROM archive "
# "WHERE feed_id = ?"
# )
# par = (url,)
# cur.execute(sql, par)
sql = (
"""
DELETE
FROM feeds_properties
WHERE id = ?
"""
)
par = (ix,)
cur.execute(sql, par)
def get_feeds_by_tag_id(db_file, tag_id):
"""
Get feeds of given tag.
Parameters
----------
db_file : str
Path to database file.
tag_id : str
Tag ID.
Returns
-------
result : tuple
List of tags.
"""
function_name = sys._getframe().f_code.co_name
logger.debug('{}: db_file: {} tag_id: {}'
.format(function_name, db_file, tag_id))
with SQLite.create_connection(db_file) as conn:
cur = conn.cursor()
sql = (
"""
SELECT feeds_properties.*
FROM feeds_properties
INNER JOIN tagged_feeds ON feeds_properties.id = tagged_feeds.feed_id
INNER JOIN tags ON tags.id = tagged_feeds.tag_id
WHERE tags.id = ?
ORDER BY feeds_properties.title;
"""
)
par = (tag_id,)
result = cur.execute(sql, par).fetchall()
return result
class Toml:
def instantiate(self, room):
"""
Callback function to instantiate action on database.
Parameters
----------
jid_file : str
Filename.
callback : ?
Function name.
message : str, optional
Optional kwarg when a message is a part or
required argument. The default is None.
Returns
-------
object
Coroutine object.
"""
data_dir = Toml.get_default_data_directory()
if not os.path.isdir(data_dir):
os.mkdir(data_dir)
data_dir_toml = os.path.join(data_dir, 'toml')
if not os.path.isdir(data_dir_toml):
os.mkdir(data_dir_toml)
filename = os.path.join(data_dir_toml, f'{room}.toml')
if not os.path.exists(filename):
Toml.create_settings_file(self, filename)
Toml.load_jid_settings(self, room, filename)
return filename
def get_default_data_directory():
if os.environ.get('HOME'):
data_home = os.path.join(os.environ.get('HOME'), '.local', 'share')
return os.path.join(data_home, 'kaikout')
elif sys.platform == 'win32':
data_home = os.environ.get('APPDATA')
if data_home is None:
return 'kaikout_data'
else:
return 'kaikout_data'
def get_data_file(data_dir, room):
toml_file = os.path.join(data_dir, 'toml', f'{room}.toml')
return toml_file
def create_settings_file(self, filename):
data = self.defaults
content = tomli_w.dumps(data)
with open(filename, 'w') as f: f.write(content)
def load_jid_settings(self, room, filename):
# data_dir = Toml.get_default_data_directory()
# filename = Toml.get_data_file(data_dir, room)
with open(filename, 'rb') as f: self.settings[room] = tomllib.load(f)
def update_jid_settings(self, room, filename, key, value):
with open(filename, 'rb') as f: data = tomllib.load(f)
self.settings[room][key] = value
data = self.settings[room]
content = tomli_w.dumps(data)
with open(filename, 'w') as f: f.write(content)