seamm_webui.routers package#

Submodules#

seamm_webui.routers.admin module#

User-management endpoints for seamm_webui’s “local” auth mode.

Web equivalent of manage.py’s seamm-webui-user CLI (create/list/ set-password/delete) – same underlying seamm_datastore.User calls, same capabilities, nothing more. Role/group management is deliberately not here: every account so far is created with the “admin” role outright (see require_admin’s docstring), matching Phase 3’s “prove who you are, not partition who sees what” – a role picker would be UI for a decision that doesn’t exist yet.

Every route requires the admin role (require_admin, not just require_permission) – this manages other accounts’ credentials, a materially bigger blast radius than the rest of the API.

class seamm_webui.routers.admin.SetPassword(*, password: str)[source]#

Bases: BaseModel

model_config = {}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

password: str#
class seamm_webui.routers.admin.UserCreate(*, username: str, password: str, email: str | None = None, first_name: str | None = None, last_name: str | None = None)[source]#

Bases: BaseModel

email: str | None#
first_name: str | None#
last_name: str | None#
model_config = {}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

password: str#
username: str#
seamm_webui.routers.admin.create_user(payload: UserCreate, _: None = Depends(dependency=<function require_admin.<locals>._check>, use_cache=True, scope=None))[source]#
seamm_webui.routers.admin.delete_user(username: str, request: Request, _: None = Depends(dependency=<function require_admin.<locals>._check>, use_cache=True, scope=None))[source]#
seamm_webui.routers.admin.list_users(_: None = Depends(dependency=<function require_admin.<locals>._check>, use_cache=True, scope=None))[source]#
seamm_webui.routers.admin.set_password(username: str, payload: SetPassword, _: None = Depends(dependency=<function require_admin.<locals>._check>, use_cache=True, scope=None))[source]#

seamm_webui.routers.auth module#

Login/logout/current-user endpoints for seamm_webui.

Deliberately not behind require_permission() like every other route – these have to be callable while logged out (that’s the point of /me, which the frontend polls on load to decide whether to show a login page at all).

class seamm_webui.routers.auth.LoginRequest(*, username: str, password: str)[source]#

Bases: BaseModel

model_config = {}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

password: str#
username: str#
seamm_webui.routers.auth.login(payload: LoginRequest, response: Response)[source]#
seamm_webui.routers.auth.logout(response: Response)[source]#
seamm_webui.routers.auth.me(request: Request)[source]#

Who’s logged in, and whether the frontend needs to ask – “none” mode always reports no username (there’s nothing to log in as, even though internally requests run as a fixed identity; see auth.NONE_MODE_USERNAME), so the frontend never needs to special-case the mode itself beyond this one field.

is_admin drives whether the frontend shows the Admin nav entry at all – naturally False in “none” mode (username is already None there), not because that mode’s fixed identity lacks the role (it has it, see auth.require_admin), but because there’s no login flow to manage users for in the first place.

seamm_webui.routers.jobs module#

Job endpoints.

Real, paginated listing + single-job lookup + file listing/download + submission, reusing seamm_datastore’s existing Job.get()/get_by_id()/create() as-is.

class seamm_webui.routers.jobs.JobIds(*, ids: List[int])[source]#

Bases: BaseModel

ids: List[int]#
model_config = {}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class seamm_webui.routers.jobs.JobSubmission(*, flowchart: str, project: str = 'default', title: str, description: str = '', parameters: dict = <factory>)[source]#

Bases: BaseModel

description: str#
flowchart: str#
model_config = {}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

parameters: dict#
project: str#
title: str#
seamm_webui.routers.jobs.delete_job(job_id: int, _: None = Depends(dependency=<function require_permission.<locals>._check>, use_cache=True, scope=None))[source]#

Delete a job: removes the DB row AND its directory on disk, matching the old dashboard’s delete_job. No status restriction (same as the old dashboard) – deleting a still-running job is allowed, and is itself what stops it (see _delete_job_files_and_row).

seamm_webui.routers.jobs.delete_jobs(payload: JobIds, _: None = Depends(dependency=<function require_permission.<locals>._check>, use_cache=True, scope=None))[source]#

Bulk version of delete_job, for the job list’s “Delete selected”.

seamm_webui.routers.jobs.download_job_file(job_id: int, filename: str, _: None = Depends(dependency=<function require_permission.<locals>._check>, use_cache=True, scope=None))[source]#

Download a single file from the job’s directory as an attachment.

seamm_webui.routers.jobs.get_job(job_id: int, _: None = Depends(dependency=<function require_permission.<locals>._check>, use_cache=True, scope=None))[source]#
seamm_webui.routers.jobs.get_job_file_content(job_id: int, filename: str, _: None = Depends(dependency=<function require_permission.<locals>._check>, use_cache=True, scope=None))[source]#

Return a file’s text content for in-browser viewing (not a download).

Guards against two things a naive “just read_text() it” would choke on: binary files (decoding fails) and files too large to reasonably render in a browser tab. Both come back as a normal 200 with content: null and a reason, not an error – the frontend falls back to offering the download link instead of showing an error page.

seamm_webui.routers.jobs.kill_job(job_id: int, _: None = Depends(dependency=<function require_permission.<locals>._check>, use_cache=True, scope=None))[source]#

Ask seamm_jobserver to stop this job, keeping its files – unlike deleting the job’s project, which removes them (routers/projects.py’s delete_project).

This only requests the stop by setting status to “kill”; it doesn’t perform it. seamm_jobserver’s check_for_stopped_jobs() polls for status == “kill” every cycle, issues the actual local-process-kill/ scancel, and then flips status to “killed” itself. So the job’s status in the response here will be “kill”, not yet “killed” – the frontend should treat both as “a kill is in flight or done”, not poll this endpoint waiting for “killed” synchronously.

seamm_webui.routers.jobs.kill_jobs(payload: JobIds, _: None = Depends(dependency=<function require_permission.<locals>._check>, use_cache=True, scope=None))[source]#

Bulk version of kill_job, for the job list’s “Kill selected”.

Silently skips anything not killable (already finished, nonexistent, no permission) rather than failing the whole batch over one job that was already done – that’s the point of a bulk action. skipped distinguishes “found but not in a killable status” from not_found (“no such job / no permission”), so the frontend can say something more useful than “some jobs were skipped.”

seamm_webui.routers.jobs.list_job_files(job_id: int, _: None = Depends(dependency=<function require_permission.<locals>._check>, use_cache=True, scope=None))[source]#

List files under the job’s directory, as relative paths + sizes.

seamm_webui.routers.jobs.list_jobs(response: Response, offset: int | None = None, limit: int | None = None, sort_by: str = 'id', order: str = 'asc', project: str | None = None, status: str | None = None, title: str | None = None, queue: str | None = None, _: None = Depends(dependency=<function require_permission.<locals>._check>, use_cache=True, scope=None))[source]#

List jobs, optionally filtered to a single project by name, plus status/title/queue – all applied before pagination, same reasoning as the project filter below: filtering Job.get()’s already-paginated results after the fact would make “page 2 of running jobs” not actually be the second page of running jobs.

seamm_datastore’s Job.get() only has title/description filters (both substring), not project/status/queue, so this builds the same permission-filtered query it uses internally (Job.permissions_query) directly rather than going through it.

The total matching count (before offset/limit) goes in an X-Total-Count response header, not the JSON body – so the body stays a plain array (what the frontend/seamm_dashboard_client’s own callers already expect) while still giving JobsPage enough to jump to the last page rather than only ever knowing “is there a next page.”

seamm_webui.routers.jobs.submit_job(submission: JobSubmission, _: None = Depends(dependency=<function require_permission.<locals>._check>, use_cache=True, scope=None))[source]#

Submit a new job.

Writes flowchart.flow + job_data.json to the project directory (mirroring seamm_dashboard’s add_job / setup_job) and registers it via Job.create(). No separate “enqueue” step – the seamm_jobserver daemon picks up jobs with status “submitted” on its own, independent of which dashboard wrote them.

seamm_webui.routers.jobs.sync_job_files(job_id: int, _: None = Depends(dependency=<function require_permission.<locals>._check>, use_cache=True, scope=None))[source]#

Pull a still-running transport = ssh job’s remote files back on demand, reusing the same seamm_scheduler.stage machinery seamm_jobserver itself uses at job-terminal time – so the file tree/viewer doesn’t stay empty (or stale) for a remote job’s entire runtime, only pulling for real once it finishes.

Independent of the JobServer process: this Dashboard already reads the same <root>/<jobserver-name>.ini it does (queue_config.py, also used by GET /api/queues), so it can recompute the job’s remote path itself (SlurmSection.remote_wdir_for()) and build its own stager, rather than signaling the JobServer and waiting for its next poll cycle.

A no-op (synced: false, never an error), not a 4xx/5xx, for anything that isn’t a real remote-ssh job right now: no queue recorded, an unknown/removed queue, or a type=local/ transport=local queue (nothing to pull – the JobServer already shares this filesystem). Also a no-op, throttled, if called again for the same job within SYNC_MIN_INTERVAL – protects against multiple tabs/users or a tight status-poll loop hammering ssh+rsync. Gated on require_permission("read"), not "update": the effect is refreshing what’s visible, not changing job state, even though it writes files to disk.

Guarded by a fasteners.InterProcessLock on the same STAGE_LOCK_FILENAME seamm_jobserver’s own end-of-run pull locks, so the two can never run rsync against the same destination concurrently. Lock contention and a real transfer failure are both reported the same way a JobServer poll-cycle failure is treated – worth trying again shortly, not an error to surface to the user as broken.

async seamm_webui.routers.jobs.upload_job_file(job_id: int, file: UploadFile = File(PydanticUndefined), _: None = Depends(dependency=<function require_permission.<locals>._check>, use_cache=True, scope=None))[source]#

Upload a file into the job’s directory – the counterpart to seamm_dashboard_client’s Dashboard.submit(), which calls this once per external data file a flowchart references (e.g. an initial structure file for a –file argument), right after creating the job itself. Mirrors the old dashboard’s add_file_to_job: strip a leading “job:” from the filename, write under the job’s directory, creating any subdirectory (e.g. “data/”) as needed.

seamm_webui.routers.projects module#

Project endpoints.

Listing + full CRUD (Phase 2), reusing seamm_datastore’s existing Project.create()/Project.update()/get_by_id() as-is, the same pattern routers/jobs.py already uses for jobs.

class seamm_webui.routers.projects.ProjectCreate(*, name: str, description: str = '')[source]#

Bases: BaseModel

description: str#
model_config = {}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

name: str#
class seamm_webui.routers.projects.ProjectUpdate(*, name: str | None = None, description: str | None = None)[source]#

Bases: BaseModel

description: str | None#
model_config = {}#

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

name: str | None#
seamm_webui.routers.projects.create_project(submission: ProjectCreate, _: None = Depends(dependency=<function require_permission.<locals>._check>, use_cache=True, scope=None))[source]#

Create a project, mirroring the old dashboard’s add_project: a directory under <datastore>/projects/<name> plus the DB row.

seamm_webui.routers.projects.delete_project(project_id: int, _: None = Depends(dependency=<function require_permission.<locals>._check>, use_cache=True, scope=None))[source]#

Delete a project: removes the DB row AND its directory on disk (all job files), matching the old dashboard’s delete_project.

Any of the project’s jobs still “submitted”/”running” are explicitly set to status “kill” first, the same request routers/jobs.py’s kill_job makes – seamm_jobserver’s check_for_stopped_jobs() polls for that and stops them. This is not a side effect of deleting the project/job-project association: deleting a Project only removes rows from the job_project join table (confirmed empirically – a job’s own row, status, and jobs table membership are untouched by deleting its project), so seamm_jobserver’s other trigger, a job’s row vanishing entirely from the jobs table, does NOT fire here and can’t be relied on. The explicit status=”kill” below is what actually stops these jobs before their files disappear out from under them.

seamm_webui.routers.projects.get_project(project_id: int, _: None = Depends(dependency=<function require_permission.<locals>._check>, use_cache=True, scope=None))[source]#
seamm_webui.routers.projects.list_projects(_: None = Depends(dependency=<function require_permission.<locals>._check>, use_cache=True, scope=None))[source]#
seamm_webui.routers.projects.update_project(project_id: int, update: ProjectUpdate, _: None = Depends(dependency=<function require_permission.<locals>._check>, use_cache=True, scope=None))[source]#

seamm_webui.routers.queues module#

Queue/cluster-target endpoints.

Read-only visibility into the <root>/<jobserver-name>.ini config the JobServer paired with this Dashboard uses to route jobs to multiple clusters/queues (local, TinkerCliffs, Owl, …) – what a submission client (the Tk desktop dialog’s queue picker, Phase 4) needs to render a queue dropdown and the per-queue .limits-constrained override fields. See seamm_jobserver’s docs/developer_guide/campaigns/2026-08-10/ (multi-queue routing) for the full design; this is Phase 3, the read side.

Deliberately never returns transport/host/remote_* – those are this host’s own system/machine config (how this JobServer instance reaches a cluster), not something a submitting client needs or should see.

seamm_webui.routers.queues.list_queues(_: None = Depends(dependency=<function require_permission.<locals>._check>, use_cache=True, scope=None))[source]#

List every queue the paired JobServer instance can route jobs to.

Empty if no root was configured for this Dashboard (see queue_config.configure()) or no <root>/<jobserver-name>.ini exists there – both mean “the queue feature isn’t in use here”, exactly as a JobServer with no such file runs every job as a plain local subprocess.

Module contents#