Skip to content

Jobbergate API Reference

jobbergate_api

Main components of the application: routers, config, main, pagination and create super user script.

apps

Resources of the API.

clusters

Module to track agent's health on the clusters.

models

Database models for the cluster health resource.

ClusterStatus

Bases: CommonMixin, TimestampMixin, Base

Cluster status table definition.

is_healthy property
is_healthy: bool

Return True if the last_reported time is before now plus the interval in seconds between pings.

routers

Cluster status API endpoints.

get_cluster_status async
get_cluster_status(secure_session: SecureSession = Depends(secure_session(Permissions.ADMIN, Permissions.CLUSTERS_READ, commit=False)))

Get the status of the cluster.

get_cluster_status_by_client_id async
get_cluster_status_by_client_id(client_id: str, secure_session: SecureSession = Depends(secure_session(Permissions.ADMIN, Permissions.CLUSTERS_READ, commit=False)))

Get the status of a specific cluster.

report_cluster_status async
report_cluster_status(interval: int = Query(description='The interval in seconds between pings.', gt=0), secure_session: SecureSession = Depends(secure_session(Permissions.CLUSTERS_UPDATE, ensure_client_id=True)))

Report the status of the cluster.

schemas

Schema definitions for the cluster app.

ClusterStatusView

Bases: BaseModel

Describes the status of a cluster.

constants

Constants to be shared by all models.

FileType

Bases: str, Enum

File type enum.

dependencies

Router dependencies shared for multiple resources.

Note

The dependencies can be reused multiple times, since FastAPI caches the results.

CrudServices

Bases: NamedTuple

Provide a container class for the CRUD services.

FileServices

Bases: NamedTuple

Provide a container class for the file services.

SecureService dataclass

Bases: SecureSession

Dataclass to hold the secure session and the bucket.

Services

Bases: NamedTuple

Provide a container class for the services.

get_bucket_name
get_bucket_name(override_bucket_name: str | None = None) -> str

Get the bucket name based on the environment.

The name can be overridden when multi tenancy is enabled by passing a bucket name.

get_bucket_url
get_bucket_url() -> str | None

Get the bucket url based on the environment.

s3_bucket async
s3_bucket(bucket_name: str, s3_url: str | None) -> AsyncIterator[Bucket]

Create a bucket using a context manager.

secure_services
secure_services(*scopes: str, permission_mode: PermissionMode = PermissionMode.SOME, commit: bool = True, ensure_email: bool = False, ensure_client_id: bool = False)

Dependency to bind database services to a secure session.

service_factory
service_factory(session: AsyncSession, bucket: Bucket) -> Iterator[Services]

Create the services and bind them to a db section and s3 bucket.

file_validation

Validation methods for the uploaded files.

SyntaxValidationEquation module-attribute
SyntaxValidationEquation = Callable[[Union[str, bytes]], bool]

Type alias describing the function signature used to validate file syntax.

syntax_validation_dispatch module-attribute
syntax_validation_dispatch: dict[str, SyntaxValidationEquation] = {}

Dictionary mapping file extensions to the function used to validate their syntax.

check_uploaded_file_syntax
check_uploaded_file_syntax(file_obj: BinaryIO, filename: str) -> bool

Check the syntax of a given file.

get_suffix
get_suffix(filename: str) -> str

Get the suffix (file extension) from a given filename.

is_valid_jinja2_template
is_valid_jinja2_template(template: Union[str, bytes]) -> bool

Check if a given jinja2 template is valid by creating a Template object and trying to render it.

Parameters:

Name Type Description Default
template Union[str, bytes]

Jinja2 template.

required

Returns:

Type Description
bool

Boolean indicating if the template is valid or not.

is_valid_python_file
is_valid_python_file(source_code: Union[str, bytes]) -> bool

Check if a given Python source code is valid by parsing it into an AST node.

Parameters:

Name Type Description Default
source_code Union[str, bytes]

Python source code.

required

Returns:

Type Description
bool

Boolean indicating if the source code is valid or not.

is_valid_yaml_file
is_valid_yaml_file(yaml_file: Union[str, bytes]) -> bool

Check if a given YAML file is valid by parsing it with yaml.safe_load.

Parameters:

Name Type Description Default
yaml_file Union[str, bytes]

YAML file.

required

Returns:

Type Description
bool

Boolean indicating if the file is valid or not.

register_syntax_validator
register_syntax_validator(*file_extensions: str)

Use this decorator to register file syntax validation functions.

It creates a new entry on validation_dispatch, mapping the equation to the file extensions that are provided as arguments.

Raise ValueError if the provided file extensions do not start with a dot.

garbage_collector

Delete unused files from jobbergate's file storage.

delete_files_from_bucket async
delete_files_from_bucket(bucket, files_to_delete: set[str]) -> None

Delete files from the bucket.

garbage_collector async
garbage_collector(session, bucket, list_of_tables, background_tasks: BackgroundTasks) -> None

Delete unused files from jobbergate's file storage.

get_files_to_delete async
get_files_to_delete(session, table, bucket) -> set[str]

Get a set of files to delete.

get_set_of_files_from_bucket async
get_set_of_files_from_bucket(bucket, table) -> set[str]

Get a set of files from the bucket.

get_set_of_files_from_database async
get_set_of_files_from_database(session, table) -> set[str]

Get a set of files from the database.

job_script_templates

Module for the job script templates.

constants

Describe constants for the job script templates module.

models

Database models for the job_script_templates resource.

JobScriptTemplate

Bases: CrudMixin, Base

Job script template table definition.

Notice all relationships are lazy="raise" to prevent n+1 implicit queries. This means that the relationships must be explicitly eager loaded using helper functions in the class.

Attributes:

Name Type Description
identifier Mapped[Optional[str]]

The identifier of the job script template.

template_vars Mapped[dict[str, Any]]

The template variables of the job script template.

See Mixin class definitions for other columns.

include_files classmethod
include_files(query: Select) -> Select

Include custom options on a query to eager load files.

searchable_fields classmethod
searchable_fields()

Add identifier as a searchable field.

sortable_fields classmethod
sortable_fields()

Add identifier as a sortable field.

JobScriptTemplateFile

Bases: FileMixin, Base

Job script template files table definition.

Attributes:

Name Type Description
parent_id Mapped[int]

A foreign key to the parent job script template row.

file_type Mapped[FileType]

The type of the file.

See Mixin class definitions for other columns

WorkflowFile

Bases: FileMixin, Base

Workflow file table definition.

Attributes:

Name Type Description
parent_id Mapped[int]

A foreign key to the parent job script template row.

runtime_config Mapped[dict[str, Any]]

The runtime configuration of the workflow.

See Mixin class definitions for other columns

routers

Router for the Job Script Template resource.

job_script_template_clone async
job_script_template_clone(id_or_identifier: str = Path(), clone_request: JobTemplateCloneRequest | None = None, secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_TEMPLATES_CREATE, ensure_email=True)))

Clone a job script template by id or identifier.

job_script_template_create async
job_script_template_create(create_request: JobTemplateCreateRequest, secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_TEMPLATES_CREATE, ensure_email=True)))

Create a new job script template.

job_script_template_delete async
job_script_template_delete(id_or_identifier: str = Path(), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_TEMPLATES_DELETE, ensure_email=True)))

Delete a job script template by id or identifier.

job_script_template_delete_file async
job_script_template_delete_file(id_or_identifier: str = Path(), file_name: str = Path(), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_TEMPLATES_DELETE, ensure_email=True)))

Delete a file from a job script template by id or identifier.

job_script_template_garbage_collector async
job_script_template_garbage_collector(background_tasks: BackgroundTasks, secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_TEMPLATES_DELETE)))

Delete all unused files from jobbergate templates on the file storage.

job_script_template_get async
job_script_template_get(id_or_identifier: str = Path(), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_TEMPLATES_READ, commit=False)))

Get a job script template by id or identifier.

job_script_template_get_file async
job_script_template_get_file(id_or_identifier: str = Path(), file_name: str = Path(), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_TEMPLATES_READ, commit=False)))

Get a job script template file by id or identifier.

Note

See https://fastapi.tiangolo.com/advanced/custom-response/#streamingresponse

job_script_template_get_list async
job_script_template_get_list(list_params: ListParams = Depends(), include_null_identifier: bool = Query(False), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_TEMPLATES_READ, commit=False)))

Get a list of job script templates.

job_script_template_update async
job_script_template_update(update_request: JobTemplateUpdateRequest, id_or_identifier: str = Path(), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_TEMPLATES_UPDATE, ensure_email=True)))

Update a job script template by id or identifier.

job_script_template_upload_file async
job_script_template_upload_file(id_or_identifier: str = Path(), file_type: FileType = Path(), filename: str | None = Query(None, max_length=255), upload_file: UploadFile | None = File(None, description='File to upload'), previous_filename: str | None = Query(None, description='Previous name of the file in case a rename is needed', max_length=255), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_TEMPLATES_CREATE, ensure_email=True)))

Upload a file to a job script template by id or identifier.

job_script_template_upload_file_by_url async
job_script_template_upload_file_by_url(id_or_identifier: str = Path(), file_type: FileType = Path(), filename: str | None = Query(None, max_length=255), file_url: AnyUrl = Query(..., description='URL of the file to upload'), previous_filename: str | None = Query(None, description='Previous name of the file in case a rename is needed', max_length=255), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_TEMPLATES_CREATE, ensure_email=True)))

Upload a file to a job script template by id or identifier using file URL.

job_script_upload_file_by_url async
job_script_upload_file_by_url(id_or_identifier: str = Path(), runtime_config: RunTimeConfig | None = Body(None, description='Runtime configuration is optional when the workflow file already exists'), file_url: AnyUrl = Query(..., description='URL of the file to upload'), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_TEMPLATES_CREATE, ensure_email=True)))

Upload a file to a job script workflow by id or identifier from a URL.

job_script_workflow_delete_file async
job_script_workflow_delete_file(id_or_identifier: str = Path(), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_TEMPLATES_DELETE, ensure_email=True)))

Delete a workflow file from a job script template by id or identifier.

job_script_workflow_get_file async
job_script_workflow_get_file(id_or_identifier: str = Path(), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_TEMPLATES_READ, commit=False)))

Get a workflow file by id or identifier.

Note

See https://fastapi.tiangolo.com/advanced/custom-response/#streamingresponse

job_script_workflow_upload_file async
job_script_workflow_upload_file(id_or_identifier: str = Path(), runtime_config: RunTimeConfig | None = Body(None, description='Runtime configuration is optional when the workflow file already exists'), upload_file: UploadFile = File(..., description='File to upload'), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_TEMPLATES_CREATE, ensure_email=True)))

Upload a file to a job script workflow by id or identifier.

schemas

Provide schemas for the job script templates component.

JobTemplateBaseDetailedView

Bases: JobTemplateListView

Schema for the request to an entry.

Notice the files are omitted.

JobTemplateCloneRequest

Bases: BaseModel

Schema for the request to clone a job template.

empty_str_to_none classmethod
empty_str_to_none(value)

Coerce an empty string value to None.

not_empty_str classmethod
not_empty_str(value)

Do not allow a string value to be empty.

JobTemplateCreateRequest

Bases: BaseModel

Schema for the request to create a job template.

empty_str_to_none classmethod
empty_str_to_none(value)

Coerce an empty string value to None.

not_empty_str classmethod
not_empty_str(value)

Do not allow a string value to be empty.

JobTemplateDetailedView

Bases: JobTemplateBaseDetailedView

Schema for the request to an entry.

Notice the files default to None, as they are not always requested, to differentiate between an empty list when they are requested, but no file is found.

JobTemplateListView

Bases: TableResource

Schema for the response to get a list of entries.

JobTemplateUpdateRequest

Bases: BaseModel

Schema for the request to update a job template.

empty_str_to_none classmethod
empty_str_to_none(value)

Coerce an empty string value to None.

not_empty_str classmethod
not_empty_str(value)

Do not allow a string value to be empty.

RunTimeConfig

Bases: BaseModel

Schema for the runtime config of a job template.

Notice this includes user supplied variables, so it has no predefined field. It also loads the contend directly from the json at the request payload.

coerce_string_to_dict classmethod
coerce_string_to_dict(data)

Get the validators.

TemplateFileDetailedView

Bases: BaseModel

Schema for the response to get a template file.

WorkflowFileDetailedView

Bases: BaseModel

Schema for the response to get a workflow file.

services

Services for the job_script_templates resource, including module specific business logic.

JobScriptTemplateFileService

Bases: FileService

Provide an empty derived class of FileService.

Although it doesn't do anything, it fixes errors with mypy: error: Value of type variable "FileModel" of "FileService" cannot be "JobScriptTemplateFile" error: Value of type variable "FileModel" of "FileService" cannot be "WorkflowFile"

JobScriptTemplateService

Bases: CrudService

Provide a CrudService that overloads the list query builder and locator logic.

build_list_query
build_list_query(sort_ascending: bool = True, search: str | None = None, sort_field: str | None = None, include_archived: bool = True, include_files: bool = False, include_parent: bool = False, include_null_identifier: bool = True, **additional_filters) -> Select

List all job_script_templates.

create async
create(**incoming_data) -> CrudModel

Add a new row for the model to the database.

locate_where_clause
locate_where_clause(id_or_identifier: Any) -> Any

Locate an instance using the id or identifier field.

update async
update(locator: Any, **incoming_data) -> CrudModel

Update a row by locator with supplied data.

validate_identifier
validate_identifier(identifier: str | None) -> None

Validate that the identifier is not an empty string nor composed only by digits.

Raise a ServiceError with status code 422 if the validation fails.

Many of the job-script-template endpoints use the id or identifier interchangeably as a path parameter. With that, we need to ensure that the identifier is not a number, as that would be identified as id.

tools

Provide a method for coercing id_or_identifier to a string or int.

coerce_id_or_identifier
coerce_id_or_identifier(id_or_identifier: str) -> int | str

Determine whether the id_or_identifier should be a string or an integer.

This is necessary because FastAPI no longer automatically converts path parameters to integers automatically if they may also be string values.

job_scripts

Provide module for job_scripts.

models

Database model for the JobScript resource.

JobScript

Bases: CrudMixin, Base

Job script table definition.

Notice all relationships are lazy="raise" to prevent n+1 implicit queries. This means that the relationships must be explicitly eager loaded using helper functions in the class.

Attributes:

Name Type Description
parent_template_id Mapped[int]

The id of the parent template.

See Mixin class definitions for other columns.

include_files classmethod
include_files(query: Select) -> Select

Include custom options on a query to eager load files.

include_parent classmethod
include_parent(query: Select) -> Select

Include custom options on a query to eager load parent data.

sortable_fields classmethod
sortable_fields()

Add parent_template_id as a sortable field.

JobScriptFile

Bases: FileMixin, Base

Job script files table definition.

Attributes:

Name Type Description
parent_template_id

The id of the parent template.

file_type Mapped[FileType]

The type of the file.

See Mixin class definitions for other columns

routers

Router for the Job Script Template resource.

job_script_auto_clean_unused_entries
job_script_auto_clean_unused_entries(background_tasks: BackgroundTasks, secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_SCRIPTS_DELETE)))

Automatically clean unused job scripts depending on a threshold.

job_script_clone async
job_script_clone(id: int = Path(), clone_request: JobScriptCloneRequest | None = None, secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_SCRIPTS_CREATE, ensure_email=True)))

Clone a job script by its id.

job_script_create async
job_script_create(create_request: JobScriptCreateRequest, secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_SCRIPTS_CREATE)))

Create a stand alone job script.

job_script_create_from_template async
job_script_create_from_template(create_request: JobScriptCreateRequest, render_request: RenderFromTemplateRequest, id_or_identifier: str = Path(...), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_SCRIPTS_CREATE, ensure_email=True)))

Create a new job script from a job script template.

job_script_delete async
job_script_delete(id: int = Path(...), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_SCRIPTS_DELETE, ensure_email=True)))

Delete a job script template by id or identifier.

job_script_delete_file async
job_script_delete_file(id: int = Path(...), file_name: str = Path(...), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_SCRIPTS_DELETE, ensure_email=True)))

Delete a file from a job script template by id or identifier.

job_script_garbage_collector
job_script_garbage_collector(background_tasks: BackgroundTasks, secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_SCRIPTS_DELETE)))

Delete all unused files from job scripts on the file storage.

job_script_get async
job_script_get(id: int = Path(), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_SCRIPTS_READ, commit=False)))

Get a job script by id.

job_script_get_file async
job_script_get_file(id: int = Path(...), file_name: str = Path(...), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_SCRIPTS_READ, commit=False)))

Get a job script file.

Note

See https://fastapi.tiangolo.com/advanced/custom-response/#streamingresponse

job_script_get_list async
job_script_get_list(list_params: ListParams = Depends(), from_job_script_template_id: int | None = Query(None, description='Filter job-scripts by the job-script-template-id they were created from.'), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_SCRIPTS_READ, commit=False)))

Get a list of job scripts.

job_script_update async
job_script_update(update_params: JobScriptUpdateRequest, id: int = Path(), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_SCRIPTS_UPDATE, ensure_email=True)))

Update a job script template by id or identifier.

job_script_upload_file async
job_script_upload_file(id: int = Path(...), file_type: FileType = Path(...), filename: str | None = Query(None, max_length=255), upload_file: UploadFile | None = File(None, description='File to upload'), previous_filename: str | None = Query(None, description='Previous name of the file in case a rename is needed', max_length=255), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_SCRIPTS_CREATE, ensure_email=True)))

Upload a file to a job script.

job_script_upload_file_by_url async
job_script_upload_file_by_url(id: int = Path(...), file_type: FileType = Path(...), filename: str | None = Query(None, max_length=255), file_url: AnyUrl = Query(..., description='URL of the file to upload'), previous_filename: str | None = Query(None, description='Previous name of the file in case a rename is needed', max_length=255), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_SCRIPTS_CREATE, ensure_email=True)))

Upload a file to a job script from a URL.

schemas

JobScript resource schema.

JobScriptBaseView

Bases: TableResource

Base model to match database for the JobScript resource.

Omits parent relationship.

JobScriptCloneRequest

Bases: BaseModel

Request model for cloning JobScript instances.

JobScriptCreateRequest

Bases: BaseModel

Request model for creating JobScript instances.

JobScriptDetailedView

Bases: JobScriptBaseView

Model to match database for the JobScript resource.

JobScriptFileDetailedView

Bases: BaseModel

Model for the job_script_files field of the JobScript resource.

JobScriptListView

Bases: JobScriptBaseView

Model to match database for the JobScript resource.

JobScriptUpdateRequest

Bases: BaseModel

Request model for updating JobScript instances.

RenderFromTemplateRequest

Bases: BaseModel

Request model for creating a JobScript entry from a template.

services

Services for the job_scripts resource, including module specific business logic.

AutoCleanResponse

Bases: NamedTuple

Named tuple for the response of auto_clean_unused_job_scripts.

JobScriptCrudService

Bases: CrudService

Provide an empty derived class of CrudService.

Although it doesn't do anything, it fixes an error with mypy: error: Value of type variable "CrudModel" of "CrudService" cannot be "JobScript"

auto_clean_unused_job_scripts async
auto_clean_unused_job_scripts() -> AutoCleanResponse

Automatically clean unused job scripts depending on a threshold.

Based on the last time each job script was updated or used to create a job submission, this will archived job scripts that were unarchived and delete jos script that were archived.

delete async
delete(locator: Any) -> None

Extend delete a row by locator.

Orphaned job-scripts are now allowed on Jobbergate. However, the agent relies on them to submit jobs after requesting GET /agent/pending. This creates a race condition and errors occur when a job-script is deleted before the agent handles its submissions.

To avoid this, they are marked as reject in this scenario.

JobScriptFileService

Bases: FileService

Provide an empty derived class of FileService.

Although it doesn't do anything, it fixes an error with mypy: error: Value of type variable "FileModel" of "FileService" cannot be "JobScriptFile"

upsert async
upsert(parent_id: int, filename: str, upload_content: str | bytes | AnyUrl | UploadFile | None, previous_filename: str | None = None, **upsert_kwargs) -> FileModel

Upsert a file instance.

validate_entrypoint_file async
validate_entrypoint_file(parent_id: int, filename: str)

Validate that the entrypoint file is unique.

tools

Provide a convenience class for managing job-script files.

inject_sbatch_params
inject_sbatch_params(job_script_data_as_string: str, sbatch_params: list[str]) -> str

Inject sbatch params into job script.

Given the job script as job_script_data_as_string, inject the sbatch params in the correct location.

job_submissions

Provide module for job_submissions.

constants

Describe constants for the job_submissions module.

JobSubmissionStatus

Bases: AutoNameEnum

Defines the set of possible statuses for a Job Submission.

SlurmJobState

Bases: AutoNameEnum

Defines the set of possible states for a job submitted to Slurm.

SlurmJobStateDetails dataclass

Defines the details for a given SlurmJobState including abbreviation and description.

models

Database model for the JobSubmission resource.

JobSubmission

Bases: CrudMixin, Base

Job submission table definition.

Notice all relationships are lazy="raise" to prevent n+1 implicit queries. This means that the relationships must be explicitly eager loaded using helper functions in the class.

Attributes:

Name Type Description
job_script_id Mapped[int]

Id number of the job scrip this submissions is based on.

execution_directory Mapped[str]

The directory where the job is executed.

slurm_job_id Mapped[int]

The id of the job in the slurm queue.

slurm_job_state Mapped[SlurmJobState]

The Slurm Job state as reported by the agent

slurm_job_info Mapped[str]

Detailed information about the Slurm Job as reported by the agent

client_id Mapped[str]

The id of the custer this submission runs on.

status Mapped[JobSubmissionStatus]

The status of the job submission.

report_message Mapped[str]

The message returned by the job.

sbatch_arguments Mapped[list[str]]

The arguments used to submit the job to the slurm queue.

See Mixin class definitions for other columns

include_files classmethod
include_files(query: Select) -> Select

Include custom options on a query to eager load files.

include_parent classmethod
include_parent(query: Select) -> Select

Include custom options on a query to eager load parent data.

searchable_fields classmethod
searchable_fields()

Add client_id as a searchable field.

sortable_fields classmethod
sortable_fields()

Add additional sortable fields.

routers

Router for the JobSubmission resource.

job_submission_agent_update async
job_submission_agent_update(update_params: JobSubmissionAgentUpdateRequest, id: int = Path(), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_SUBMISSIONS_UPDATE, ensure_client_id=True)))

Update a job_submission with slurm_job_state and slurm_job_info.

Note that if the new slurm_job_state is a termination state, the job submission status will be updated.

job_submission_clone async
job_submission_clone(id: int = Path(...), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_SUBMISSIONS_CREATE, ensure_email=True)))

Clone a job_submission given its id.

job_submission_create async
job_submission_create(create_request: JobSubmissionCreateRequest, secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_SUBMISSIONS_CREATE, ensure_email=True)))

Create a new job submission.

Make a post request to this endpoint with the required values to create a new job submission.

job_submission_delete async
job_submission_delete(id: int = Path(..., description='id of the job submission to delete'), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_SUBMISSIONS_DELETE, ensure_email=True)))

Delete job_submission given its id.

job_submission_get async
job_submission_get(id: int = Path(...), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_SUBMISSIONS_READ, commit=False)))

Return the job_submission given its id.

job_submission_get_list async
job_submission_get_list(list_params: ListParams = Depends(), slurm_job_ids: str | None = Query(None, description='Comma-separated list of slurm-job-ids to match active job_submissions'), submit_status: JobSubmissionStatus | None = Query(None, description='Limit results to those with matching status'), from_job_script_id: int | None = Query(None, description='Filter job-submissions by the job-script-id they were created from.'), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_SUBMISSIONS_READ, commit=False)))

List job_submissions for the authenticated user.

job_submission_update async
job_submission_update(update_params: JobSubmissionUpdateRequest, id: int = Path(), secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_SUBMISSIONS_UPDATE, ensure_email=True)))

Update a job_submission given its id.

job_submissions_agent_active async
job_submissions_agent_active(secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_SUBMISSIONS_READ, commit=False, ensure_client_id=True)))

Get a list of active job submissions for the cluster-agent.

job_submissions_agent_pending async
job_submissions_agent_pending(secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_SUBMISSIONS_READ, commit=False, ensure_client_id=True)))

Get a list of pending job submissions for the cluster-agent.

job_submissions_agent_rejected async
job_submissions_agent_rejected(rejected_request: JobSubmissionAgentRejectedRequest, secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_SUBMISSIONS_UPDATE, ensure_client_id=True)))

Update a job_submission to indicate that it was rejected by Slurm.

job_submissions_agent_submitted async
job_submissions_agent_submitted(submitted_request: JobSubmissionAgentSubmittedRequest, secure_services: SecureService = Depends(secure_services(Permissions.ADMIN, Permissions.JOB_SUBMISSIONS_UPDATE, ensure_client_id=True)))

Update a job_submission to indicate that it was submitted to Slurm.

schemas

JobSubmission resource schema.

ActiveJobSubmission

Bases: BaseModel

Specialized model for the cluster-agent to pull an active job_submission.

JobSubmissionAgentRejectedRequest

Bases: BaseModel

Request model for marking JobSubmission instances as REJECTED.

JobSubmissionAgentSubmittedRequest

Bases: BaseModel

Request model for marking JobSubmission instances as SUBMITTED.

JobSubmissionAgentUpdateRequest

Bases: BaseModel

Request model for updating JobSubmission instances.

JobSubmissionBaseView

Bases: TableResource

Base model to match the database for the JobSubmission resource.

Omits parent relationship.

JobSubmissionCreateRequest

Bases: BaseModel

Request model for creating JobSubmission instances.

empty_str_to_none classmethod
empty_str_to_none(v)

Ensure empty strings are converted to None to avoid problems with Path downstream.

JobSubmissionDetailedView

Bases: JobSubmissionBaseView

Complete model to match the database for the JobSubmission resource in a detailed view.

JobSubmissionListView

Bases: JobSubmissionBaseView

Complete model to match the database for the JobSubmission resource in a list view.

JobSubmissionUpdateRequest

Bases: BaseModel

Request model for updating JobSubmission instances.

empty_str_to_none
empty_str_to_none(v)

Ensure empty strings are converted to None to avoid problems with Path downstream.

PendingJobSubmission

Bases: BaseModel

Specialized model for the cluster-agent to pull pending job_submissions.

Model also includes data from its job_script and application sources.

services

Services for the job_submissions resource, including module specific business logic.

JobSubmissionService

Bases: CrudService

Provide a CrudService that overloads the list query builder.

build_list_query
build_list_query(sort_ascending: bool = True, search: str | None = None, sort_field: str | None = None, include_archived: bool = True, include_files: bool = False, include_parent: bool = False, filter_slurm_job_ids: list[int] | None = None, **additional_filters) -> Select

List all job_script_templates.

models

Functionalities to be shared by all models.

ArchiveMixin

Add is_archived column to a table.

Attributes:

Name Type Description
is_archived Mapped[bool]

Specify is a row is considered archived, hidden it by default when listing rows.

Base

Bases: DeclarativeBase

Base class for all models.

References

https://docs.sqlalchemy.org/en/20/orm/declarative_mixins.html

CommonMixin

Provide a dynamic table and helper methods for displaying instances.

__str__
__str__()

Produce a pretty string representation of the class instance.

__tablename__ classmethod
__tablename__() -> str

Dynamically create table name based on the class name.

CrudMixin

Bases: CommonMixin, IdMixin, TimestampMixin, OwnerMixin, NameMixin, ArchiveMixin

Add needed columns and declared attributes for all models that support a CrudService.

include_files classmethod
include_files(query: Select) -> Select

Include custom options on a query to eager load files.

This should be overridden by derived classes.

include_parent classmethod
include_parent(query: Select) -> Select

Include custom options on a query to eager load parent data.

This should be overridden by derived classes.

searchable_fields classmethod
searchable_fields()

Describe the fields that may be used in search queries.

sortable_fields classmethod
sortable_fields()

Describe the fields that may be used for sorting queries.

FileMixin

Bases: CommonMixin, TimestampMixin

Add needed columns and declared attributes for all models that support a FileService.

Attributes:

Name Type Description
parent_id Mapped[int]

The id of the parent row in another table. Note: Derived classes should override this attribute to make it a foreign key as well.

description Mapped[int]

The description of the job script template.

file_key
file_key() -> str

Dynamically define the s3 key for the file.

IdMixin

Provide an id primary_key column.

Attributes:

Name Type Description
id Mapped[int]

The id of the job script template.

cloned_from Mapped[int]

Specify the id of the row that this row was cloned from.

cloned_from_id
cloned_from_id() -> Mapped[int | None]

Dynamically create a cloned_from_id column.

NameMixin

Add name and description columns to a table.

Attributes:

Name Type Description
name Mapped[str]

The name of the job script template.

description Mapped[str | None]

The description of the job script template.

OwnerMixin

Add an owner email columns to a table.

Attributes:

Name Type Description
owner_email Mapped[str]

The email of the owner of the job script template.

TimestampMixin

Add timestamp columns to a table.

Attributes:

Name Type Description
created_at Mapped[DateTime]

The date and time when the job script template was created.

updated_at Mapped[DateTime]

The date and time when the job script template was updated.

permissions

Provide a module that describes permissions in the API.

Permissions

Bases: str, Enum

Describe the permissions that may be used for protecting Jobbergate routes.

can_bypass_ownership_check
can_bypass_ownership_check(permissions: list[str]) -> bool

Determine if the user has permissions that allow them to bypass ownership checks.

schemas

Define app-wide, reusable pydantic schemas.

ListParams

Bases: BaseModel

Describe the shared parameters for a list request.

PydanticDateTime

Bases: DateTime

A pendulum.DateTime object. At runtime, this type decomposes into pendulum.DateTime automatically.

This type exists because Pydantic throws a fit on unknown types.

This code is borrowed and enhanced from the pydantic-extra-types module but provides conversion from standard datetimes as well.

__get_pydantic_core_schema__ classmethod
__get_pydantic_core_schema__(source: Type[Any], handler: GetCoreSchemaHandler) -> core_schema.CoreSchema

Return a Pydantic CoreSchema with the Datetime validation.

Parameters:

Name Type Description Default
source Type[Any]

The source type to be converted.

required
handler GetCoreSchemaHandler

The handler to get the CoreSchema.

required

Returns:

Type Description
CoreSchema

A Pydantic CoreSchema with the Datetime validation.

TableResource

Bases: BaseModel

Describes a base for table models that include basic, common info.

services

Provide a generic services for CRUD and file operations in routers.

BucketBoundService

Provide base class for services that bind to an s3 bucket.

This class holds a reference to the bucket and provides methods to bind and unbind the bucket. It also keeps track of all instances of the service so that they can be iterated over.

bucket property
bucket: Bucket

Fetch the currently bound bucket.

Raise an exception if the service is not bound to a bucket.

__init__
__init__()

Initialize the service with a null bucket.

bind_bucket
bind_bucket(bucket: Bucket)

Bind the service to a bucket.

bound_bucket
bound_bucket(bucket: Bucket)

Provide a context within which the service is bound to a bucket.

unbind_bucket
unbind_bucket()

Unbind the service from a bucket.

CrudModelProto

Bases: Protocol

Provide a protocol for models that can be operated on by the CrudService.

This protocol enables type hints for editors and type checking with mypy.

These services would best be served by an intersection type so that the model_type is actually specified to inherit from both the mixins and the Base. This would allow static type checkers to recognize that all of the columns in a mixin are available and that the class can be instantiated in the create method. However, intersection types are not supported yet. For more information, see this discussion: https://github.com/python/typing/issues/213

__init__
__init__(**kwargs)

Declare that the protocol can be instantiated.

__tablename__
__tablename__() -> str

Declare that the protocol has a method to dynamically produce the table name.

include_files classmethod
include_files(query: Select) -> Select

Declare that the protocol has a method to include files in a query.

include_parent classmethod
include_parent(query: Select) -> Select

Declare that the protocol has a method to include details about the parent entry in a query.

searchable_fields classmethod
searchable_fields() -> set[str]

Declare that the protocol has searchable fields.

sortable_fields classmethod
sortable_fields() -> set[str]

Declare that the protocol has sortable fields.

CrudService

Bases: DatabaseBoundService, Generic[CrudModel]

Provide a service that can perform various crud operations using a supplied ORM model type.

name property
name

Helper property to recover the name of the table.

__init__
__init__(model_type: type[CrudModel])

Initialize the instance with an ORM model type.

build_list_query
build_list_query(sort_ascending: bool = True, search: str | None = None, sort_field: str | None = None, include_archived: bool = True, include_files: bool = False, include_parent: bool = False, **additional_filters) -> Select

Build the query to list matching rows.

Decomposed into a separate function so that deriving subclasses can add additional logic into the query.

clone_instance async
clone_instance(original_instance: CrudModel, **incoming_data) -> CrudModel

Clone an instance and update it with the supplied data.

count async
count() -> int

Count the number of rows in the table on the database.

create async
create(**incoming_data) -> CrudModel

Add a new row for the model to the database.

delete async
delete(locator: Any) -> None

Delete a row by locator.

In almost all cases, the locator will just be an id value.

ensure_attribute
ensure_attribute(instance: CrudModel, **attributes) -> None

Ensure that a model instance has the specified values on key attributes.

Raises HTTPException if the instance does not have the specified values.

get async
get(locator: Any, include_files: bool = False, include_parent: bool = False, ensure_attributes: dict[str, Any] | None = None) -> CrudModel

Get a row by locator.

In almost all cases, the locator will just be an id value.

Key value pairs can be provided as ensure_attributes to assert that the key fields have the specified values. This is useful to assert email ownership of a row before modifying it, besides any other attribute.

list async
list(**filter_kwargs) -> list[CrudModel]

List all crud rows matching specified filters.

For details on the supported filters, see the build_list_query() method.

locate_where_clause
locate_where_clause(locator: Any) -> Any

Provide the where clause expression to locate a row by locator.

This method allows derived classes to locate by alternative identifiers, though locator is an id value in almost all cases. compound primary keys.

paginated_list async
paginated_list(**filter_kwargs) -> Page[CrudModel]

List all crud rows matching specified filters with pagination.

For details on the supported filters, see the build_list_query() method.

update async
update(locator: Any, **incoming_data) -> CrudModel

Update a row by locator with supplied data.

In almost all cases, the locator will just be an id value.

DatabaseBoundService

Provide base class for services that bind to a database session.

This class holds a reference to the session and provides methods to bind and unbind the session. It also keeps track of all instances of the service so that they can be iterated over.

session property
session: AsyncSession

Fetch the currently bound session.

Raise an exception if the service is not bound to a session.

__init__
__init__()

Instantiate the service with a null session.

bind_session
bind_session(session: AsyncSession)

Bind the service to a session.

bound_session
bound_session(session: AsyncSession)

Provide a context within which the service is bound to a session.

unbind_session
unbind_session()

Unbind the service from a session.

FileModelProto

Bases: Protocol

Provide a protocol for models that can be operated on by the FileService.

This protocol enables type hints for editors and type checking with mypy.

These services would best be served by an intersection type so that the model_type is actually specified to inherit from both the mixins and the Base. This would allow static type checkers to recognize that all of the columns in a mixin are available and that the class can be instantiated in the create method. However, intersection types are not supported yet. For more information, see this discussion: https://github.com/python/typing/issues/213

__init__
__init__(**kwargs)

Declare that the protocol can be instantiated.

__tablename__
__tablename__() -> str

Declare that the protocol has a method to dynamically produce the table name.

FileService

Bases: DatabaseBoundService, BucketBoundService, Generic[FileModel]

Provide a service that can perform various file management operations using a supplied ORM model type.

__init__
__init__(model_type: type[FileModel])

Initialize the instance with an ORM model type.

add_instance async
add_instance(parent_id, filename, upsert_kwargs) -> FileModel

Add a file instance to the database.

clone_instance async
clone_instance(original_instance: FileModel, new_parent_id: int) -> FileModel

Clone a file instance and assign it to a new parent-id.

copy_file_content async
copy_file_content(source_instance: FileModel, destination_instance: FileModel) -> None

Copy the content of a file from one instance to another.

delete async
delete(instance: FileModel) -> None

Delete a file from s3 and from the corresponding table.

find_children async
find_children(parent_id: int) -> list[FileModel]

Find matching instances by parent_id.

get async
get(parent_id: int, filename: str) -> FileModel

Get a single instance by its parent id and filename (primary keys).

Requires that one and only one result is found.

get_file_content async
get_file_content(instance: FileModel) -> bytes

Get the full contents for a file entry.

render async
render(instance: FileModel, parameters: dict[str, Any]) -> str

Render the file using Jinja2.

The parameters are passed to the template as the context, and two of them are supported: * Directly as the context, for instance, if the template contains {{ foo }}. * As a data key for backward compatibility, for instance, if the template contains {{ data.foo }}.

stream_file_content async
stream_file_content(instance: FileModel) -> StreamingBody

Stream the content of a file using a boto3 StreamingBody.

The StreamingBody is an async generator that can be used for a StreamingResponse in a FastAPI app.

upload_file_content async
upload_file_content(instance: FileModel, upload_content: str | bytes | AnyUrl | UploadFile) -> None

Upload the content of a file to s3.

upsert async
upsert(parent_id: int, filename: str, upload_content: str | bytes | AnyUrl | UploadFile | None, previous_filename: str | None = None, **upsert_kwargs) -> FileModel

Upsert a file instance.

This method will either create a new file instance or update an existing one.

If a 'previous_filename' is provided, it is replaced by the new one, being deleted in the process. In this case, the 'upload_content' is optional, as the content can be copied from the previous file.

ServiceError

Bases: HTTPException

Make HTTPException more friendly by changing the default behavior so that the first arg is a message.

Also needed to play nice with py-buzz methods.

__init__
__init__(message, status_code=status.HTTP_400_BAD_REQUEST, **kwargs)

Instantiate the HTTPException super class by setting detail to the message provided.

config

Provide configuration settings for the app.

Pull settings from environment variables or a .env file if available.

LogLevelEnum

Bases: str, Enum

Provide an enumeration class describing the available log levels.

Settings

Bases: BaseSettings

Provide a pydantic BaseSettings model for the application settings.

remove_blank_env classmethod
remove_blank_env(values)

Remove any settings from the environment that are blank strings.

This allows the defaults to be set if docker-compose defaults a missing environment variable to a blank string.

check_none_or_all_keys_exist
check_none_or_all_keys_exist(input_dict: dict, target_keys: set) -> bool

Verify if none or all of the target keys exist in the input dictionary.

email_notification

Email notification system for Jobbergate.

EmailManager dataclass

Email manager.

send_email
send_email(to_emails: Union[str, List[str]], subject: str, skip_on_failure: bool = False, **kwargs) -> None

Send an email using this manager.

EmailNotificationError

Bases: Buzz

Custom error to be raised for problems at the email notification system.

notify_submission_rejected
notify_submission_rejected(job_submission_id: Union[str, int], report_message: str, to_emails: Union[str, List[str]]) -> None

Notify an email or a list of emails about a job submission that has been rejected.

logging

Provide functions to configure logging.

InterceptHandler

Bases: Handler

Specialized handler to intercept log lines sent to standard logging by 3rd party tools.

emit
emit(record: logging.LogRecord) -> None

Handle emission of the log record.

init_logging
init_logging()

Initialize logging by setting log level for normal logger and for the SQL logging.

main

Main file to startup the fastapi server.

health_check async
health_check()

Provide a health-check endpoint for the app.

lifespan async
lifespan(_: FastAPI)

Provide a lifespan context for the app.

Will set up logging and cleanup database engines when the app is shut down.

This is the preferred method of handling lifespan events in FastAPI. For mor details, see: https://fastapi.tiangolo.com/advanced/events/

validation_exception_handler async
validation_exception_handler(request: Request, err: RequestValidationError)

Handle exceptions from pydantic validators.

meta_mapper

Provides a metadata-mapper for re-using descriptions and examples across many pydantic models.

MetaField dataclass

Provides a dataclass that describes the metadata that will be mapped for an individual field.

MetaMapper

Maps re-usable metadata for fields. Should be used with the schema_extra property of a Model's Config.

Example::

foo_meta = MetaMapper(
    id=MetaField(
        description="The unique identifier of this Foo",
        example=13,
    ),
    name=MetaField(
        description="The name of this Foo",
        example="Bar",
    ),
    is_active=MetaField(
        description="Indicates if this Foo is active",
        example=True,
    ),
    created_at=MetaField(
        description="The timestamp indicating when this Foo was created",
        example="2023-08-18T13:55:37.172285",
    ),
)


class CreateFooRequest(BaseModel):
    name: str
    is_active: Optional[bool]

    class Config:
        schema_extra = foo_meta


class UpdateFooRequest(BaseModel):
    name: Optional[str] = None
    is_active: Optional[bool] = None

    class Config:
        schema_extra = foo_meta


class FooResponse(BaseModel):
    id: int
    name: str
    is_active: bool
    created_at: DateTime

    class Config:
        schema_extra = foo_meta

Notice in this example that the fields may be required in some models and optional in others. Further, not all the fields are present in all the models. The MetaMapper allows the models to share field metadata and yet define the fields independently.

__call__
__call__(schema: Dict[str, Any], *_) -> None

Map the MetaFields onto the metadata properties of a schema.

Should be used in a pydantic Model's Config class.

__init__
__init__(**kwargs: MetaField)

Map the kwargs into the field_dict.

All kwargs should be MetaFields, but any object duck-typed to include all the attributes of a MetaField will be accepted.

rabbitmq_notification

RabbitMQ notification system for Jobbergate.

publish_status_change async
publish_status_change(job_submission: JobSubmission, organization_id: Optional[str] = None)

Publish a status change for a JobSubmission to the RabbitMQ exchange used for notifications.

rabbitmq_connect async
rabbitmq_connect(exchange_name=None, do_purge=False)

Connect to a RabbitMQ queue and exchange.

safe_types

Provide "safe" type annotatons to avoid issues with mypy and Fast api.

Regarding the JobScript and JobSubmission type

These are needed for the relationships in the models. This avoids issues with circular imports at runtime.

Regarding the Bucket type

This is necessary because the Bucket type isn't importable from the normal boto3 modules. Instead, it must be imported from the mypy typing plugin for boto3.

The "type" must be bound to Any when not type checking because FastAPI does type inspection for its dependency injection system. Thus, there must be a type associated with Bucket even when not type checking.

security

Instantiates armasec resources for auth on api endpoints using project settings.

Also provides a factory function for TokenSecurity to reduce boilerplate.

IdentityPayload

Bases: TokenPayload

Provide an extension of TokenPayload that includes the user's identity.

extract_organization classmethod
extract_organization(values)

Extract the organization_id from the organization payload.

The payload is expected to look like: { ..., "organization": { "adf99e01-5cd5-41ac-a1af-191381ad7780": { ... } } }

get_domain_configs
get_domain_configs() -> list[DomainConfig]

Return a list of DomainConfig objects based on the input variables for the Settings class.

lockdown_with_identity
lockdown_with_identity(*scopes: str, permission_mode: PermissionMode = PermissionMode.SOME, ensure_email: bool = False, ensure_organization: bool = False, ensure_client_id: bool = False)

Provide a wrapper to be used with dependency injection to extract identity on a secured route.

storage

Provide functions to interact with persistent data storage.

EngineFactory

Provide a factory class that creates engines and keeps track of them in an engine mapping.

This is used for multi-tenancy and database URL creation at request time.

__init__
__init__()

Initialize the EngineFactory.

auto_session async
auto_session(override_db_name: str | None = None, commit: bool = True) -> typing.AsyncIterator[AsyncSession]

Get an asynchronous database session.

Gets a new session from the correct engine in the engine map.

cleanup async
cleanup()

Close all engines stored in the engine map and clears the engine_map.

get_engine
get_engine(override_db_name: str | None = None) -> AsyncEngine

Get a database engine.

If the database url is already in the engine map, return the engine stored there. Otherwise, build a new one, store it, and return the new engine.

SecureSession dataclass

Provide a container class for an IdentityPayload and AsyncSesson for the current request.

build_db_url
build_db_url(override_db_name: str | None = None, force_test: bool = False, asynchronous: bool = True) -> str

Build a database url based on settings.

If force_test is set, build from the test database settings. If asynchronous is set, use asyncpg. If override_db_name replace the database name in the settings with the supplied value.

handle_fk_error
handle_fk_error(_: fastapi.Request, err: asyncpg.exceptions.ForeignKeyViolationError)

Unpack metadata from a ForeignKeyViolationError and return a 409 response.

render_sql
render_sql(session: AsyncSession, query) -> str

Render a sqlalchemy query into a string for debugging.

search_clause
search_clause(search_terms: str, searchable_fields: set) -> ColumnElement[bool]

Create search clause across searchable fields with search terms.

Regarding the False first argument to or_(): The or_() function must have one fixed positional argument. See: https://docs.sqlalchemy.org/en/20/core/sqlelement.html#sqlalchemy.sql.expression.or_

secure_session
secure_session(*scopes: str, permission_mode: PermissionMode = PermissionMode.SOME, commit: bool = True, ensure_email: bool = False, ensure_organization: bool = False, ensure_client_id: bool = False)

Provide an injectable for FastAPI that checks permissions and returns a database session for this request.

This should be used for all secured routes that need access to the database. It will commit the transaction upon completion of the request. If an exception occurs, it will rollback the transaction. If multi-tenancy is enabled, it will retrieve a database session for the database associated with the client_id found in the requesting user's auth token.

If testing mode is enabled, it will flush the session instead of committing changes to the database.

Note that the session should NEVER be explicitly committed anywhere else in the source code.

sort_clause
sort_clause(sort_field: str, sortable_fields: set, sort_ascending: bool) -> typing.Union[Mapped, UnaryExpression, Case]

Create a sort clause given a sort field, the list of sortable fields, and a sort_ascending flag.

version

Provide the version of the package.

get_version
get_version() -> str

Get the version from the metadata if available, otherwise from pyproject.toml.

Returns "unknown" if both methods fail.

get_version_from_metadata
get_version_from_metadata() -> str

Get the version from the metadata.

This is the preferred method of getting the version, but only works if the package is properly installed in a Python environment.

get_version_from_poetry
get_version_from_poetry() -> str

Get the version from pyproject.toml.

This is a fallback method if the package is not installed, but just copied and accessed locally, like in a Docker image.