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.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_admindrives 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: nulland areason, 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.
skippeddistinguishes “found but not in a killable status” fromnot_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-Countresponse 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 = sshjob’s remote files back on demand, reusing the sameseamm_scheduler.stagemachineryseamm_jobserveritself 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>.iniit does (queue_config.py, also used byGET /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 atype=local/transport=localqueue (nothing to pull – the JobServer already shares this filesystem). Also a no-op, throttled, if called again for the same job withinSYNC_MIN_INTERVAL– protects against multiple tabs/users or a tight status-poll loop hammering ssh+rsync. Gated onrequire_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.InterProcessLockon the sameSTAGE_LOCK_FILENAMEseamm_jobserver’s own end-of-run pull locks, so the two can never runrsyncagainst 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
rootwas configured for this Dashboard (seequeue_config.configure()) or no<root>/<jobserver-name>.iniexists 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.