Project

General

Profile

Download (10.3 KB) Statistics
| Branch: | Tag: | Revision:
137b22ce dsorber
import datetime
655be0cd dsorber
import re
4c6410dc dsorber
from urllib.error import HTTPError, URLError
655be0cd dsorber
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))
4c6410dc dsorber
db = self._db_conn.cursor()
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.execute(sql, values)
# Reset dirty flags
for key in self._dirty.keys():
self._dirty[key] = False
aa15db9e dsorber
fd2c83de dsorber
# Inner function FTW
4c6410dc dsorber
def update_tag_lists(delete_sql, insert_sql, tags):
fd2c83de dsorber
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:
4c6410dc dsorber
try:
response = urlopen(self.url)
if response.getcode() == HTTP_OKAY:
content = response.read()
try:
self._content = content.decode('utf-8')
except UnicodeDecodeError:
self._content = content.decode('ISO-8859-1')
except (HTTPError, URLError):
return ''
655be0cd dsorber
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)
4c6410dc dsorber
except (IndexError, AttributeError, TypeError):
655be0cd dsorber
title = None
return title
def is_reachable(self):
""" Check if the page is reachable. """
if self.url:
4c6410dc dsorber
try:
response = urlopen(self.url)
if response.getcode() == HTTP_OKAY:
self.last_reachable = datetime.datetime.now()
return True
except (HTTPError, URLError):
return False
655be0cd dsorber
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()