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, lines: int | None = None, end: bool = False, header_lines: int = 0, _: 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.

With lines, the first lines lines of the file of any size, or with end its first header_lines lines (the header of a table) and its last lines lines – the head or tail of a long table such as a LAMMPS .trj. total_lines then gives the length of the file and truncated whether part of it was left out.

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, depth: int | None = Query(None), _: 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.

depth limits how many levels down to look (1: only the files in the job directory itself), for jobs with very many files, such as a long parallel loop; without it, every file.

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

The job’s tasks per step and its parallel loops’ iterations, read from the task manifests and the iterations’ files (as of now for a local job, as of the last sync for a remote one).

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.resume_job(job_id: int, _: None = Depends(dependency=<function require_permission.<locals>._check>, use_cache=True, scope=None))[source]#

Ask seamm_jobserver to run a killed or failed job again, continuing from its checkpoint.

Like a kill, this only requests it: the job goes back to “submitted” with parameters["resume"] set, and seamm_jobserver starts it as it would a new job, in its own directory and queue, asking the flowchart to resume. The step that was interrupted runs again from its start. seamm_exec checks that the checkpoint matches the flowchart and command line, and runs from the top, saying why, if it cannot resume.

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#