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 json
from datetime import datetime
from photoapp.types import Photo, PhotoSet, Tag, TagItem, PhotoStatus, User, known_extensions, known_mimes, \
genuuid, generate_storage_path
from photoapp.types import Photo, PhotoSet, Tag, TagItem, PhotoStatus, User, JobTargetType, known_extensions, \
known_mimes, genuuid, generate_storage_path
from photoapp.utils import copysha, get_extension, slugify
from photoapp.image import special_magic_fobj
from photoapp.storage import StorageAdapter
@ -287,44 +287,74 @@ class PhotosApiV1PhotoTags(object):
return {}
@cherrypy.expose
@cherrypy.tools.json_out()
@cherrypy.popargs("uuid")
class JobsApiV1(object):
def __init__(self, library):
self.library = library
@cherrypy.expose
@cherrypy.tools.allow(methods=["POST", "GET"])
@cherrypy.tools.json_out()
def index(
self,
uuid=None, # when fetching an existing job, the job's UUID
):
def GET(self, uuid=None):
"""
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
# print()
# print('body', cherrypy.request.body.read())
# return 'handle post'
return "jobs get " + str(uuid)
# 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()
cherrypy._cptools.HandlerTool.callable_method_handler(self.other)
{
"name": "blah",
"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
def other(self):
print("in other")
yield "other result"
# translate target UUIDs to IDs
target_ids = []
for target_number, target in enumerate(body["targets"]):
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.storage import uri_to_storage
from photoapp.webutils import validate_password, serve_thumbnail_placeholder
from photoapp.jobs import JobsClient, JobSubscriber
from jinja2 import Environment, FileSystemLoader, select_autoescape
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_manager = LibraryManager(library_storage)
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
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 cherrypy
from cherrypy.process import plugins
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.pool import NullPool
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm.session import Session
from sqlalchemy import func
Base = declarative_base()
@ -115,3 +118,20 @@ class SATool(cherrypy.Tool):
raise
finally:
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()))
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)
targets = relationship("JobTarget", back_populates="job")