1
0
mirror of https://github.com/aquatix/digimarks.git synced 2025-12-06 23:05:10 +01:00

50 Commits

Author SHA1 Message Date
d3dff5a4e8 Latest peewee 2025-07-08 15:30:01 +02:00
b6ffc24175 Merge branch 'master' of github.com:aquatix/digimarks 2025-07-08 15:29:04 +02:00
c5ca4d97e3 lxml 6.0.0 and more updates 2025-07-08 15:28:49 +02:00
372c382364 Merge pull request #49 from aquatix/snyk-fix-92dec271d89f9676c64380ff8fb6ad44
[Snyk] Security upgrade urllib3 from 2.4.0 to 2.5.0
2025-07-08 14:09:32 +02:00
snyk-bot
6ff63f47fe fix: requirements.txt to reduce vulnerabilities
The following vulnerabilities are fixed by pinning transitive dependencies:
- https://snyk.io/vuln/SNYK-PYTHON-URLLIB3-10390193
- https://snyk.io/vuln/SNYK-PYTHON-URLLIB3-10390194
2025-06-21 11:27:36 +00:00
3fbab07944 requests security update and more 2025-06-11 09:20:54 +02:00
db091ae02e Flask security update and more 2025-05-21 12:24:53 +02:00
24935dad9f jinja2 security update and more 2025-01-10 14:59:56 +01:00
6864e7f5a4 werkzeug security update and more 2024-12-10 21:16:03 +01:00
ed8e02f0d5 New certifi, latest dependencies 2024-09-06 22:02:08 +02:00
76e4924e2c certifi security update and more 2024-07-07 11:27:39 +02:00
a35fbffaec requests security update and more 2024-05-26 14:08:19 +02:00
fad7dc59bc jinja2 security update and more 2024-05-08 23:00:56 +02:00
1a4ca1a4c7 Merge pull request #39 from aquatix/snyk-fix-98ecd74aa298dc47942423af99957199
[Snyk] Security upgrade idna from 3.6 to 3.7
2024-04-14 21:55:20 +02:00
snyk-bot
ad614b2872 fix: requirements.txt to reduce vulnerabilities
The following vulnerabilities are fixed by pinning transitive dependencies:
- https://snyk.io/vuln/SNYK-PYTHON-IDNA-6597975
2024-04-14 19:32:08 +00:00
b658651c07 Security update for peewee and more 2024-03-02 11:08:11 +01:00
c1d795e704 Merge pull request #37 from aquatix/snyk-fix-67c48228bceafdd6ed7fac5925180c4f
[Snyk] Security upgrade jinja2 from 3.1.2 to 3.1.3
2024-02-11 16:13:10 +01:00
4b36b448cf jinja2 security update and more 2024-01-11 20:53:24 +01:00
snyk-bot
29b1d045cd fix: requirements.txt to reduce vulnerabilities
The following vulnerabilities are fixed by pinning transitive dependencies:
- https://snyk.io/vuln/SNYK-PYTHON-JINJA2-6150717
2024-01-11 14:48:51 +00:00
d8bf52c9d2 urllib3 security update and more 2023-10-21 19:02:42 +02:00
010905086f urllib security update; latest Flask and everything 2023-10-04 11:10:43 +02:00
a4225829e3 certifi security update and more 2023-07-26 22:59:53 +02:00
f3dff354fc requests security update 2023-05-23 22:38:31 +02:00
ca71fa66df flask security update and more 2023-05-09 09:14:34 +02:00
09ab5acf76 Security updates 2023-02-16 10:22:42 +01:00
e90d35238a Security updates 2022-12-09 11:51:50 +01:00
3092f83c8b Serve the search JS with the correct mimetype 2022-07-23 11:10:55 +02:00
a27787e956 Dependency updates 2022-07-23 11:10:50 +02:00
21800911db Security: lxml; more updates 2022-07-13 18:45:34 +02:00
20a3d9f838 Latest everything 2022-04-23 09:09:50 +02:00
0ab3bd2263 Security: lxml; more updates 2021-12-13 21:28:43 +01:00
32b074b859 Security: urllib3; more updates 2021-07-06 15:10:18 +02:00
5789bbe006 Latest everything 2021-03-25 10:16:17 +01:00
2ef7358ac7 Jinja2 security fix; new-style pip-tools requirements 2021-02-02 08:51:18 +01:00
d1e590390c lxml security update and more 2020-12-02 09:19:05 +01:00
7a1bc11004 Replaced deprecated AtomFeed by feedgen; some dep updates 2020-07-28 14:52:44 +02:00
315c664fcc Document the RapidAPI (MashApe) key for favicons 2020-05-06 14:04:33 +02:00
db5944cec4 Werkzeug 1.0 has deprecated the AtomFeed 2020-05-06 14:02:09 +02:00
Michiel Scholten
becb734d17 Merge pull request #19 from mnishig/doc-MASHAPE_API_KEY
Add description for 'MASHAPE_API_KEY'
2020-05-06 13:56:57 +02:00
64ee0856c5 (Security) bumps 2020-05-05 19:59:27 +02:00
Masahide Nishihama
6c2be3070e add description 'MASHAPE_API_KEY' 2020-04-28 10:30:52 +09:00
426c2eda68 RapidAPI all the things 2020-04-16 13:34:10 +02:00
b0e53d4a85 Back to a version that actually works 2020-03-20 20:47:58 +01:00
1f69d9e53f Lots of updates 2020-03-20 20:44:28 +01:00
Michiel Scholten
fc27d9f186 Merge pull request #17 from aquatix/snyk-fix-e8cd4803e12d5fe482405c22b3c5b385
[Snyk] Security upgrade urllib3 from 1.25.3 to 1.25.8
2020-03-07 09:22:34 +01:00
snyk-bot
6341b384bf fix: requirements.txt to reduce vulnerabilities
The following vulnerabilities are fixed by pinning transitive dependencies:
- https://snyk.io/vuln/SNYK-PYTHON-URLLIB3-559452
2020-03-07 07:48:53 +00:00
f698ebfe18 Security updates for Flask&Werkzeug, more version bumps 2019-08-12 19:27:49 +02:00
9f736ffe82 Move to RapidAPI url for favicons API 2019-07-27 10:45:04 +02:00
e1a45a21b5 urllib3 security update, more version bumps 2019-05-04 10:41:49 +02:00
9492d26511 Security update for urllib3, more bumps 2019-04-19 21:08:49 +02:00
26 changed files with 607 additions and 554 deletions

View File

@@ -39,6 +39,8 @@ Usage / example configuration
Copy ``settings.py`` from example_config to the parent directory and Copy ``settings.py`` from example_config to the parent directory and
configure to your needs (*at the least* change the value of `SYSTEMKEY`). configure to your needs (*at the least* change the value of `SYSTEMKEY`).
Do not forget to fill in the `MASHAPE_API_KEY` value, which you [can request on the RapidAPI website](https://rapidapi.com/realfavicongenerator/api/realfavicongenerator).
Run digimarks as a service under nginx or apache and call the appropriate Run digimarks as a service under nginx or apache and call the appropriate
url's when wanted. url's when wanted.

View File

@@ -1,17 +1,154 @@
from __future__ import print_function from __future__ import print_function
import binascii
import datetime import datetime
import gzip
import hashlib
import os import os
import shutil
import sys import sys
from urllib.parse import urljoin
from flask import abort, jsonify, redirect, render_template, request, url_for import bs4
from werkzeug.contrib.atom import AtomFeed import requests
from dateutil import tz
from feedgen.feed import FeedGenerator
from flask import (Flask, abort, jsonify, make_response, redirect,
render_template, request, url_for)
from peewee import * # noqa
from . import themes try:
from .models import Bookmark, PublicTag, User, get_tags_for_user # Python 3
from urllib.parse import urljoin, urlparse, urlunparse
except ImportError:
# Python 2
from urlparse import urljoin, urlparse, urlunparse
DIGIMARKS_USER_AGENT = 'digimarks/2.0.0-dev'
DIGIMARKS_USER_AGENT = 'digimarks/1.2.0-dev'
DEFAULT_THEME = 'freshgreen'
themes = {
'green': {
'BROWSERCHROME': '#2e7d32', # green darken-2
'BODY': 'grey lighten-4',
'TEXT': 'black-text',
'TEXTHEX': '#000',
'NAV': 'green darken-3',
'PAGEHEADER': 'grey-text lighten-5',
'MESSAGE_BACKGROUND': 'orange lighten-2',
'MESSAGE_TEXT': 'white-text',
'ERRORMESSAGE_BACKGROUND': 'red darken-1',
'ERRORMESSAGE_TEXT': 'white-text',
'BUTTON': '#1b5e20', # green darken-4
'BUTTON_ACTIVE': '#43a047', # green darken-1
'LINK_TEXT': '#1b5e20', # green darken-4
'CARD_BACKGROUND': 'green darken-3',
'CARD_TEXT': 'white-text',
'CARD_LINK': '#FFF', # white-text
'CHIP_TEXT': '#1b5e20', # green darken-4
'FAB': 'red',
'STAR': 'yellow-text',
'PROBLEM': 'red-text',
'COMMENT': '',
},
'freshgreen': {
'BROWSERCHROME': '#43a047', # green darken-1
'BODY': 'grey lighten-5',
'TEXT': 'black-text',
'TEXTHEX': '#000',
'NAV': 'green darken-1',
'PAGEHEADER': 'grey-text lighten-5',
'MESSAGE_BACKGROUND': 'orange lighten-2',
'MESSAGE_TEXT': 'white-text',
'ERRORMESSAGE_BACKGROUND': 'red darken-1',
'ERRORMESSAGE_TEXT': 'white-text',
'BUTTON': '#1b5e20', # green darken-4
'BUTTON_ACTIVE': '#43a047', # green darken-1
'LINK_TEXT': '#1b5e20', # green darken-4
'CARD_BACKGROUND': 'green darken-1',
'CARD_TEXT': 'white-text',
'CARD_LINK': '#FFF', # white-text
'CHIP_TEXT': '#1b5e20', # green darken-4
'FAB': 'red',
'STAR': 'yellow-text',
'PROBLEM': 'red-text',
'COMMENT': '',
},
'lightblue': {
'BROWSERCHROME': '#0288d1', # light-blue darken-2
'BODY': 'white',
'TEXT': 'black-text',
'TEXTHEX': '#000',
'NAV': 'light-blue darken-2',
'PAGEHEADER': 'grey-text lighten-5',
'MESSAGE_BACKGROUND': 'orange lighten-2',
'MESSAGE_TEXT': 'white-text',
'ERRORMESSAGE_BACKGROUND': 'red darken-1',
'ERRORMESSAGE_TEXT': 'white-text',
'BUTTON': '#fb8c00', # orange darken-1
'BUTTON_ACTIVE': '#ffa726', # orange lighten-1
'LINK_TEXT': '#FFF', # white
'CARD_BACKGROUND': 'light-blue lighten-2',
'CARD_TEXT': 'black-text',
'CARD_LINK': '#263238', # blue-grey-text darken-4
'CHIP_TEXT': '#FFF', # white
'FAB': 'light-blue darken-4',
'STAR': 'yellow-text',
'PROBLEM': 'red-text',
'COMMENT': '',
},
'dark': {
'BROWSERCHROME': '#212121', # grey darken-4
'BODY': 'grey darken-4',
'TEXT': 'grey-text lighten-1',
'TEXTHEX': '#bdbdbd',
'NAV': 'grey darken-3',
'PAGEHEADER': 'grey-text lighten-1',
'MESSAGE_BACKGROUND': 'orange lighten-2',
'MESSAGE_TEXT': 'white-text',
'ERRORMESSAGE_BACKGROUND': 'red darken-1',
'ERRORMESSAGE_TEXT': 'white-text',
'BUTTON': '#fb8c00', # orange darken-1
'BUTTON_ACTIVE': '#ffa726', # orange lighten-1
'LINK_TEXT': '#fb8c00', # orange-text darken-1
'CARD_BACKGROUND': 'grey darken-3',
'CARD_TEXT': 'grey-text lighten-1',
'CARD_LINK': '#fb8c00', # orange-text darken-1
'CHIP_TEXT': '#fb8c00', # orange-text darken-1
'FAB': 'red',
'STAR': 'yellow-text',
'PROBLEM': 'red-text',
'COMMENT': '',
},
'amoled': {
'BROWSERCHROME': '#000', # grey darken-4
'BODY': 'black',
'TEXT': 'grey-text lighten-1',
'TEXTHEX': '#bdbdbd',
'NAV': 'grey darken-3',
'PAGEHEADER': 'grey-text lighten-1',
'MESSAGE_BACKGROUND': 'orange lighten-2',
'MESSAGE_TEXT': 'white-text',
'ERRORMESSAGE_BACKGROUND': 'red darken-1',
'ERRORMESSAGE_TEXT': 'white-text',
'BUTTON': '#fb8c00', # orange darken-1
'BUTTON_ACTIVE': '#ffa726', # orange lighten-1
'LINK_TEXT': '#fb8c00', # orange-text darken-1
'CARD_BACKGROUND': 'grey darken-3',
'CARD_TEXT': 'grey-text lighten-1',
'CARD_LINK': '#fb8c00', # orange-text darken-1
'CHIP_TEXT': '#fb8c00', # orange-text darken-1
'FAB': 'red',
'STAR': 'yellow-text',
'PROBLEM': 'red-text',
'COMMENT': '',
}
}
try: try:
import settings import settings
@@ -29,23 +166,320 @@ DATABASE = {
} }
#PHANTOM = '/usr/local/bin/phantomjs' #PHANTOM = '/usr/local/bin/phantomjs'
#SCRIPT = os.path.join(APP_ROOT, 'screenshot.js') #SCRIPT = os.path.join(APP_ROOT, 'screenshot.js')
SYSTEMKEY = None
try:
SYSTEMKEY = os.environ['SYSTEMKEY']
except KeyError:
print('No ENV var found for SYSTEMKEY')
MASHAPE_API_KEY = None # create our flask app and a database wrapper
app = Flask(__name__)
app.config.from_object(__name__)
database = SqliteDatabase(os.path.join(APP_ROOT, 'bookmarks.db'))
# Strip unnecessary whitespace due to jinja2 codeblocks
app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True
# set custom url for the app, for example '/bookmarks'
try: try:
MASHAPE_API_KEY = os.environ['MASHAPE_API_KEY'] app.config['APPLICATION_ROOT'] = settings.APPLICATION_ROOT
except KeyError: except AttributeError:
print('No ENV var found for MASHAPE_API_KEY') pass
# Cache the tags # Cache the tags
all_tags = {} all_tags = {}
usersettings = {} usersettings = {}
def ifilterfalse(predicate, iterable):
# ifilterfalse(lambda x: x%2, range(10)) --> 0 2 4 6 8
if predicate is None:
predicate = bool
for x in iterable:
if not predicate(x):
yield x
def unique_everseen(iterable, key=None):
"List unique elements, preserving order. Remember all elements ever seen."
# unique_everseen('AAAABBBCCDAABBB') --> A B C D
# unique_everseen('ABBCcAD', str.lower) --> A B C D
seen = set()
seen_add = seen.add
if key is None:
for element in ifilterfalse(seen.__contains__, iterable):
seen_add(element)
yield element
else:
for element in iterable:
k = key(element)
if k not in seen:
seen_add(k)
yield element
def clean_tags(tags_list):
tags_res = [x.strip() for x in tags_list]
tags_res = list(unique_everseen(tags_res))
tags_res.sort()
if tags_res and tags_res[0] == '':
del tags_res[0]
return tags_res
magic_dict = {
b"\x1f\x8b\x08": "gz",
b"\x42\x5a\x68": "bz2",
b"\x50\x4b\x03\x04": "zip"
}
max_len = max(len(x) for x in magic_dict)
def file_type(filename):
with open(filename, "rb") as f:
file_start = f.read(max_len)
for magic, filetype in magic_dict.items():
if file_start.startswith(magic):
return filetype
return "no match"
class BaseModel(Model):
class Meta:
database = database
class User(BaseModel):
""" User account """
username = CharField()
key = CharField()
theme = CharField(default=DEFAULT_THEME)
created_date = DateTimeField(default=datetime.datetime.now)
def generate_key(self):
""" Generate userkey """
self.key = binascii.hexlify(os.urandom(24))
return self.key
class Bookmark(BaseModel):
""" Bookmark instance, connected to User """
# Foreign key to User
userkey = CharField()
title = CharField(default='')
url = CharField()
note = TextField(default='')
#image = CharField(default='')
url_hash = CharField(default='')
tags = CharField(default='')
starred = BooleanField(default=False)
# Website (domain) favicon
favicon = CharField(null=True)
# Status code: 200 is OK, 404 is not found, for example (showing an error)
HTTP_CONNECTIONERROR = 0
HTTP_OK = 200
HTTP_ACCEPTED = 202
HTTP_MOVEDTEMPORARILY = 304
HTTP_NOTFOUND = 404
http_status = IntegerField(default=200)
redirect_uri = None
created_date = DateTimeField(default=datetime.datetime.now)
modified_date = DateTimeField(null=True)
deleted_date = DateTimeField(null=True)
# Bookmark status; deleting doesn't remove from DB
VISIBLE = 0
DELETED = 1
status = IntegerField(default=VISIBLE)
class Meta:
ordering = (('created_date', 'desc'),)
def set_hash(self):
""" Generate hash """
self.url_hash = hashlib.md5(self.url.encode('utf-8')).hexdigest()
def set_title_from_source(self):
""" Request the title by requesting the source url """
try:
result = requests.get(self.url, headers={'User-Agent': DIGIMARKS_USER_AGENT})
self.http_status = result.status_code
except:
# For example 'MissingSchema: Invalid URL 'abc': No schema supplied. Perhaps you meant http://abc?'
self.http_status = 404
if self.http_status == 200 or self.http_status == 202:
html = bs4.BeautifulSoup(result.text, 'html.parser')
try:
self.title = html.title.text.strip()
except AttributeError:
self.title = ''
return self.title
def set_status_code(self):
""" Check the HTTP status of the url, as it might not exist for example """
try:
result = requests.head(self.url, headers={'User-Agent': DIGIMARKS_USER_AGENT})
self.http_status = result.status_code
except requests.ConnectionError:
self.http_status = self.HTTP_CONNECTIONERROR
return self.http_status
def _set_favicon_with_iconsbetterideaorg(self, domain):
""" Fetch favicon for the domain """
fileextension = '.png'
meta = requests.head(
'http://icons.better-idea.org/icon?size=60&url=' + domain,
allow_redirects=True,
headers={'User-Agent': DIGIMARKS_USER_AGENT}
)
if meta.url[-3:].lower() == 'ico':
fileextension = '.ico'
response = requests.get(
'http://icons.better-idea.org/icon?size=60&url=' + domain,
stream=True,
headers={'User-Agent': DIGIMARKS_USER_AGENT}
)
filename = os.path.join(MEDIA_ROOT, 'favicons/' + domain + fileextension)
with open(filename, 'wb') as out_file:
shutil.copyfileobj(response.raw, out_file)
del response
filetype = file_type(filename)
if filetype == 'gz':
# decompress
orig = gzip.GzipFile(filename, 'rb')
origcontent = orig.read()
orig.close()
os.remove(filename)
with open(filename, 'wb') as new:
new.write(origcontent)
self.favicon = domain + fileextension
def _set_favicon_with_realfavicongenerator(self, domain):
""" Fetch favicon for the domain """
response = requests.get(
'https://realfavicongenerator.p.rapidapi.com/favicon/icon?platform=android_chrome&site=' + domain,
stream=True,
headers={'User-Agent': DIGIMARKS_USER_AGENT, 'X-Mashape-Key': settings.MASHAPE_API_KEY}
)
if response.status_code == 404:
# Fall back to desktop favicon
response = requests.get(
'https://realfavicongenerator.p.rapidapi.com/favicon/icon?platform=desktop&site=' + domain,
stream=True,
headers={'User-Agent': DIGIMARKS_USER_AGENT, 'X-Mashape-Key': settings.MASHAPE_API_KEY}
)
# Debug for the moment
print(domain)
print(response.headers)
if 'Content-Length' in response.headers and response.headers['Content-Length'] == '0':
# No favicon found, likely
print('Skipping this favicon, needs fallback')
return
# Default to 'image/png'
fileextension = '.png'
if response.headers['content-type'] == 'image/jpeg':
fileextension = '.jpg'
if response.headers['content-type'] == 'image/x-icon':
fileextension = '.ico'
filename = os.path.join(MEDIA_ROOT, 'favicons/' + domain + fileextension)
with open(filename, 'wb') as out_file:
shutil.copyfileobj(response.raw, out_file)
del response
filetype = file_type(filename)
if filetype == 'gz':
# decompress
orig = gzip.GzipFile(filename, 'rb')
origcontent = orig.read()
orig.close()
os.remove(filename)
with open(filename, 'wb') as new:
new.write(origcontent)
self.favicon = domain + fileextension
def set_favicon(self):
""" Fetch favicon for the domain """
u = urlparse(self.url)
domain = u.netloc
if os.path.isfile(os.path.join(MEDIA_ROOT, 'favicons/' + domain + '.png')):
# If file exists, don't re-download it
self.favicon = domain + '.png'
return
if os.path.isfile(os.path.join(MEDIA_ROOT, 'favicons/' + domain + '.ico')):
# If file exists, don't re-download it
self.favicon = domain + '.ico'
return
#self._set_favicon_with_iconsbetterideaorg(domain)
self._set_favicon_with_realfavicongenerator(domain)
def set_tags(self, newtags):
""" Set tags from `tags`, strip and sort them """
tags_split = newtags.split(',')
tags_clean = clean_tags(tags_split)
self.tags = ','.join(tags_clean)
def get_redirect_uri(self):
if self.redirect_uri:
return self.redirect_uri
if self.http_status == 301 or self.http_status == 302:
result = requests.head(self.url, allow_redirects=True, headers={'User-Agent': DIGIMARKS_USER_AGENT})
self.http_status = result.status_code
self.redirect_uri = result.url
return result.url
return None
def get_uri_domain(self):
parsed = urlparse(self.url)
return parsed.hostname
@classmethod
def strip_url_params(cls, url):
parsed = urlparse(url)
return urlunparse((parsed.scheme, parsed.netloc, parsed.path, parsed.params, '', parsed.fragment))
@property
def tags_list(self):
""" Get the tags as a list, iterable in template """
if self.tags:
return self.tags.split(',')
return []
def to_dict(self):
result = {
'title': self.title,
'url': self.url,
'created': self.created_date.strftime('%Y-%m-%d %H:%M:%S'),
'url_hash': self.url_hash,
'tags': self.tags,
}
return result
@property
def serialize(self):
return self.to_dict()
class PublicTag(BaseModel):
""" Publicly shared tag """
tagkey = CharField()
userkey = CharField()
tag = CharField()
created_date = DateTimeField(default=datetime.datetime.now)
def generate_key(self):
""" Generate hash-based key for publicly shared tag """
self.tagkey = binascii.hexlify(os.urandom(16))
def get_tags_for_user(userkey):
""" Extract all tags from the bookmarks """
bookmarks = Bookmark.select().filter(Bookmark.userkey == userkey, Bookmark.status == Bookmark.VISIBLE)
tags = []
for bookmark in bookmarks:
tags += bookmark.tags_list
return clean_tags(tags)
def get_cached_tags(userkey): def get_cached_tags(userkey):
""" Fail-safe way to get the cached tags for `userkey` """ """ Fail-safe way to get the cached tags for `userkey` """
try: try:
@@ -57,9 +491,9 @@ def get_cached_tags(userkey):
def get_theme(userkey): def get_theme(userkey):
try: try:
usertheme = usersettings[userkey]['theme'] usertheme = usersettings[userkey]['theme']
return themes.themes[usertheme] return themes[usertheme]
except KeyError: except KeyError:
return themes.themes[themes.DEFAULT_THEME] # default return themes[DEFAULT_THEME] # default
def make_external(url): def make_external(url):
@@ -80,14 +514,14 @@ def _find_bookmarks(userkey, filter_text):
@app.errorhandler(404) @app.errorhandler(404)
def page_not_found(e): def page_not_found(e):
theme = themes.themes[themes.DEFAULT_THEME] theme = themes[DEFAULT_THEME]
return render_template('404.html', error=e, theme=theme), 404 return render_template('404.html', error=e, theme=theme), 404
@app.route('/') @app.route('/')
def index(): def index():
""" Homepage, point visitors to project page """ """ Homepage, point visitors to project page """
theme = themes.themes[themes.DEFAULT_THEME] theme = themes[DEFAULT_THEME]
return render_template('index.html', theme=theme) return render_template('index.html', theme=theme)
@@ -171,10 +605,12 @@ def bookmarks_js(userkey):
Bookmark.userkey == userkey, Bookmark.userkey == userkey,
Bookmark.status == Bookmark.VISIBLE Bookmark.status == Bookmark.VISIBLE
).order_by(Bookmark.created_date.desc()) ).order_by(Bookmark.created_date.desc())
return render_template( resp = make_response(render_template(
'bookmarks.js', 'bookmarks.js',
bookmarks=bookmarks bookmarks=bookmarks
) ))
resp.headers['Content-type'] = 'text/javascript; charset=utf-8'
return resp
@app.route('/r/<userkey>/<urlhash>') @app.route('/r/<userkey>/<urlhash>')
@@ -486,7 +922,7 @@ def publictag_page(tagkey):
#this_tag = get_object_or_404(PublicTag.select().where(PublicTag.tagkey == tagkey)) #this_tag = get_object_or_404(PublicTag.select().where(PublicTag.tagkey == tagkey))
try: try:
this_tag, bookmarks = get_publictag(tagkey) this_tag, bookmarks = get_publictag(tagkey)
theme = themes.themes[themes.DEFAULT_THEME] theme = themes[DEFAULT_THEME]
return render_template( return render_template(
'publicbookmarks.html', 'publicbookmarks.html',
bookmarks=bookmarks, bookmarks=bookmarks,
@@ -527,23 +963,34 @@ def publictag_feed(tagkey):
Bookmark.tags.contains(this_tag.tag), Bookmark.tags.contains(this_tag.tag),
Bookmark.status == Bookmark.VISIBLE Bookmark.status == Bookmark.VISIBLE
) )
feed = AtomFeed(this_tag.tag, feed_url=request.url, url=make_external(url_for('publictag_page', tagkey=tagkey)))
feed = FeedGenerator()
feed.title(this_tag.tag)
feed.id(request.url)
feed.link(href=request.url, rel='self')
feed.link(href=make_external(url_for('publictag_page', tagkey=tagkey)))
for bookmark in bookmarks: for bookmark in bookmarks:
entry = feed.add_entry()
updated_date = bookmark.modified_date updated_date = bookmark.modified_date
if not bookmark.modified_date: if not bookmark.modified_date:
updated_date = bookmark.created_date updated_date = bookmark.created_date
bookmarktitle = '{} (no title)'.format(bookmark.url) bookmarktitle = '{} (no title)'.format(bookmark.url)
if bookmark.title: if bookmark.title:
bookmarktitle = bookmark.title bookmarktitle = bookmark.title
feed.add(
bookmarktitle, entry.id(bookmark.url)
content_type='html', entry.title(bookmarktitle)
author='digimarks', entry.link(href=bookmark.url)
url=bookmark.url, entry.author(name='digimarks')
updated=updated_date, entry.pubdate(bookmark.created_date.replace(tzinfo=tz.tzlocal()))
published=bookmark.created_date entry.published(bookmark.created_date.replace(tzinfo=tz.tzlocal()))
) entry.updated(updated_date.replace(tzinfo=tz.tzlocal()))
return feed.get_response()
response = make_response(feed.atom_str(pretty=True))
response.headers.set('Content-Type', 'application/atom+xml')
return response
except PublicTag.DoesNotExist: except PublicTag.DoesNotExist:
abort(404) abort(404)
@@ -636,7 +1083,6 @@ def findmissingfavicons(systemkey):
# Initialisation == create the bookmark, user and public tag tables if they do not exist # Initialisation == create the bookmark, user and public tag tables if they do not exist
# TODO: move to __init__.py
Bookmark.create_table(True) Bookmark.create_table(True)
User.create_table(True) User.create_table(True)
PublicTag.create_table(True) PublicTag.create_table(True)
@@ -647,3 +1093,8 @@ for user in users:
all_tags[user.key] = get_tags_for_user(user.key) all_tags[user.key] = get_tags_for_user(user.key)
usersettings[user.key] = {'theme': user.theme} usersettings[user.key] = {'theme': user.theme}
print(user.key) print(user.key)
# Run when called standalone
if __name__ == '__main__':
# run the application
app.run(host='0.0.0.0', port=9999, debug=True)

View File

@@ -1,29 +0,0 @@
import os
from flask import Flask
def create_app(test_config=None):
# create our flask app and a database wrapper
app = Flask(__name__)
app.config.from_object(__name__)
if test_config is None:
# load the instance config, if it exists, when not testing
app.config.from_pyfile('settings.py', silent=True)
else:
# load the test config if passed in
app.config.from_mapping(test_config)
# Strip unnecessary whitespace due to jinja2 codeblocks
app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True
# set custom url for the app, for example '/bookmarks'
try:
# TODO: get settings from ENV vars
app.config['APPLICATION_ROOT'] = os.environ['APPLICATION_ROOT']
except AttributeError:
pass
return app

View File

@@ -1,312 +0,0 @@
"""digimarks data models and accompanying convenience functions"""
import binascii
import datetime
import gzip
import hashlib
import os
import shutil
from urllib.parse import urlparse, urlunparse
import bs4
import requests
from peewee import * # noqa
from . import themes
DATABASE_PATH = os.path.dirname(os.path.realpath(__file__))
if 'DIGIMARKS_DB_PATH' in os.environ:
DATABASE_PATH = os.environ['DIGIMARKS_DB_PATH']
database = SqliteDatabase(os.path.join(DATABASE_PATH, 'bookmarks.db'))
def ifilterfalse(predicate, iterable):
# ifilterfalse(lambda x: x%2, range(10)) --> 0 2 4 6 8
if predicate is None:
predicate = bool
for x in iterable:
if not predicate(x):
yield x
def unique_everseen(iterable, key=None):
"List unique elements, preserving order. Remember all elements ever seen."
# unique_everseen('AAAABBBCCDAABBB') --> A B C D
# unique_everseen('ABBCcAD', str.lower) --> A B C D
seen = set()
seen_add = seen.add
if key is None:
for element in ifilterfalse(seen.__contains__, iterable):
seen_add(element)
yield element
else:
for element in iterable:
k = key(element)
if k not in seen:
seen_add(k)
yield element
def clean_tags(tags_list):
tags_res = [x.strip() for x in tags_list]
tags_res = list(unique_everseen(tags_res))
tags_res.sort()
if tags_res and tags_res[0] == '':
del tags_res[0]
return tags_res
magic_dict = {
b"\x1f\x8b\x08": "gz",
b"\x42\x5a\x68": "bz2",
b"\x50\x4b\x03\x04": "zip"
}
max_len = max(len(x) for x in magic_dict)
def file_type(filename):
with open(filename, "rb") as f:
file_start = f.read(max_len)
for magic, filetype in magic_dict.items():
if file_start.startswith(magic):
return filetype
return "no match"
class BaseModel(Model):
class Meta:
database = database
class User(BaseModel):
""" User account """
username = CharField()
key = CharField()
theme = CharField(default=themes.DEFAULT_THEME)
created_date = DateTimeField(default=datetime.datetime.now)
def generate_key(self):
""" Generate userkey """
self.key = binascii.hexlify(os.urandom(24))
return self.key
class Bookmark(BaseModel):
""" Bookmark instance, connected to User """
# Foreign key to User
userkey = CharField()
title = CharField(default='')
url = CharField()
note = TextField(default='')
#image = CharField(default='')
url_hash = CharField(default='')
tags = CharField(default='')
starred = BooleanField(default=False)
# Website (domain) favicon
favicon = CharField(null=True)
# Status code: 200 is OK, 404 is not found, for example (showing an error)
HTTP_CONNECTIONERROR = 0
HTTP_OK = 200
HTTP_ACCEPTED = 202
HTTP_MOVEDTEMPORARILY = 304
HTTP_NOTFOUND = 404
http_status = IntegerField(default=200)
redirect_uri = None
created_date = DateTimeField(default=datetime.datetime.now)
modified_date = DateTimeField(null=True)
deleted_date = DateTimeField(null=True)
# Bookmark status; deleting doesn't remove from DB
VISIBLE = 0
DELETED = 1
status = IntegerField(default=VISIBLE)
class Meta:
ordering = (('created_date', 'desc'),)
def set_hash(self):
""" Generate hash """
self.url_hash = hashlib.md5(self.url.encode('utf-8')).hexdigest()
def set_title_from_source(self):
""" Request the title by requesting the source url """
try:
result = requests.get(self.url, headers={'User-Agent': DIGIMARKS_USER_AGENT})
self.http_status = result.status_code
except:
# For example 'MissingSchema: Invalid URL 'abc': No schema supplied. Perhaps you meant http://abc?'
self.http_status = 404
if self.http_status == 200 or self.http_status == 202:
html = bs4.BeautifulSoup(result.text, 'html.parser')
try:
self.title = html.title.text.strip()
except AttributeError:
self.title = ''
return self.title
def set_status_code(self):
""" Check the HTTP status of the url, as it might not exist for example """
try:
result = requests.head(self.url, headers={'User-Agent': DIGIMARKS_USER_AGENT})
self.http_status = result.status_code
except requests.ConnectionError:
self.http_status = self.HTTP_CONNECTIONERROR
return self.http_status
def _set_favicon_with_iconsbetterideaorg(self, domain):
""" Fetch favicon for the domain """
fileextension = '.png'
meta = requests.head(
'http://icons.better-idea.org/icon?size=60&url=' + domain,
allow_redirects=True,
headers={'User-Agent': DIGIMARKS_USER_AGENT}
)
if meta.url[-3:].lower() == 'ico':
fileextension = '.ico'
response = requests.get(
'http://icons.better-idea.org/icon?size=60&url=' + domain,
stream=True,
headers={'User-Agent': DIGIMARKS_USER_AGENT}
)
filename = os.path.join(MEDIA_ROOT, 'favicons/' + domain + fileextension)
with open(filename, 'wb') as out_file:
shutil.copyfileobj(response.raw, out_file)
del response
filetype = file_type(filename)
if filetype == 'gz':
# decompress
orig = gzip.GzipFile(filename, 'rb')
origcontent = orig.read()
orig.close()
os.remove(filename)
with open(filename, 'wb') as new:
new.write(origcontent)
self.favicon = domain + fileextension
def _set_favicon_with_realfavicongenerator(self, domain):
""" Fetch favicon for the domain """
response = requests.get(
'https://realfavicongenerator.p.rapidapi.com/favicon/icon?platform=android_chrome&site=' + domain,
stream=True,
headers={'User-Agent': DIGIMARKS_USER_AGENT, 'X-Mashape-Key': MASHAPE_API_KEY}
)
if response.status_code == 404:
# Fall back to desktop favicon
response = requests.get(
'https://realfavicongenerator.p.rapidapi.com/favicon/icon?platform=desktop&site=' + domain,
stream=True,
headers={'User-Agent': DIGIMARKS_USER_AGENT, 'X-Mashape-Key': settings.MASHAPE_API_KEY}
)
# Debug for the moment
print(domain)
print(response.headers)
if 'Content-Length' in response.headers and response.headers['Content-Length'] == '0':
# No favicon found, likely
print('Skipping this favicon, needs fallback')
return
# Default to 'image/png'
fileextension = '.png'
if response.headers['content-type'] == 'image/jpeg':
fileextension = '.jpg'
if response.headers['content-type'] == 'image/x-icon':
fileextension = '.ico'
filename = os.path.join(MEDIA_ROOT, 'favicons/' + domain + fileextension)
with open(filename, 'wb') as out_file:
shutil.copyfileobj(response.raw, out_file)
del response
filetype = file_type(filename)
if filetype == 'gz':
# decompress
orig = gzip.GzipFile(filename, 'rb')
origcontent = orig.read()
orig.close()
os.remove(filename)
with open(filename, 'wb') as new:
new.write(origcontent)
self.favicon = domain + fileextension
def set_favicon(self):
""" Fetch favicon for the domain """
u = urlparse(self.url)
domain = u.netloc
if os.path.isfile(os.path.join(MEDIA_ROOT, 'favicons/' + domain + '.png')):
# If file exists, don't re-download it
self.favicon = domain + '.png'
return
if os.path.isfile(os.path.join(MEDIA_ROOT, 'favicons/' + domain + '.ico')):
# If file exists, don't re-download it
self.favicon = domain + '.ico'
return
#self._set_favicon_with_iconsbetterideaorg(domain)
self._set_favicon_with_realfavicongenerator(domain)
def set_tags(self, newtags):
""" Set tags from `tags`, strip and sort them """
tags_split = newtags.split(',')
tags_clean = clean_tags(tags_split)
self.tags = ','.join(tags_clean)
def get_redirect_uri(self):
if self.redirect_uri:
return self.redirect_uri
if self.http_status == 301 or self.http_status == 302:
result = requests.head(self.url, allow_redirects=True, headers={'User-Agent': DIGIMARKS_USER_AGENT})
self.http_status = result.status_code
self.redirect_uri = result.url
return result.url
return None
def get_uri_domain(self):
parsed = urlparse(self.url)
return parsed.hostname
@classmethod
def strip_url_params(cls, url):
parsed = urlparse(url)
return urlunparse((parsed.scheme, parsed.netloc, parsed.path, parsed.params, '', parsed.fragment))
@property
def tags_list(self):
""" Get the tags as a list, iterable in template """
if self.tags:
return self.tags.split(',')
return []
def to_dict(self):
result = {
'title': self.title,
'url': self.url,
'created': self.created_date.strftime('%Y-%m-%d %H:%M:%S'),
'url_hash': self.url_hash,
'tags': self.tags,
}
return result
@property
def serialize(self):
return self.to_dict()
class PublicTag(BaseModel):
""" Publicly shared tag """
tagkey = CharField()
userkey = CharField()
tag = CharField()
created_date = DateTimeField(default=datetime.datetime.now)
def generate_key(self):
""" Generate hash-based key for publicly shared tag """
self.tagkey = binascii.hexlify(os.urandom(16))
def get_tags_for_user(userkey):
""" Extract all tags from the bookmarks """
bookmarks = Bookmark.select().filter(Bookmark.userkey == userkey, Bookmark.status == Bookmark.VISIBLE)
tags = []
for bookmark in bookmarks:
tags += bookmark.tags_list
return clean_tags(tags)

View File

@@ -1,125 +0,0 @@
"""digimarks theme definitions"""
DEFAULT_THEME = 'freshgreen'
themes = {
'green': {
'BROWSERCHROME': '#2e7d32', # green darken-2
'BODY': 'grey lighten-4',
'TEXT': 'black-text',
'TEXTHEX': '#000',
'NAV': 'green darken-3',
'PAGEHEADER': 'grey-text lighten-5',
'MESSAGE_BACKGROUND': 'orange lighten-2',
'MESSAGE_TEXT': 'white-text',
'ERRORMESSAGE_BACKGROUND': 'red darken-1',
'ERRORMESSAGE_TEXT': 'white-text',
'BUTTON': '#1b5e20', # green darken-4
'BUTTON_ACTIVE': '#43a047', # green darken-1
'LINK_TEXT': '#1b5e20', # green darken-4
'CARD_BACKGROUND': 'green darken-3',
'CARD_TEXT': 'white-text',
'CARD_LINK': '#FFF', # white-text
'CHIP_TEXT': '#1b5e20', # green darken-4
'FAB': 'red',
'STAR': 'yellow-text',
'PROBLEM': 'red-text',
'COMMENT': '',
},
'freshgreen': {
'BROWSERCHROME': '#43a047', # green darken-1
'BODY': 'grey lighten-5',
'TEXT': 'black-text',
'TEXTHEX': '#000',
'NAV': 'green darken-1',
'PAGEHEADER': 'grey-text lighten-5',
'MESSAGE_BACKGROUND': 'orange lighten-2',
'MESSAGE_TEXT': 'white-text',
'ERRORMESSAGE_BACKGROUND': 'red darken-1',
'ERRORMESSAGE_TEXT': 'white-text',
'BUTTON': '#1b5e20', # green darken-4
'BUTTON_ACTIVE': '#43a047', # green darken-1
'LINK_TEXT': '#1b5e20', # green darken-4
'CARD_BACKGROUND': 'green darken-1',
'CARD_TEXT': 'white-text',
'CARD_LINK': '#FFF', # white-text
'CHIP_TEXT': '#1b5e20', # green darken-4
'FAB': 'red',
'STAR': 'yellow-text',
'PROBLEM': 'red-text',
'COMMENT': '',
},
'lightblue': {
'BROWSERCHROME': '#0288d1', # light-blue darken-2
'BODY': 'white',
'TEXT': 'black-text',
'TEXTHEX': '#000',
'NAV': 'light-blue darken-2',
'PAGEHEADER': 'grey-text lighten-5',
'MESSAGE_BACKGROUND': 'orange lighten-2',
'MESSAGE_TEXT': 'white-text',
'ERRORMESSAGE_BACKGROUND': 'red darken-1',
'ERRORMESSAGE_TEXT': 'white-text',
'BUTTON': '#fb8c00', # orange darken-1
'BUTTON_ACTIVE': '#ffa726', # orange lighten-1
'LINK_TEXT': '#FFF', # white
'CARD_BACKGROUND': 'light-blue lighten-2',
'CARD_TEXT': 'black-text',
'CARD_LINK': '#263238', # blue-grey-text darken-4
'CHIP_TEXT': '#FFF', # white
'FAB': 'light-blue darken-4',
'STAR': 'yellow-text',
'PROBLEM': 'red-text',
'COMMENT': '',
},
'dark': {
'BROWSERCHROME': '#212121', # grey darken-4
'BODY': 'grey darken-4',
'TEXT': 'grey-text lighten-1',
'TEXTHEX': '#bdbdbd',
'NAV': 'grey darken-3',
'PAGEHEADER': 'grey-text lighten-1',
'MESSAGE_BACKGROUND': 'orange lighten-2',
'MESSAGE_TEXT': 'white-text',
'ERRORMESSAGE_BACKGROUND': 'red darken-1',
'ERRORMESSAGE_TEXT': 'white-text',
'BUTTON': '#fb8c00', # orange darken-1
'BUTTON_ACTIVE': '#ffa726', # orange lighten-1
'LINK_TEXT': '#fb8c00', # orange-text darken-1
'CARD_BACKGROUND': 'grey darken-3',
'CARD_TEXT': 'grey-text lighten-1',
'CARD_LINK': '#fb8c00', # orange-text darken-1
'CHIP_TEXT': '#fb8c00', # orange-text darken-1
'FAB': 'red',
'STAR': 'yellow-text',
'PROBLEM': 'red-text',
'COMMENT': '',
},
'amoled': {
'BROWSERCHROME': '#000', # grey darken-4
'BODY': 'black',
'TEXT': 'grey-text lighten-1',
'TEXTHEX': '#bdbdbd',
'NAV': 'grey darken-3',
'PAGEHEADER': 'grey-text lighten-1',
'MESSAGE_BACKGROUND': 'orange lighten-2',
'MESSAGE_TEXT': 'white-text',
'ERRORMESSAGE_BACKGROUND': 'red darken-1',
'ERRORMESSAGE_TEXT': 'white-text',
'BUTTON': '#fb8c00', # orange darken-1
'BUTTON_ACTIVE': '#ffa726', # orange lighten-1
'LINK_TEXT': '#fb8c00', # orange-text darken-1
'CARD_BACKGROUND': 'grey darken-3',
'CARD_TEXT': 'grey-text lighten-1',
'CARD_LINK': '#fb8c00', # orange-text darken-1
'CHIP_TEXT': '#fb8c00', # orange-text darken-1
'FAB': 'red',
'STAR': 'yellow-text',
'PROBLEM': 'red-text',
'COMMENT': '',
}
}

View File

@@ -10,6 +10,10 @@ DEBUG = False
# echo -n "yourstring" | sha1sum # echo -n "yourstring" | sha1sum
SYSTEMKEY = 'S3kr1t' SYSTEMKEY = 'S3kr1t'
# RapidAPI key for favicons
# https://rapidapi.com/realfavicongenerator/api/realfavicongenerator
MASHAPE_API_KEY = 'your_MASHAPE_key'
LOG_LOCATION = 'digimarks.log' LOG_LOCATION = 'digimarks.log'
#LOG_LOCATION = '/var/log/digimarks/digimarks.log' #LOG_LOCATION = '/var/log/digimarks/digimarks.log'
# How many logs to keep in log rotation: # How many logs to keep in log rotation:

View File

@@ -1,29 +1,61 @@
# # This file was autogenerated by uv via the following command:
# This file is autogenerated by pip-compile # uv pip compile requirements-dev.in
# To update, run: astroid==3.3.10
# # via pylint
# pip-compile requirements-dev.in beautifulsoup4==4.13.4
# # via bs4
astroid==2.3.3 # via pylint blinker==1.9.0
beautifulsoup4==4.8.2 # via bs4 # via flask
bs4==0.0.1 bs4==0.0.2
certifi==2019.11.28 # via requests # via -r requirements.in
chardet==3.0.4 # via requests certifi==2025.6.15
click==7.0 # via flask # via requests
flask==1.1.1 charset-normalizer==3.4.2
idna==2.8 # via requests # via requests
isort==4.3.21 # via pylint click==8.2.1
itsdangerous==1.1.0 # via flask # via flask
jinja2==2.10.3 # via flask dill==0.4.0
lazy-object-proxy==1.4.3 # via astroid # via pylint
markupsafe==1.1.1 # via jinja2 feedgen==1.0.0
mccabe==0.6.1 # via pylint # via -r requirements.in
peewee==3.13.1 flask==3.1.1
pylint==2.4.4 # via -r requirements.in
requests==2.22.0 idna==3.10
six==1.13.0 # via astroid # via requests
soupsieve==1.9.5 # via beautifulsoup4 isort==6.0.1
typed-ast==1.4.0 # via astroid # via pylint
urllib3==1.25.7 # via requests itsdangerous==2.2.0
werkzeug==0.16.0 # via flask # via flask
wrapt==1.11.2 # via astroid jinja2==3.1.6
# via flask
lxml==6.0.0
# via feedgen
markupsafe==3.0.2
# via
# flask
# jinja2
# werkzeug
mccabe==0.7.0
# via pylint
peewee==3.18.2
# via -r requirements.in
platformdirs==4.3.8
# via pylint
pylint==3.3.7
# via -r requirements-dev.in
python-dateutil==2.9.0.post0
# via feedgen
requests==2.32.4
# via -r requirements.in
six==1.17.0
# via python-dateutil
soupsieve==2.7
# via beautifulsoup4
tomlkit==0.13.3
# via pylint
typing-extensions==4.14.1
# via beautifulsoup4
urllib3==2.5.0
# via requests
werkzeug==3.1.3
# via flask

View File

@@ -1,4 +1,10 @@
# Core application
flask flask
peewee peewee
# Fetch title etc from links
bs4 bs4
requests requests
# Generate (atom) feeds for tags and such
feedgen

View File

@@ -1,21 +1,47 @@
# # This file was autogenerated by uv via the following command:
# This file is autogenerated by pip-compile # uv pip compile requirements.in
# To update, run: beautifulsoup4==4.13.4
# # via bs4
# pip-compile requirements.in --upgrade blinker==1.9.0
# # via flask
beautifulsoup4==4.7.1 # via bs4 bs4==0.0.2
bs4==0.0.1 # via -r requirements.in
certifi==2019.3.9 # via requests certifi==2025.6.15
chardet==3.0.4 # via requests # via requests
click==7.0 # via flask charset-normalizer==3.4.2
flask==1.0.2 # via requests
idna==2.8 # via requests click==8.2.1
itsdangerous==1.1.0 # via flask # via flask
jinja2==2.10.1 # via flask feedgen==1.0.0
markupsafe==1.1.1 # via jinja2 # via -r requirements.in
peewee==3.9.3 flask==3.1.1
requests==2.21.0 # via -r requirements.in
soupsieve==1.9 # via beautifulsoup4 idna==3.10
urllib3==1.24.1 # via requests # via requests
werkzeug==0.15.2 # via flask itsdangerous==2.2.0
# via flask
jinja2==3.1.6
# via flask
lxml==6.0.0
# via feedgen
markupsafe==3.0.2
# via
# flask
# jinja2
# werkzeug
peewee==3.18.2
# via -r requirements.in
python-dateutil==2.9.0.post0
# via feedgen
requests==2.32.4
# via -r requirements.in
six==1.17.0
# via python-dateutil
soupsieve==2.7
# via beautifulsoup4
typing-extensions==4.14.1
# via beautifulsoup4
urllib3==2.5.0
# via requests
werkzeug==3.1.3
# via flask

View File

@@ -5,12 +5,11 @@ https://packaging.python.org/en/latest/distributing.html
https://github.com/pypa/sampleproject https://github.com/pypa/sampleproject
""" """
from setuptools import setup
# To use a consistent encoding # To use a consistent encoding
from codecs import open as codecopen from codecs import open as codecopen
from os import path from os import path
from setuptools import setup
here = path.abspath(path.dirname(__file__)) here = path.abspath(path.dirname(__file__))
# Get the long description from the relevant file # Get the long description from the relevant file
@@ -19,8 +18,7 @@ with codecopen(path.join(here, 'README.rst'), encoding='utf-8') as f:
setup( setup(
name='digimarks', # pip install digimarks name='digimarks', # pip install digimarks
description='Simple bookmarking service, using a SQLite database to store bookmarks, supporting tags, automatic ' description='Simple bookmarking service, using a SQLite database to store bookmarks, supporting tags, automatic title fetching and REST API calls.',
'title fetching and REST API calls.',
#long_description=open('README.md', 'rt').read(), #long_description=open('README.md', 'rt').read(),
long_description=long_description, long_description=long_description,
@@ -37,7 +35,7 @@ setup(
# as a practice no need to hard code version unless you know program wont # as a practice no need to hard code version unless you know program wont
# work unless the specific versions are used # work unless the specific versions are used
install_requires=['Flask', 'Peewee', 'requests', 'bs4'], install_requires=['Flask', 'Peewee', 'Flask-Peewee', 'requests', 'bs4'],
py_modules=['digimarks'], py_modules=['digimarks'],

View File

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 89 KiB

View File

Before

Width:  |  Height:  |  Size: 254 B

After

Width:  |  Height:  |  Size: 254 B