vague jobs client / api architecture

This commit is contained in:
dave 2023-01-20 20:58:47 -08:00
parent 7b487e562b
commit 6958040ad5
5 changed files with 126 additions and 32 deletions

View File

@ -2,8 +2,8 @@ import os
import cherrypy import cherrypy
import json import json
from datetime import datetime from datetime import datetime
from photoapp.types import Photo, PhotoSet, Tag, TagItem, PhotoStatus, User, known_extensions, known_mimes, \ from photoapp.types import Photo, PhotoSet, Tag, TagItem, PhotoStatus, User, JobTargetType, known_extensions, \
genuuid, generate_storage_path known_mimes, genuuid, generate_storage_path
from photoapp.utils import copysha, get_extension, slugify from photoapp.utils import copysha, get_extension, slugify
from photoapp.image import special_magic_fobj from photoapp.image import special_magic_fobj
from photoapp.storage import StorageAdapter from photoapp.storage import StorageAdapter
@ -287,44 +287,74 @@ class PhotosApiV1PhotoTags(object):
return {} return {}
@cherrypy.expose
@cherrypy.tools.json_out()
@cherrypy.popargs("uuid") @cherrypy.popargs("uuid")
class JobsApiV1(object): class JobsApiV1(object):
def __init__(self, library): def __init__(self, library):
self.library = library self.library = library
@cherrypy.expose def GET(self, uuid=None):
@cherrypy.tools.allow(methods=["POST", "GET"])
@cherrypy.tools.json_out()
def index(
self,
uuid=None, # when fetching an existing job, the job's UUID
):
""" """
show the list of jobs or the specified job show the list of jobs or the specified job
or, if we're POST/PUTing, create a new job
POSTing without the job_args field will tell you the list of job args you need to submit
POSTing with job_args will actually create the job
""" """
# if cherrypy.request.method == "POST": # creating job return "jobs get " + str(uuid)
# print()
# print('body', cherrypy.request.body.read())
# return 'handle post'
# return 'job: ' + str(uuid) def POST(self):
"""
create a new job or update an existing one
# return cherrypy.dispatch.Dispatcher.call(self.other) JSON body should look like:
request, response = cherrypy.request, cherrypy.response {
request.dispatch = cherrypy.dispatch.Dispatcher() "name": "blah",
cherrypy._cptools.HandlerTool.callable_method_handler(self.other) "targets": [
{
"type": "photo",
"ids": [
1,
2,
3
]
}
]
}
print("response:", response) POSTing without the job_args field will tell you the list of job args you need to submit
POSTing with job_args will actually create the job
"""
return "lol" body = json.loads(cherrypy.request.body.read().decode()) # TODO max size
@cherrypy.expose # translate target UUIDs to IDs
def other(self): target_ids = []
print("in other") for target_number, target in enumerate(body["targets"]):
yield "other result" typ = JobTargetType[target["type"]]
if typ == JobTargetType.photo:
query = db.query(Photo.id).filter(Photo.uuid.in_(target["uuids"]))
elif typ == JobTargetType.photoset:
query = db.query(PhotoSet.id).filter(PhotoSet.uuid.in_(target["uuids"]))
elif typ == JobTargetType.tag:
query = db.query(Tag.id).filter(Tag.name.in_(target["uuids"]))
else:
raise Exception()
ids = [r[0] for r in query.all()]
if len(target["uuids"]) != len(ids): # TODO would be nice if we would say exactly which
raise cherrypy.HTTPError(400, "missing or duplicate UUIDs in target {}".format(target_number))
target_ids.append(dict(
type=JobTargetType[target["type"]],
targets=[r[0] for r in query.all()]
))
# output the job's UUID
return cherrypy.engine.publish("job-create", body["name"], target_ids)[0]
def DELETE(self, uuid):
"""
delete an existing job
"""
return "jobs delete"

View File

@ -18,6 +18,7 @@ from photoapp.dbutils import SAEnginePlugin, SATool, db, get_db_engine, date_for
from photoapp.utils import auth, require_auth, photoset_auth_filter, slugify, cherryparam, number_format from photoapp.utils import auth, require_auth, photoset_auth_filter, slugify, cherryparam, number_format
from photoapp.storage import uri_to_storage from photoapp.storage import uri_to_storage
from photoapp.webutils import validate_password, serve_thumbnail_placeholder from photoapp.webutils import validate_password, serve_thumbnail_placeholder
from photoapp.jobs import JobsClient, JobSubscriber
from jinja2 import Environment, FileSystemLoader, select_autoescape from jinja2 import Environment, FileSystemLoader, select_autoescape
from sqlalchemy import desc, asc, func, and_, or_ from sqlalchemy import desc, asc, func, and_, or_
@ -632,6 +633,8 @@ def setup_webapp(database_url, library_url, cache_url, thumb_service_url, debug=
library_storage = uri_to_storage(library_url) library_storage = uri_to_storage(library_url)
library_manager = LibraryManager(library_storage) library_manager = LibraryManager(library_storage)
thumbnail_tool = ThumbGenerator(library_manager, uri_to_storage(cache_url), thumb_service_url) thumbnail_tool = ThumbGenerator(library_manager, uri_to_storage(cache_url), thumb_service_url)
jobs_client = JobsClient(engine)
JobSubscriber(jobs_client)
# Setup and mount web ui # Setup and mount web ui
tpl_dir = os.path.join(APPROOT, "templates") if not debug else "templates" tpl_dir = os.path.join(APPROOT, "templates") if not debug else "templates"

View File

@ -1,11 +1,14 @@
from contextlib import closing
import sqlalchemy import sqlalchemy
import cherrypy import cherrypy
from cherrypy.process import plugins from cherrypy.process import plugins
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.pool import NullPool from sqlalchemy.pool import NullPool
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm.session import Session
from sqlalchemy import func from sqlalchemy import func
Base = declarative_base() Base = declarative_base()
@ -115,3 +118,20 @@ class SATool(cherrypy.Tool):
raise raise
finally: finally:
self.session.remove() self.session.remove()
def cursorwrap(func):
"""
Provides a cursor to the wrapped method as the first arg. This assumes that the wrapped function belongs to an
object because the cursor is sourced from the object's session attribute which is assumed to be a
sessionmaker callable.
"""
def wrapped(*args, **kwargs):
self = args[0]
# passthru if someone already passed a session
if len(args) >= 2 and isinstance(args[1], (Session, sqlalchemy.orm.scoping.scoped_session, DbAlias)):
return func(*args, **kwargs)
else:
with closing(self.session()) as c:
return func(self, c, *args[1:], **kwargs)
return wrapped

View File

@ -1,4 +1,45 @@
import cherrypy
from photoapp.dbutils import create_db_sessionmaker, cursorwrap
from photoapp.types import Job, JobTargetType, JobTarget
class JobsDAO(object):
pass class JobSubscriber(object):
"""
adapter between cherrypy bus and JobsClient
"""
def __init__(self, client):
self.client = client
cherrypy.engine.subscribe("job-create", self.client.create_job) # TODO make "job-create" a const somewhere?
class JobsClient(object):
def __init__(self, dbengine):
self.engine = dbengine
self.session = create_db_sessionmaker(self.engine)
@cursorwrap
def create_job(self, db, name, targets):
"""
targets: list of dict(
type=JobTargetType.TYPE,
targets=[1, 2, 3]
)
"""
job_targets = []
for target in targets:
for target_id in target["targets"]:
job_targets.append(
JobTarget(target_type=target["type"],
target=target_id)
)
j = Job(
job_name=name,
targets=job_targets,
)
db.add(j)
db.commit()
return j.uuid

View File

@ -237,7 +237,7 @@ class Job(Base):
uuid = Column(Unicode(length=36), unique=True, nullable=False, default=lambda: str(uuid.uuid4())) uuid = Column(Unicode(length=36), unique=True, nullable=False, default=lambda: str(uuid.uuid4()))
created = Column(DateTime, nullable=False, default=lambda: datetime.now()) created = Column(DateTime, nullable=False, default=lambda: datetime.now())
job_name = Column(String(length=64), nullable=False) job_name = Column(String(length=64), unique=True, nullable=False)
status = Column(Enum(JobStatus), nullable=False, default=JobStatus.paused) status = Column(Enum(JobStatus), nullable=False, default=JobStatus.paused)
targets = relationship("JobTarget", back_populates="job") targets = relationship("JobTarget", back_populates="job")