1
0
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:
2023-07-30 21:19:51 +02:00
parent 96e7ef16d4
commit 7e397f9d2b
6 changed files with 407 additions and 157 deletions

View File

@@ -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

View File

@@ -1,3 +1,8 @@
-r requirements.in -r requirements.in
black
pylint pylint
ruff
build
twine

View File

@@ -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

View File

@@ -1,5 +1,5 @@
# Core application # Core application
fastapi fastapi[all]
sqlalchemy sqlalchemy
# Fetch title etc from links # Fetch title etc from links

View File

@@ -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

View File

@@ -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'])