Project

General

Profile

Download (9.8 KB) Statistics
| Branch: | Tag: | Revision:
137b22ce dsorber
import datetime
655be0cd dsorber
import re
from urllib.request import urlopen

137b22ce dsorber
import dateutil.parser
aa15db9e dsorber
import sqlite3

7218b9c1 dsorber
from tag import Tag

aa15db9e dsorber
DB_PATH = 'bookmarks.db'

655be0cd dsorber
SQL_SELECT_BOOKMARK = "SELECT * FROM bookmarks WHERE id=?"
137b22ce dsorber
SQL_INSERT_BOOKMARK = "INSERT INTO bookmarks VALUES (?,?,?,?,?,?,?,?,?)"
aa15db9e dsorber
SQL_UPDATE_BOOKMARK = "UPDATE bookmarks SET {:s} WHERE id=?"
fd2c83de dsorber
SQL_SELECT_CAT_TAGS = "SELECT tag_id FROM category_tags WHERE bookmark_id=?"
SQL_SELECT_DESC_TAGS = "SELECT tag_id FROM description_tags WHERE bookmark_id=?"

SQL_DELETE_CAT_TAGS = "DELETE FROM category_tags WHERE bookmark_id=?"
SQL_DELETE_DESC_TAGS = "DELETE FROM description_tags WHERE bookmark_id=?"

SQL_INSERT_CAT_TAGS = "INSERT INTO category_tags VALUES (?, ?)"
SQL_INSERT_DESC_TAGS = "INSERT INTO description_tags VALUES (?, ?)"
aa15db9e dsorber
655be0cd dsorber
HTTP_OKAY = 200

class BookmarkNotFoundError(Exception):
def __init__(self, value):
self.value = value
def __str__(self, value):
return repr(self.value)

aa15db9e dsorber
class DBConnectionError(Exception):
def __init__(self, value):
self.value = value
def __str__(self, value):
return repr(self.value)

137b22ce dsorber
def _format_date(date):
if date is not None:
return date.isoformat()
else:
return date


aa15db9e dsorber
class Bookmark(object):
def __init__(self, db_conn, id=None, url=None):
7056140d dsorber
# Make sure the DB connection is valid before doing anything else
aa15db9e dsorber
if not db_conn:
raise DBConnectionError('Invalid database connection object!')
# Setup internal parameters
self._db_conn = db_conn
655be0cd dsorber
self._content = None
aa15db9e dsorber
# Setup defaut object data attributes
self.id = id
self.url = url
self.title = ''
self.description = ''
self.times_visited = 0
137b22ce dsorber
self.last_visited = None
self.last_reachable = None
self.date_added = None
aa15db9e dsorber
self.deleted = False
7056140d dsorber
self.category_tags = set()
self.description_tags = set()
aa15db9e dsorber
7056140d dsorber
# Retrieve an existing bookmark if an ID is passed in
fd2c83de dsorber
if self.id is not None:
655be0cd dsorber
db = self._db_conn.cursor()
db.execute(SQL_SELECT_BOOKMARK, (self.id,))
row = db.fetchone()
# Check to see if a record was found
if not row:
raise BookmarkNotFoundError(self.id)
# Assign data attributes
self.url = row[1]
self.title = row[2]
self.description = row[3]
self.times_visited = int(row[4])
137b22ce dsorber
if row[5]:
self.last_visited = dateutil.parser.parse(row[5])
if row[6]:
self.last_reachable = dateutil.parser.parse(row[6])
self.date_added = dateutil.parser.parse(row[7])
if int(row[8]) == 0:
655be0cd dsorber
self.deleted = False
else:
self.deleted = True
7218b9c1 dsorber
# Grab category tags
for row in db.execute(SQL_SELECT_CAT_TAGS, (self.id,)):
fd2c83de dsorber
self.category_tags.add(Tag(int(row[0])))
# Grab description tags
7218b9c1 dsorber
for row in db.execute(SQL_SELECT_DESC_TAGS, (self.id,)):
fd2c83de dsorber
self.description_tags.add(Tag(int(row[0])))
137b22ce dsorber
# Store hash of tag sets for dirty comparison
fd2c83de dsorber
self._cat_tags_hash = hash(frozenset(self.category_tags))
self._desc_tags_hash = hash(frozenset(self.description_tags))
655be0cd dsorber
# This dict MUST appear *after* the data attributes, it is used to
aa15db9e dsorber
# record which data attributes are dirty
self._dirty = {'url': False,
'title': False,
'description': False,
'times_visited': False,
'last_visited': False,
'last_reachable': False,
7218b9c1 dsorber
'deleted': False}
aa15db9e dsorber
def __del__(self):
655be0cd dsorber
""" Automatically save any changes to a record before the object is
destroyed.
"""
self.save()
def save(self):
""" Save any changes to a bookmark record if it is dirty. """
aa15db9e dsorber
# Check if the record is dirty
if self.record_dirty:
if self.id:
655be0cd dsorber
# Update existing bookmark record
self._db_update()
aa15db9e dsorber
else:
655be0cd dsorber
# Insert new bookmark record
aa15db9e dsorber
self._db_insert()
def __setattr__(self, name, value):
""" Customize setting data attributes so we can tell which ones
are dirty.
"""
137b22ce dsorber
# The date_added attribute is immutable once it has been set
if hasattr(self, 'date_added') and name == 'date_added':
if getattr(self, name) is not None:
return
aa15db9e dsorber
# Check if the attr is a data attribute then check if the value has
# changed. If so, mark the attribute as dirty
if hasattr(self, '_dirty'):
if name in self._dirty and getattr(self, name) != value:
self._dirty[name] = True
# Set the attribute value using the super class's __setattr__ to avoid
# infinite recursion (recursion joke here)
super(Bookmark, self).__setattr__(name, value)
def _db_insert(self):
655be0cd dsorber
""" Insert a new bookmark record into the database. """
137b22ce dsorber
self.date_added = datetime.datetime.now()
aa15db9e dsorber
db = self._db_conn.cursor()
fd2c83de dsorber
values = (None, self.url, self.title, self.description,
137b22ce dsorber
self.times_visited, _format_date(self.last_visited),
_format_date(self.last_reachable),
_format_date(self.date_added),
aa15db9e dsorber
self.deleted and '1' or '0')
db.execute(SQL_INSERT_BOOKMARK, values)
fd2c83de dsorber
# Set bookmark ID after insert
self.id = db.lastrowid
# Insert any category tags
for tag in self.category_tags:
tag.save()
db.execute(SQL_INSERT_CAT_TAGS, (tag.id, self.id))
# Insert any description tags
for tag in self.description_tags:
tag.save()
db.execute(SQL_INSERT_DESC_TAGS, (tag.id, self.id))
aa15db9e dsorber
self._db_conn.commit()
# Reset dirty flags
for key in self._dirty.keys():
self._dirty[key] = False
def _db_update(self):
655be0cd dsorber
""" Update an exisitng bookmark record in the database. """
aa15db9e dsorber
values = []
sets = []
# Find dirty data attributes for updating
for key in self._dirty.keys():
655be0cd dsorber
if self._dirty[key] == True:
aa15db9e dsorber
sets.append('{:s}=?'.format(key))
137b22ce dsorber
if key in ['last_reachable', 'last_visited']:
# Datetime objects need to be formated before update
values.append(_format_date(getattr(self, key)))
else:
values.append(getattr(self, key))
655be0cd dsorber
fd2c83de dsorber
if values and sets:
# Add the record id as the last value
values.append(self.id)
# Build full SQL statement then execute it
sql = SQL_UPDATE_BOOKMARK.format(','.join(sets))
db = self._db_conn.cursor()
db.execute(sql, values)
# Reset dirty flags
for key in self._dirty.keys():
self._dirty[key] = False
aa15db9e dsorber
fd2c83de dsorber
# Inner function FTW
def update_tags_lists(delete_sql, insert_sql, tags):
db.execute(delete_sql, (self.id,))
tag_updates = []
for tag in tags:
tag.save()
tag_updates.append((tag.id, self.id))
db.executemany(insert_sql, tag_updates)
# Update tag lists if they are dirty
if self.category_tags_dirty:
update_tag_lists(SQL_DELETE_CAT_TAGS, SQL_INSERT_CAT_TAGS,
self.category_tags)
if self.description_tags_dirty:
update_tag_lists(SQL_DELETE_DESC_TAGS, SQL_INSERT_DESC_TAGS,
self.description_tags)
137b22ce dsorber
self._db_conn.commit()
fd2c83de dsorber
@property
def category_tags_dirty(self):
return self._cat_tags_hash != hash(frozenset(self.category_tags))
aa15db9e dsorber
fd2c83de dsorber
@property
def description_tags_dirty(self):
return self._desc_tags_hash != hash(frozenset(self.description_tags))
aa15db9e dsorber
@property
def record_dirty(self):
""" Check if any data attributes are dirty, which indicates the record
is dirty.
"""
fd2c83de dsorber
dirty = [val for key, val in self._dirty.items()]
dirty.append(self.category_tags_dirty)
dirty.append(self.description_tags_dirty)
return any(dirty)
655be0cd dsorber
def _get_content(self):
""" Read the page and get its raw page HTML content. """
if self.url:
response = urlopen(self.url)
if response.getcode() == HTTP_OKAY:
self._content = response.read().decode('utf-8')
def suggest_title(self):
""" Return the contents of the page's <title> tag as a title suggestion
"""
if self.url and not self._content:
self._get_content()
try:
title = re.search('<title>(.+)</title>', self._content).group(1)
except IndexError:
title = None
return title
def is_reachable(self):
""" Check if the page is reachable. """
if self.url:
response = urlopen(self.url)
if response.getcode() == HTTP_OKAY:
137b22ce dsorber
self.last_reachable = datetime.datetime.now()
655be0cd dsorber
return True
return False
137b22ce dsorber
def visit(self):
""" Increment the visit count and update the last visited datestamp """
self.times_visited += 1
self.last_visited = datetime.datetime.now()