From 16271aa54a62632427b2c174f9441e8b417b6bc3 Mon Sep 17 00:00:00 2001 From: dave Date: Mon, 17 Jun 2019 22:43:57 -0700 Subject: [PATCH] api beginnings --- photoapp/api.py | 41 +++++++++++++++++++++++++++++++++ photoapp/daemon.py | 55 +++++++++------------------------------------ photoapp/dbutils.py | 55 +++++++++++++++++++++++++++++++++++++++++++++ photoapp/ingest.py | 2 +- photoapp/types.py | 5 +---- photoapp/utils.py | 47 ++++++++++++++++++++++++++++++++++++++ requirements.txt | 6 ++--- 7 files changed, 158 insertions(+), 53 deletions(-) create mode 100644 photoapp/api.py create mode 100644 photoapp/dbutils.py create mode 100644 photoapp/utils.py diff --git a/photoapp/api.py b/photoapp/api.py new file mode 100644 index 0000000..f084390 --- /dev/null +++ b/photoapp/api.py @@ -0,0 +1,41 @@ +import os +import cherrypy +import logging +from datetime import datetime, timedelta +from photoapp.library import PhotoLibrary +from photoapp.types import Photo, PhotoSet, Tag, TagItem, PhotoStatus, User +from jinja2 import Environment, FileSystemLoader, select_autoescape +from sqlalchemy import desc +from sqlalchemy import func, and_, or_ +from photoapp.common import pwhash +import math +from urllib.parse import urlparse +from photoapp.utils import mime2ext, auth, require_auth, photo_auth_filter, slugify +from photoapp.dbutils import db + + +class PhotosApi(object): + def __init__(self): + self.v1 = PhotosApiV1() + + +class PhotosApiV1(object): + def __init__(self): + # self.tpl.filters.update(mime2ext=mime2ext, + # basename=os.path.basename, + # ceil=math.ceil, + # statusstr=lambda x: str(x).split(".")[-1]) + pass + + @cherrypy.expose + def index(self): + yield f"hello, this is the api. my database is: {db()}\n" + + @cherrypy.expose + def upload(self, files, meta): + pass + + @cherrypy.expose + @cherrypy.tools.json_out() + def findbysha(self, sha): + pass diff --git a/photoapp/daemon.py b/photoapp/daemon.py index 715e788..a1d4453 100644 --- a/photoapp/daemon.py +++ b/photoapp/daemon.py @@ -8,58 +8,16 @@ from jinja2 import Environment, FileSystemLoader, select_autoescape from sqlalchemy import desc from sqlalchemy import func, and_, or_ from photoapp.common import pwhash +from photoapp.api import PhotosApi +from photoapp.dbutils import SAEnginePlugin, SATool import math from urllib.parse import urlparse +from photoapp.utils import mime2ext, auth, require_auth, photo_auth_filter, slugify APPROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../")) -def auth(): - """ - Return the currently authorized username (per request) or None - """ - return cherrypy.session.get('authed', None) - - -def mime2ext(mime): - """ - Given a mime type return the canonical file extension - """ - return {"image/png": "png", - "image/jpeg": "jpg", - "image/gif": "gif", - "application/octet-stream-xmp": "xmp", - "image/x-canon-cr2": "cr2", - "video/mp4": "mp4", - "video/quicktime": "mov"}[mime] - - -def require_auth(func): - """ - Decorator: raise 403 unless session is authed - """ - def wrapped(*args, **kwargs): - if not auth(): - raise cherrypy.HTTPError(403) - return func(*args, **kwargs) - return wrapped - - -def photo_auth_filter(query): - """ - Sqlalchemy helper: filter the given PhotoSet query to items that match the authorized user's PhotoStatus access - level. Currently, authed users can access ALL photos, and unauthed users can access only PhotoStatus.public - status items. - """ - return query.filter(PhotoSet.status == PhotoStatus.public) if not auth() else query - - -def slugify(words): - return ''.join(letter for letter in '-'.join(words.lower().split()) - if ('a' <= letter <= 'z') or ('0' <= letter <= '9') or letter == '-') - - class PhotosWeb(object): def __init__(self, library, template_dir): self.library = library @@ -511,6 +469,13 @@ def main(): 'tools.auth_basic.realm': 'photolib', 'tools.auth_basic.checkpassword': validate_password}}) + cherrypy.tools.db = SATool() + SAEnginePlugin(cherrypy.engine, library.engine).subscribe() + api = PhotosApi() + cherrypy.tree.mount(api, '/api', {'/': {'tools.trailing_slash.on': False, + 'tools.auth_basic.checkpassword': validate_password, + 'tools.db.on': True}}) + cherrypy.config.update({ 'tools.sessions.on': True, 'tools.sessions.locking': 'explicit', diff --git a/photoapp/dbutils.py b/photoapp/dbutils.py new file mode 100644 index 0000000..16bcb02 --- /dev/null +++ b/photoapp/dbutils.py @@ -0,0 +1,55 @@ +import sqlalchemy +import cherrypy +from cherrypy.process import plugins +from sqlalchemy.ext.declarative import declarative_base + + +Base = declarative_base() + + +def db(): + return cherrypy.request.db + + +class SAEnginePlugin(plugins.SimplePlugin): + def __init__(self, bus, dbcon): + plugins.SimplePlugin.__init__(self, bus) + self.sa_engine = dbcon + self.bus.subscribe("bind", self.bind) + + def start(self): + Base.metadata.create_all(self.sa_engine) + + def bind(self, session): + session.configure(bind=self.sa_engine) + + +class SATool(cherrypy.Tool): + def __init__(self): + cherrypy.Tool.__init__(self, 'before_request_body', + self.bind_session, + priority=100) + + self.session = sqlalchemy.orm.scoped_session( + sqlalchemy.orm.sessionmaker(autoflush=True, autocommit=False)) + + def _setup(self): + cherrypy.Tool._setup(self) + cherrypy.request.hooks.attach('on_end_resource', self.commit_transaction, priority=80) + + def bind_session(self): + cherrypy.engine.publish('bind', self.session) + cherrypy.request.db = self.session + con = cherrypy.request.db.connection().connection.connection + if hasattr(con, "ping"): # not available on sqlite + con.ping() + + def commit_transaction(self): + cherrypy.request.db = None + try: + self.session.commit() + except Exception: + self.session.rollback() + raise + finally: + self.session.remove() \ No newline at end of file diff --git a/photoapp/ingest.py b/photoapp/ingest.py index df54ebe..4a67c16 100644 --- a/photoapp/ingest.py +++ b/photoapp/ingest.py @@ -122,7 +122,7 @@ def main(): parser.add_argument("files", nargs="+") args = parser.parse_args() - library = PhotoLibrary("photos.db", "./library/") + library = PhotoLibrary("photos.db", "./library", "./cache") batch_ingest(library, args.files) diff --git a/photoapp/types.py b/photoapp/types.py index b8e0fa9..a8df0a6 100644 --- a/photoapp/types.py +++ b/photoapp/types.py @@ -1,15 +1,12 @@ from sqlalchemy import Column, Integer, String, DateTime, Unicode, DECIMAL, ForeignKey, Boolean, Enum from sqlalchemy.orm import relationship from sqlalchemy.schema import UniqueConstraint -from sqlalchemy.ext.declarative import declarative_base +from photoapp.dbutils import Base from datetime import datetime import uuid import enum -Base = declarative_base() - - class PhotoStatus(enum.Enum): private = 0 public = 1 diff --git a/photoapp/utils.py b/photoapp/utils.py new file mode 100644 index 0000000..9af93f2 --- /dev/null +++ b/photoapp/utils.py @@ -0,0 +1,47 @@ +import cherrypy +from photoapp.types import PhotoSet, PhotoStatus + + +def mime2ext(mime): + """ + Given a mime type return the canonical file extension + """ + return {"image/png": "png", + "image/jpeg": "jpg", + "image/gif": "gif", + "application/octet-stream-xmp": "xmp", + "image/x-canon-cr2": "cr2", + "video/mp4": "mp4", + "video/quicktime": "mov"}[mime] + + +def auth(): + """ + Return the currently authorized username (per request) or None + """ + return cherrypy.session.get('authed', None) + + +def require_auth(func): + """ + Decorator: raise 403 unless session is authed + """ + def wrapped(*args, **kwargs): + if not auth(): + raise cherrypy.HTTPError(403) + return func(*args, **kwargs) + return wrapped + + +def photo_auth_filter(query): + """ + Sqlalchemy helper: filter the given PhotoSet query to items that match the authorized user's PhotoStatus access + level. Currently, authed users can access ALL photos, and unauthed users can access only PhotoStatus.public + status items. + """ + return query.filter(PhotoSet.status == PhotoStatus.public) if not auth() else query + + +def slugify(words): + return ''.join(letter for letter in '-'.join(words.lower().split()) + if ('a' <= letter <= 'z') or ('0' <= letter <= '9') or letter == '-') diff --git a/requirements.txt b/requirements.txt index b50e975..2d5eaa9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ backports.functools-lru-cache==1.5 cheroot==6.5.2 -CherryPy==18.0.0 +CherryPy==18.1.1 contextlib2==0.5.5 jaraco.functools==1.20 -Jinja2==2.10 +Jinja2==2.10.1 MarkupSafe==1.0 more-itertools==4.3.0 Pillow==5.2.0 @@ -11,6 +11,6 @@ portend==2.3 python-magic==0.4.15 pytz==2018.5 six==1.11.0 -SQLAlchemy==1.2.11 +SQLAlchemy==1.3.5 tempora==1.13 zc.lockfile==1.3.0