mirror of
https://github.com/aquatix/digimarks.git
synced 2025-12-06 22:05:09 +01:00
Refactoring to FastAPI and SQLAlchemy
This commit is contained in:
21
README.rst
21
README.rst
@@ -46,6 +46,18 @@ url's when wanted.
|
|||||||
|
|
||||||
Url's are of the form https://marks.example.com/<userkey>/<action>
|
Url's are of the form https://marks.example.com/<userkey>/<action>
|
||||||
|
|
||||||
|
digimarks can also be run from the command line: ``uvicorn digimarks:app --reload``
|
||||||
|
|
||||||
|
Be sure to export/set the ``SECRETKEY`` environment variable before running, it's needed for some management URI's.
|
||||||
|
|
||||||
|
Run ``gunicorn -k uvicorn.workers.UvicornWorker`` for production. For an example of how to set up a server `see this article <https://www.slingacademy.com/article/deploying-fastapi-on-ubuntu-with-nginx-and-lets-encrypt/>`_ with configuration for nginx, uvicorn, systemd, security and such.
|
||||||
|
|
||||||
|
The RQ background worker can be run from the command line: ``rq worker --with-scheduler``
|
||||||
|
|
||||||
|
Url's are of the form https://hook.example.com/app/<appkey>/<triggerkey>
|
||||||
|
|
||||||
|
API documentation is auto-generated, and can be browsed at https://hook.example.com/docs
|
||||||
|
|
||||||
|
|
||||||
Bookmarklet
|
Bookmarklet
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
@@ -74,8 +86,9 @@ If you for whatever reason would lose this user key, just either look on the con
|
|||||||
Server configuration
|
Server configuration
|
||||||
~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
* `vhost for Apache2.4`_
|
* `systemd for digimarks API`_ which uses the `gunicorn config`_
|
||||||
* `uwsgi.ini`_
|
* `nginx for digimarks API`_
|
||||||
|
* `more config`_
|
||||||
|
|
||||||
|
|
||||||
What's new?
|
What's new?
|
||||||
@@ -91,7 +104,6 @@ Attributions
|
|||||||
|
|
||||||
|
|
||||||
.. _digimarks: https://github.com/aquatix/digimarks
|
.. _digimarks: https://github.com/aquatix/digimarks
|
||||||
.. _webhook: https://en.wikipedia.org/wiki/Webhook
|
|
||||||
.. |PyPI version| image:: https://img.shields.io/pypi/v/digimarks.svg
|
.. |PyPI version| image:: https://img.shields.io/pypi/v/digimarks.svg
|
||||||
:target: https://pypi.python.org/pypi/digimarks/
|
:target: https://pypi.python.org/pypi/digimarks/
|
||||||
.. |PyPI license| image:: https://img.shields.io/github/license/aquatix/digimarks.svg
|
.. |PyPI license| image:: https://img.shields.io/github/license/aquatix/digimarks.svg
|
||||||
@@ -107,3 +119,6 @@ Attributions
|
|||||||
.. _uwsgi.ini: https://github.com/aquatix/digimarks/blob/master/example_config/uwsgi.ini
|
.. _uwsgi.ini: https://github.com/aquatix/digimarks/blob/master/example_config/uwsgi.ini
|
||||||
.. _Changelog: https://github.com/aquatix/digimarks/blob/master/CHANGELOG.md
|
.. _Changelog: https://github.com/aquatix/digimarks/blob/master/CHANGELOG.md
|
||||||
.. _Freepik: http://www.flaticon.com/free-icon/letter-m_2041
|
.. _Freepik: http://www.flaticon.com/free-icon/letter-m_2041
|
||||||
|
.. _systemd for digimarks API: https://github.com/aquatix/digimarks/blob/master/example_config/systemd/digimarks.service
|
||||||
|
.. _gunicorn config: https://github.com/aquatix/digimarks/blob/master/example_config/gunicorn_digimarks_conf.py
|
||||||
|
.. _more config: https://github.com/aquatix/digimarks/tree/master/example_config
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
-r requirements.in
|
-r requirements.in
|
||||||
|
|
||||||
|
black
|
||||||
pylint
|
pylint
|
||||||
|
ruff
|
||||||
|
|
||||||
|
build
|
||||||
|
twine
|
||||||
|
|||||||
@@ -1,71 +1,223 @@
|
|||||||
#
|
#
|
||||||
# This file is autogenerated by pip-compile with python 3.10
|
# This file is autogenerated by pip-compile with Python 3.10
|
||||||
# To update, run:
|
# by the following command:
|
||||||
#
|
#
|
||||||
# pip-compile requirements-dev.in
|
# pip-compile requirements-dev.in
|
||||||
#
|
#
|
||||||
anyio==3.6.1
|
annotated-types==0.5.0
|
||||||
# via starlette
|
# via pydantic
|
||||||
astroid==2.11.7
|
anyio==3.7.1
|
||||||
|
# via
|
||||||
|
# httpcore
|
||||||
|
# starlette
|
||||||
|
# watchfiles
|
||||||
|
astroid==2.15.6
|
||||||
# via pylint
|
# via pylint
|
||||||
beautifulsoup4==4.11.1
|
beautifulsoup4==4.12.2
|
||||||
# via bs4
|
# via bs4
|
||||||
|
black==23.7.0
|
||||||
|
# via -r requirements-dev.in
|
||||||
|
bleach==6.0.0
|
||||||
|
# via readme-renderer
|
||||||
bs4==0.0.1
|
bs4==0.0.1
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
certifi==2022.6.15
|
build==0.10.0
|
||||||
|
# via -r requirements-dev.in
|
||||||
|
certifi==2023.7.22
|
||||||
|
# via
|
||||||
|
# httpcore
|
||||||
|
# httpx
|
||||||
|
# requests
|
||||||
|
cffi==1.15.1
|
||||||
|
# via cryptography
|
||||||
|
charset-normalizer==3.2.0
|
||||||
# via requests
|
# via requests
|
||||||
charset-normalizer==2.1.0
|
click==8.1.6
|
||||||
# via requests
|
# via
|
||||||
dill==0.3.5.1
|
# black
|
||||||
|
# uvicorn
|
||||||
|
cryptography==41.0.2
|
||||||
|
# via secretstorage
|
||||||
|
dill==0.3.7
|
||||||
# via pylint
|
# via pylint
|
||||||
fastapi==0.79.0
|
dnspython==2.4.1
|
||||||
|
# via email-validator
|
||||||
|
docutils==0.20.1
|
||||||
|
# via readme-renderer
|
||||||
|
email-validator==2.0.0.post2
|
||||||
|
# via fastapi
|
||||||
|
exceptiongroup==1.1.2
|
||||||
|
# via anyio
|
||||||
|
fastapi[all]==0.100.1
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
feedgen==0.9.0
|
feedgen==0.9.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
greenlet==1.1.2
|
greenlet==2.0.2
|
||||||
# via sqlalchemy
|
# via sqlalchemy
|
||||||
idna==3.3
|
h11==0.14.0
|
||||||
|
# via
|
||||||
|
# httpcore
|
||||||
|
# uvicorn
|
||||||
|
httpcore==0.17.3
|
||||||
|
# via httpx
|
||||||
|
httptools==0.6.0
|
||||||
|
# via uvicorn
|
||||||
|
httpx==0.24.1
|
||||||
|
# via fastapi
|
||||||
|
idna==3.4
|
||||||
# via
|
# via
|
||||||
# anyio
|
# anyio
|
||||||
|
# email-validator
|
||||||
|
# httpx
|
||||||
# requests
|
# requests
|
||||||
isort==5.10.1
|
importlib-metadata==6.8.0
|
||||||
|
# via
|
||||||
|
# keyring
|
||||||
|
# twine
|
||||||
|
isort==5.12.0
|
||||||
# via pylint
|
# via pylint
|
||||||
lazy-object-proxy==1.7.1
|
itsdangerous==2.1.2
|
||||||
|
# via fastapi
|
||||||
|
jaraco-classes==3.3.0
|
||||||
|
# via keyring
|
||||||
|
jeepney==0.8.0
|
||||||
|
# via
|
||||||
|
# keyring
|
||||||
|
# secretstorage
|
||||||
|
jinja2==3.1.2
|
||||||
|
# via fastapi
|
||||||
|
keyring==24.2.0
|
||||||
|
# via twine
|
||||||
|
lazy-object-proxy==1.9.0
|
||||||
# via astroid
|
# via astroid
|
||||||
lxml==4.9.1
|
lxml==4.9.3
|
||||||
# via feedgen
|
# via feedgen
|
||||||
|
markdown-it-py==3.0.0
|
||||||
|
# via rich
|
||||||
|
markupsafe==2.1.3
|
||||||
|
# via jinja2
|
||||||
mccabe==0.7.0
|
mccabe==0.7.0
|
||||||
# via pylint
|
# via pylint
|
||||||
platformdirs==2.5.2
|
mdurl==0.1.2
|
||||||
# via pylint
|
# via markdown-it-py
|
||||||
pydantic==1.9.1
|
more-itertools==10.0.0
|
||||||
|
# via jaraco-classes
|
||||||
|
mypy-extensions==1.0.0
|
||||||
|
# via black
|
||||||
|
orjson==3.9.2
|
||||||
# via fastapi
|
# via fastapi
|
||||||
pylint==2.14.5
|
packaging==23.1
|
||||||
|
# via
|
||||||
|
# black
|
||||||
|
# build
|
||||||
|
pathspec==0.11.2
|
||||||
|
# via black
|
||||||
|
pkginfo==1.9.6
|
||||||
|
# via twine
|
||||||
|
platformdirs==3.10.0
|
||||||
|
# via
|
||||||
|
# black
|
||||||
|
# pylint
|
||||||
|
pycparser==2.21
|
||||||
|
# via cffi
|
||||||
|
pydantic==2.1.1
|
||||||
|
# via
|
||||||
|
# fastapi
|
||||||
|
# pydantic-extra-types
|
||||||
|
# pydantic-settings
|
||||||
|
pydantic-core==2.4.0
|
||||||
|
# via pydantic
|
||||||
|
pydantic-extra-types==2.0.0
|
||||||
|
# via fastapi
|
||||||
|
pydantic-settings==2.0.2
|
||||||
|
# via fastapi
|
||||||
|
pygments==2.15.1
|
||||||
|
# via
|
||||||
|
# readme-renderer
|
||||||
|
# rich
|
||||||
|
pylint==2.17.5
|
||||||
# via -r requirements-dev.in
|
# via -r requirements-dev.in
|
||||||
|
pyproject-hooks==1.0.0
|
||||||
|
# via build
|
||||||
python-dateutil==2.8.2
|
python-dateutil==2.8.2
|
||||||
# via feedgen
|
# via feedgen
|
||||||
requests==2.28.1
|
python-dotenv==1.0.0
|
||||||
# via -r requirements.in
|
# via
|
||||||
|
# pydantic-settings
|
||||||
|
# uvicorn
|
||||||
|
python-multipart==0.0.6
|
||||||
|
# via fastapi
|
||||||
|
pyyaml==6.0.1
|
||||||
|
# via
|
||||||
|
# fastapi
|
||||||
|
# uvicorn
|
||||||
|
readme-renderer==40.0
|
||||||
|
# via twine
|
||||||
|
requests==2.31.0
|
||||||
|
# via
|
||||||
|
# -r requirements.in
|
||||||
|
# requests-toolbelt
|
||||||
|
# twine
|
||||||
|
requests-toolbelt==1.0.0
|
||||||
|
# via twine
|
||||||
|
rfc3986==2.0.0
|
||||||
|
# via twine
|
||||||
|
rich==13.5.0
|
||||||
|
# via twine
|
||||||
|
ruff==0.0.280
|
||||||
|
# via -r requirements-dev.in
|
||||||
|
secretstorage==3.3.3
|
||||||
|
# via keyring
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
# via python-dateutil
|
# via
|
||||||
sniffio==1.2.0
|
# bleach
|
||||||
# via anyio
|
# python-dateutil
|
||||||
soupsieve==2.3.2.post1
|
sniffio==1.3.0
|
||||||
|
# via
|
||||||
|
# anyio
|
||||||
|
# httpcore
|
||||||
|
# httpx
|
||||||
|
soupsieve==2.4.1
|
||||||
# via beautifulsoup4
|
# via beautifulsoup4
|
||||||
sqlalchemy==1.4.39
|
sqlalchemy==2.0.19
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
starlette==0.19.1
|
starlette==0.27.0
|
||||||
# via fastapi
|
# via fastapi
|
||||||
tomli==2.0.1
|
tomli==2.0.1
|
||||||
|
# via
|
||||||
|
# black
|
||||||
|
# build
|
||||||
|
# pylint
|
||||||
|
# pyproject-hooks
|
||||||
|
tomlkit==0.12.1
|
||||||
# via pylint
|
# via pylint
|
||||||
tomlkit==0.11.1
|
twine==4.0.2
|
||||||
# via pylint
|
# via -r requirements-dev.in
|
||||||
typing-extensions==4.3.0
|
typing-extensions==4.7.1
|
||||||
# via pydantic
|
# via
|
||||||
urllib3==1.26.11
|
# astroid
|
||||||
# via requests
|
# fastapi
|
||||||
wrapt==1.14.1
|
# pydantic
|
||||||
|
# pydantic-core
|
||||||
|
# sqlalchemy
|
||||||
|
# uvicorn
|
||||||
|
ujson==5.8.0
|
||||||
|
# via fastapi
|
||||||
|
urllib3==2.0.4
|
||||||
|
# via
|
||||||
|
# requests
|
||||||
|
# twine
|
||||||
|
uvicorn[standard]==0.23.1
|
||||||
|
# via fastapi
|
||||||
|
uvloop==0.17.0
|
||||||
|
# via uvicorn
|
||||||
|
watchfiles==0.19.0
|
||||||
|
# via uvicorn
|
||||||
|
webencodings==0.5.1
|
||||||
|
# via bleach
|
||||||
|
websockets==11.0.3
|
||||||
|
# via uvicorn
|
||||||
|
wrapt==1.15.0
|
||||||
# via astroid
|
# via astroid
|
||||||
|
zipp==3.16.2
|
||||||
# The following packages are considered to be unsafe in a requirements file:
|
# via importlib-metadata
|
||||||
# setuptools
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Core application
|
# Core application
|
||||||
fastapi
|
fastapi[all]
|
||||||
sqlalchemy
|
sqlalchemy
|
||||||
|
|
||||||
# Fetch title etc from links
|
# Fetch title etc from links
|
||||||
|
|||||||
123
requirements.txt
123
requirements.txt
@@ -1,44 +1,121 @@
|
|||||||
#
|
#
|
||||||
# This file is autogenerated by pip-compile with python 3.10
|
# This file is autogenerated by pip-compile with Python 3.10
|
||||||
# To update, run:
|
# by the following command:
|
||||||
#
|
#
|
||||||
# pip-compile requirements.in
|
# pip-compile requirements.in
|
||||||
#
|
#
|
||||||
beautifulsoup4==4.11.1
|
annotated-types==0.5.0
|
||||||
|
# via pydantic
|
||||||
|
anyio==3.7.1
|
||||||
|
# via
|
||||||
|
# httpcore
|
||||||
|
# starlette
|
||||||
|
# watchfiles
|
||||||
|
beautifulsoup4==4.12.2
|
||||||
# via bs4
|
# via bs4
|
||||||
bs4==0.0.1
|
bs4==0.0.1
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
certifi==2022.6.15
|
certifi==2023.7.22
|
||||||
|
# via
|
||||||
|
# httpcore
|
||||||
|
# httpx
|
||||||
|
# requests
|
||||||
|
charset-normalizer==3.2.0
|
||||||
# via requests
|
# via requests
|
||||||
charset-normalizer==2.1.0
|
click==8.1.6
|
||||||
# via requests
|
# via uvicorn
|
||||||
click==8.1.3
|
dnspython==2.4.1
|
||||||
# via flask
|
# via email-validator
|
||||||
|
email-validator==2.0.0.post2
|
||||||
|
# via fastapi
|
||||||
|
exceptiongroup==1.1.2
|
||||||
|
# via anyio
|
||||||
|
fastapi[all]==0.100.1
|
||||||
|
# via -r requirements.in
|
||||||
feedgen==0.9.0
|
feedgen==0.9.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
flask==2.1.3
|
greenlet==2.0.2
|
||||||
# via -r requirements.in
|
# via sqlalchemy
|
||||||
idna==3.3
|
h11==0.14.0
|
||||||
# via requests
|
# via
|
||||||
|
# httpcore
|
||||||
|
# uvicorn
|
||||||
|
httpcore==0.17.3
|
||||||
|
# via httpx
|
||||||
|
httptools==0.6.0
|
||||||
|
# via uvicorn
|
||||||
|
httpx==0.24.1
|
||||||
|
# via fastapi
|
||||||
|
idna==3.4
|
||||||
|
# via
|
||||||
|
# anyio
|
||||||
|
# email-validator
|
||||||
|
# httpx
|
||||||
|
# requests
|
||||||
itsdangerous==2.1.2
|
itsdangerous==2.1.2
|
||||||
# via flask
|
# via fastapi
|
||||||
jinja2==3.1.2
|
jinja2==3.1.2
|
||||||
# via flask
|
# via fastapi
|
||||||
lxml==4.9.1
|
lxml==4.9.3
|
||||||
# via feedgen
|
# via feedgen
|
||||||
markupsafe==2.1.1
|
markupsafe==2.1.3
|
||||||
# via jinja2
|
# via jinja2
|
||||||
peewee==3.15.1
|
orjson==3.9.2
|
||||||
# via -r requirements.in
|
# via fastapi
|
||||||
|
pydantic==2.1.1
|
||||||
|
# via
|
||||||
|
# fastapi
|
||||||
|
# pydantic-extra-types
|
||||||
|
# pydantic-settings
|
||||||
|
pydantic-core==2.4.0
|
||||||
|
# via pydantic
|
||||||
|
pydantic-extra-types==2.0.0
|
||||||
|
# via fastapi
|
||||||
|
pydantic-settings==2.0.2
|
||||||
|
# via fastapi
|
||||||
python-dateutil==2.8.2
|
python-dateutil==2.8.2
|
||||||
# via feedgen
|
# via feedgen
|
||||||
requests==2.28.1
|
python-dotenv==1.0.0
|
||||||
|
# via
|
||||||
|
# pydantic-settings
|
||||||
|
# uvicorn
|
||||||
|
python-multipart==0.0.6
|
||||||
|
# via fastapi
|
||||||
|
pyyaml==6.0.1
|
||||||
|
# via
|
||||||
|
# fastapi
|
||||||
|
# uvicorn
|
||||||
|
requests==2.31.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
# via python-dateutil
|
# via python-dateutil
|
||||||
soupsieve==2.3.2.post1
|
sniffio==1.3.0
|
||||||
|
# via
|
||||||
|
# anyio
|
||||||
|
# httpcore
|
||||||
|
# httpx
|
||||||
|
soupsieve==2.4.1
|
||||||
# via beautifulsoup4
|
# via beautifulsoup4
|
||||||
urllib3==1.26.10
|
sqlalchemy==2.0.19
|
||||||
|
# via -r requirements.in
|
||||||
|
starlette==0.27.0
|
||||||
|
# via fastapi
|
||||||
|
typing-extensions==4.7.1
|
||||||
|
# via
|
||||||
|
# fastapi
|
||||||
|
# pydantic
|
||||||
|
# pydantic-core
|
||||||
|
# sqlalchemy
|
||||||
|
# uvicorn
|
||||||
|
ujson==5.8.0
|
||||||
|
# via fastapi
|
||||||
|
urllib3==2.0.4
|
||||||
# via requests
|
# via requests
|
||||||
werkzeug==2.1.2
|
uvicorn[standard]==0.23.1
|
||||||
# via flask
|
# via fastapi
|
||||||
|
uvloop==0.17.0
|
||||||
|
# via uvicorn
|
||||||
|
watchfiles==0.19.0
|
||||||
|
# via uvicorn
|
||||||
|
websockets==11.0.3
|
||||||
|
# via uvicorn
|
||||||
|
|||||||
@@ -1,28 +1,25 @@
|
|||||||
from __future__ import print_function
|
|
||||||
|
|
||||||
import binascii
|
import binascii
|
||||||
import datetime
|
import datetime
|
||||||
import gzip
|
import gzip
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
from urllib.parse import urljoin, urlparse, urlunparse
|
||||||
|
|
||||||
import bs4
|
import bs4
|
||||||
import requests
|
import requests
|
||||||
from dateutil import tz
|
from dateutil import tz
|
||||||
from feedgen.feed import FeedGenerator
|
|
||||||
#from flask import (Flask, abort, jsonify, make_response, redirect,
|
#from flask import (Flask, abort, jsonify, make_response, redirect,
|
||||||
# render_template, request, url_for)
|
# render_template, request, url_for)
|
||||||
from typing import List
|
from fastapi import Depends, FastAPI, HTTPException, Request, Response
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
import databases
|
from feedgen.feed import FeedGenerator
|
||||||
import sqlalchemy
|
from pydantic import BaseModel, DirectoryPath, FilePath, validator
|
||||||
from fastapi import FastAPI
|
from pydantic_settings import BaseSettings
|
||||||
from pydantic import BaseModel
|
from sqlalchemy import VARCHAR, Boolean, Column, DateTime, ForeignKey, Integer, String, Text, create_engine
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from urlparse import urljoin, urlparse, urlunparse
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
|
||||||
DIGIMARKS_USER_AGENT = 'digimarks/2.0.0-dev'
|
DIGIMARKS_USER_AGENT = 'digimarks/2.0.0-dev'
|
||||||
|
|
||||||
@@ -150,44 +147,44 @@ themes = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
|
||||||
import settings
|
|
||||||
except ImportError:
|
|
||||||
print('Copy settings_example.py to settings.py and set the configuration to your own preferences')
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# app configuration
|
class Settings(BaseSettings):
|
||||||
APP_ROOT = os.path.dirname(os.path.realpath(__file__))
|
"""Configuration needed for digimarks to find its database, favicons, API integrations"""
|
||||||
MEDIA_ROOT = os.path.join(APP_ROOT, 'static')
|
|
||||||
MEDIA_URL = '/static/'
|
|
||||||
DATABASE = {
|
|
||||||
'name': os.path.join(APP_ROOT, 'bookmarks.db'),
|
|
||||||
'engine': 'peewee.SqliteDatabase',
|
|
||||||
}
|
|
||||||
DATABASE_URL = os.path.join(APP_ROOT, 'bookmarks.db')
|
|
||||||
#PHANTOM = '/usr/local/bin/phantomjs'
|
|
||||||
#SCRIPT = os.path.join(APP_ROOT, 'screenshot.js')
|
|
||||||
|
|
||||||
# create our flask app and a database wrapper
|
database_file: FilePath = './bookmarks.db'
|
||||||
#app = Flask(__name__)
|
media_dir: DirectoryPath
|
||||||
#app.config.from_object(__name__)
|
media_url: str = '/static/'
|
||||||
#database = SqliteDatabase(os.path.join(APP_ROOT, 'bookmarks.db'))
|
|
||||||
|
|
||||||
database = databases.Database(DATABASE_URL)
|
mashape_api_key: str
|
||||||
|
|
||||||
metadata = sqlalchemy.MetaData()
|
debug: bool = False
|
||||||
|
|
||||||
# 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'
|
settings = Settings()
|
||||||
try:
|
|
||||||
app.config['APPLICATION_ROOT'] = settings.APPLICATION_ROOT
|
engine = create_engine(
|
||||||
except AttributeError:
|
f'sqlite:///{settings.database_file}', connect_args={'check_same_thread': False}
|
||||||
pass
|
)
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
logger = logging.getLogger('digimarks')
|
||||||
|
if settings.debug:
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
# CORS configuration
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"], # Allow requests from everywhere
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Cache the tags
|
|
||||||
all_tags = {}
|
all_tags = {}
|
||||||
usersettings = {}
|
usersettings = {}
|
||||||
|
|
||||||
@@ -202,9 +199,11 @@ def ifilterfalse(predicate, iterable):
|
|||||||
|
|
||||||
|
|
||||||
def unique_everseen(iterable, key=None):
|
def unique_everseen(iterable, key=None):
|
||||||
"List unique elements, preserving order. Remember all elements ever seen."
|
"""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
|
unique_everseen('AAAABBBCCDAABBB') --> A B C D
|
||||||
|
unique_everseen('ABBCcAD', str.lower) --> A B C D
|
||||||
|
"""
|
||||||
seen = set()
|
seen = set()
|
||||||
seen_add = seen.add
|
seen_add = seen.add
|
||||||
if key is None:
|
if key is None:
|
||||||
@@ -218,6 +217,7 @@ def unique_everseen(iterable, key=None):
|
|||||||
seen_add(k)
|
seen_add(k)
|
||||||
yield element
|
yield element
|
||||||
|
|
||||||
|
|
||||||
def clean_tags(tags_list):
|
def clean_tags(tags_list):
|
||||||
tags_res = [x.strip() for x in tags_list]
|
tags_res = [x.strip() for x in tags_list]
|
||||||
tags_res = list(unique_everseen(tags_res))
|
tags_res = list(unique_everseen(tags_res))
|
||||||
@@ -244,17 +244,13 @@ def file_type(filename):
|
|||||||
return "no match"
|
return "no match"
|
||||||
|
|
||||||
|
|
||||||
class BaseModel(Model):
|
class User(Base):
|
||||||
class Meta:
|
|
||||||
database = database
|
|
||||||
|
|
||||||
|
|
||||||
class User(BaseModel):
|
|
||||||
""" User account """
|
""" User account """
|
||||||
username = CharField()
|
username = Column(VARCHAR(255))
|
||||||
key = CharField()
|
key = Column(VARCHAR(255))
|
||||||
theme = CharField(default=DEFAULT_THEME)
|
# theme = CharField(default=DEFAULT_THEME)
|
||||||
created_date = DateTimeField(default=datetime.datetime.now)
|
theme = Column(VARCHAR(20), default=DEFAULT_THEME)
|
||||||
|
created_date = Column(DateTime, default=datetime.datetime.now)
|
||||||
|
|
||||||
def generate_key(self):
|
def generate_key(self):
|
||||||
""" Generate userkey """
|
""" Generate userkey """
|
||||||
@@ -262,21 +258,21 @@ class User(BaseModel):
|
|||||||
return self.key
|
return self.key
|
||||||
|
|
||||||
|
|
||||||
class Bookmark(BaseModel):
|
class Bookmark(Base):
|
||||||
""" Bookmark instance, connected to User """
|
""" Bookmark instance, connected to User """
|
||||||
# Foreign key to User
|
# Foreign key to User
|
||||||
userkey = CharField()
|
userkey = Column(VARCHAR(255))
|
||||||
|
|
||||||
title = CharField(default='')
|
title = Column(VARCHAR(255), default='')
|
||||||
url = CharField()
|
url = Column(VARCHAR(255))
|
||||||
note = TextField(default='')
|
note = Column(Text, default='')
|
||||||
#image = CharField(default='')
|
#image = CharField(default='')
|
||||||
url_hash = CharField(default='')
|
url_hash = Column(VARCHAR(255) , default='')
|
||||||
tags = CharField(default='')
|
tags = Column(VARCHAR(255), default='')
|
||||||
starred = BooleanField(default=False)
|
starred = Column(Boolean, default=False)
|
||||||
|
|
||||||
# Website (domain) favicon
|
# Website (domain) favicon
|
||||||
favicon = CharField(null=True)
|
favicon = Column(VARCHAR(255), null=True)
|
||||||
|
|
||||||
# Status code: 200 is OK, 404 is not found, for example (showing an error)
|
# Status code: 200 is OK, 404 is not found, for example (showing an error)
|
||||||
HTTP_CONNECTIONERROR = 0
|
HTTP_CONNECTIONERROR = 0
|
||||||
@@ -285,17 +281,17 @@ class Bookmark(BaseModel):
|
|||||||
HTTP_MOVEDTEMPORARILY = 304
|
HTTP_MOVEDTEMPORARILY = 304
|
||||||
HTTP_NOTFOUND = 404
|
HTTP_NOTFOUND = 404
|
||||||
|
|
||||||
http_status = IntegerField(default=200)
|
http_status = Column(Integer, default=200)
|
||||||
redirect_uri = None
|
redirect_uri = None
|
||||||
|
|
||||||
created_date = DateTimeField(default=datetime.datetime.now)
|
created_date = Column(DateTime, default=datetime.datetime.now)
|
||||||
modified_date = DateTimeField(null=True)
|
modified_date = Column(DateTime, null=True)
|
||||||
deleted_date = DateTimeField(null=True)
|
deleted_date = Column(DateTime, null=True)
|
||||||
|
|
||||||
# Bookmark status; deleting doesn't remove from DB
|
# Bookmark status; deleting doesn't remove from DB
|
||||||
VISIBLE = 0
|
VISIBLE = 0
|
||||||
DELETED = 1
|
DELETED = 1
|
||||||
status = IntegerField(default=VISIBLE)
|
status = Column(Integer, default=VISIBLE)
|
||||||
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -345,7 +341,7 @@ class Bookmark(BaseModel):
|
|||||||
stream=True,
|
stream=True,
|
||||||
headers={'User-Agent': DIGIMARKS_USER_AGENT}
|
headers={'User-Agent': DIGIMARKS_USER_AGENT}
|
||||||
)
|
)
|
||||||
filename = os.path.join(MEDIA_ROOT, 'favicons/' + domain + fileextension)
|
filename = os.path.join(settings.media_dir, 'favicons/', domain + fileextension)
|
||||||
with open(filename, 'wb') as out_file:
|
with open(filename, 'wb') as out_file:
|
||||||
shutil.copyfileobj(response.raw, out_file)
|
shutil.copyfileobj(response.raw, out_file)
|
||||||
del response
|
del response
|
||||||
@@ -387,7 +383,7 @@ class Bookmark(BaseModel):
|
|||||||
fileextension = '.jpg'
|
fileextension = '.jpg'
|
||||||
if response.headers['content-type'] == 'image/x-icon':
|
if response.headers['content-type'] == 'image/x-icon':
|
||||||
fileextension = '.ico'
|
fileextension = '.ico'
|
||||||
filename = os.path.join(MEDIA_ROOT, 'favicons/' + domain + fileextension)
|
filename = os.path.join(settings.media_dir, 'favicons/', domain + fileextension)
|
||||||
with open(filename, 'wb') as out_file:
|
with open(filename, 'wb') as out_file:
|
||||||
shutil.copyfileobj(response.raw, out_file)
|
shutil.copyfileobj(response.raw, out_file)
|
||||||
del response
|
del response
|
||||||
@@ -406,11 +402,11 @@ class Bookmark(BaseModel):
|
|||||||
""" Fetch favicon for the domain """
|
""" Fetch favicon for the domain """
|
||||||
u = urlparse(self.url)
|
u = urlparse(self.url)
|
||||||
domain = u.netloc
|
domain = u.netloc
|
||||||
if os.path.isfile(os.path.join(MEDIA_ROOT, 'favicons/' + domain + '.png')):
|
if os.path.isfile(os.path.join(settings.media_dir, 'favicons/', domain + '.png')):
|
||||||
# If file exists, don't re-download it
|
# If file exists, don't re-download it
|
||||||
self.favicon = domain + '.png'
|
self.favicon = domain + '.png'
|
||||||
return
|
return
|
||||||
if os.path.isfile(os.path.join(MEDIA_ROOT, 'favicons/' + domain + '.ico')):
|
if os.path.isfile(os.path.join(settings.media_dir, 'favicons/', domain + '.ico')):
|
||||||
# If file exists, don't re-download it
|
# If file exists, don't re-download it
|
||||||
self.favicon = domain + '.ico'
|
self.favicon = domain + '.ico'
|
||||||
return
|
return
|
||||||
@@ -466,10 +462,10 @@ class Bookmark(BaseModel):
|
|||||||
|
|
||||||
class PublicTag(BaseModel):
|
class PublicTag(BaseModel):
|
||||||
""" Publicly shared tag """
|
""" Publicly shared tag """
|
||||||
tagkey = CharField()
|
tagkey = Column(VARCHAR(255))
|
||||||
userkey = CharField()
|
userkey = Column(VARCHAR(255))
|
||||||
tag = CharField()
|
tag = Column(VARCHAR(255))
|
||||||
created_date = DateTimeField(default=datetime.datetime.now)
|
created_date = Column(DateTime, default=datetime.datetime.now)
|
||||||
|
|
||||||
def generate_key(self):
|
def generate_key(self):
|
||||||
""" Generate hash-based key for publicly shared tag """
|
""" Generate hash-based key for publicly shared tag """
|
||||||
@@ -958,8 +954,8 @@ def publictag_json(tagkey):
|
|||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/pub/<tagkey>/feed')
|
@app.get('/pub/<tagkey>/feed')
|
||||||
def publictag_feed(tagkey):
|
async def publictag_feed(request: Request, tagkey: str):
|
||||||
""" rss/atom representation of the Read-only overview of the bookmarks in the userkey/tag of this PublicTag """
|
""" rss/atom representation of the Read-only overview of the bookmarks in the userkey/tag of this PublicTag """
|
||||||
try:
|
try:
|
||||||
this_tag = PublicTag.get(PublicTag.tagkey == tagkey)
|
this_tag = PublicTag.get(PublicTag.tagkey == tagkey)
|
||||||
@@ -973,7 +969,7 @@ def publictag_feed(tagkey):
|
|||||||
feed.title(this_tag.tag)
|
feed.title(this_tag.tag)
|
||||||
feed.id(request.url)
|
feed.id(request.url)
|
||||||
feed.link(href=request.url, rel='self')
|
feed.link(href=request.url, rel='self')
|
||||||
feed.link(href=make_external(url_for('publictag_page', tagkey=tagkey)))
|
feed.link(href=make_external(app.url_path_for('publictag_page', tagkey=tagkey)))
|
||||||
|
|
||||||
for bookmark in bookmarks:
|
for bookmark in bookmarks:
|
||||||
entry = feed.add_entry()
|
entry = feed.add_entry()
|
||||||
@@ -993,20 +989,21 @@ def publictag_feed(tagkey):
|
|||||||
entry.published(bookmark.created_date.replace(tzinfo=tz.tzlocal()))
|
entry.published(bookmark.created_date.replace(tzinfo=tz.tzlocal()))
|
||||||
entry.updated(updated_date.replace(tzinfo=tz.tzlocal()))
|
entry.updated(updated_date.replace(tzinfo=tz.tzlocal()))
|
||||||
|
|
||||||
response = make_response(feed.atom_str(pretty=True))
|
response = Response(data=feed.atom_str(pretty=True), media_type='application/xml')
|
||||||
|
|
||||||
response.headers.set('Content-Type', 'application/atom+xml')
|
response.headers.set('Content-Type', 'application/atom+xml')
|
||||||
return response
|
return response
|
||||||
except PublicTag.DoesNotExist:
|
except PublicTag.DoesNotExist:
|
||||||
abort(404)
|
raise HTTPException(status_code=404, detail='Tag not found')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/<userkey>/<tag>/makepublic', methods=['GET', 'POST'])
|
@app.get('/<userkey>/<tag>/makepublic')
|
||||||
def addpublictag(userkey, tag):
|
@app.post('/<userkey>/<tag>/makepublic')
|
||||||
#user = get_object_or_404(User.get(User.key == userkey))
|
async def addpublictag(userkey: str, tag: str):
|
||||||
try:
|
try:
|
||||||
User.get(User.key == userkey)
|
User.get(User.key == userkey)
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
abort(404)
|
raise HTTPException(status_code=404, detail='User not found')
|
||||||
try:
|
try:
|
||||||
publictag = PublicTag.get(PublicTag.userkey == userkey, PublicTag.tag == tag)
|
publictag = PublicTag.get(PublicTag.userkey == userkey, PublicTag.tag == tag)
|
||||||
except PublicTag.DoesNotExist:
|
except PublicTag.DoesNotExist:
|
||||||
@@ -1019,10 +1016,14 @@ def addpublictag(userkey, tag):
|
|||||||
newpublictag.save()
|
newpublictag.save()
|
||||||
|
|
||||||
message = 'Public link to this tag created'
|
message = 'Public link to this tag created'
|
||||||
return redirect(url_for('tag_page', userkey=userkey, tag=tag, message=message))
|
success = True
|
||||||
|
# return RedirectResponse(url=url_path_for('tag_page', userkey=userkey, tag=tag, message=message))
|
||||||
message = 'Public link already existed'
|
else:
|
||||||
return redirect(url_for('tag_page', userkey=userkey, tag=tag, message=message))
|
message = 'Public link already existed'
|
||||||
|
success = False
|
||||||
|
# return redirect(url_for('tag_page', userkey=userkey, tag=tag, message=message))
|
||||||
|
url = app.url_path_for('tag_page', userkey=userkey, tag=tag, message=message)
|
||||||
|
return {'success': success, 'message': message, 'url': url}
|
||||||
|
|
||||||
|
|
||||||
@app.route('/<userkey>/<tag>/removepublic/<tagkey>', methods=['GET', 'POST'])
|
@app.route('/<userkey>/<tag>/removepublic/<tagkey>', methods=['GET', 'POST'])
|
||||||
|
|||||||
Reference in New Issue
Block a user