示例#1
0
def test_make_request__raises_an_Abort_if_the_response_data_cannot_be_serialized_into_the_response_model_cls(
        respx_mock, dummy_client):
    """
    Validate that the ``make_request()`` function will raise an Abort if the response data cannot be serialized as and
    validated with the ``response_model_cls``.
    """
    client = dummy_client(headers={"content-type": "garbage"})
    req_path = "/fake-path"

    respx_mock.get(f"{DEFAULT_DOMAIN}{req_path}").mock(
        return_value=httpx.Response(
            httpx.codes.OK,
            json=dict(a=1, b=2, c=3),
        ), )

    with pytest.raises(
            Abort, match="There was a big problem: Unexpected data in response"
    ) as err_info:
        make_request(
            client,
            req_path,
            "GET",
            abort_message="There was a big problem",
            abort_subject="BIG PROBLEM",
            support=True,
            response_model_cls=DummyResponseModel,
        )
    assert err_info.value.subject == "BIG PROBLEM"
    assert err_info.value.support is True
    assert err_info.value.log_message == f"Unexpected format in response data: {dict(a=1, b=2, c=3)}"
    assert isinstance(err_info.value.original_error, pydantic.ValidationError)
示例#2
0
def test_make_request__raises_an_Abort_if_the_response_cannot_be_deserialized_with_JSON(
        respx_mock, dummy_client):
    """
    Validate that the ``make_request()`` function will raise an Abort if the response is not JSON de-serializable.
    """
    client = dummy_client(headers={"content-type": "garbage"})
    req_path = "/fake-path"

    respx_mock.get(f"{DEFAULT_DOMAIN}{req_path}").mock(
        return_value=httpx.Response(
            httpx.codes.OK,
            text="Not JSON, my dude",
        ), )

    with pytest.raises(
            Abort, match="There was a big problem: Response carried no data"
    ) as err_info:
        make_request(client,
                     req_path,
                     "GET",
                     abort_message="There was a big problem",
                     abort_subject="BIG PROBLEM",
                     support=True)
    assert err_info.value.subject == "BIG PROBLEM"
    assert err_info.value.support is True
    assert err_info.value.log_message == "Failed unpacking json: Not JSON, my dude"
    assert isinstance(err_info.value.original_error,
                      json.decoder.JSONDecodeError)
示例#3
0
def test_make_request__raises_Abort_when_expected_status_is_not_None_and_response_status_does_not_match_it(
        respx_mock, dummy_client):
    """
    Validate that the ``make_request()`` function will raise an Abort if the ``expected_status`` arg is set and it
    does not match the status code of the response.
    """
    client = dummy_client(headers={"content-type": "garbage"})
    req_path = "/fake-path"

    respx_mock.get(f"{DEFAULT_DOMAIN}{req_path}").mock(
        return_value=httpx.Response(
            httpx.codes.BAD_REQUEST,
            text="It blowed up",
        ), )

    with pytest.raises(
            Abort, match="There was a big problem: Received an error response"
    ) as err_info:
        make_request(
            client,
            req_path,
            "GET",
            expected_status=200,
            abort_message="There was a big problem",
            abort_subject="BIG PROBLEM",
            support=True,
        )
    assert err_info.value.subject == "BIG PROBLEM"
    assert err_info.value.support is True
    assert err_info.value.log_message == "Got an error code for request: 400: It blowed up"
    assert err_info.value.original_error is None
示例#4
0
def delete(
    ctx: typer.Context,
    id: int = typer.Option(
        ...,
        help="The id of the job submission to delete",
    ),
):
    """
    Delete an existing job submission.
    """
    jg_ctx: JobbergateContext = ctx.obj

    # Make static type checkers happy
    assert jg_ctx.client is not None, "Client is uninitialized"

    make_request(
        jg_ctx.client,
        f"/job-submissions/{id}",
        "DELETE",
        expected_status=204,
        abort_message="Request to delete job submission was not accepted by the API",
        support=True,
    )
    terminal_message(
        "The job submission was successfully deleted.",
        subject="Job submission delete succeeded",
    )
示例#5
0
def test_make_request__raises_Abort_if_client_request_raises_exception(
        respx_mock, dummy_client):
    """
    Validate that the ``make_request()`` function will raise an Abort if the call to ``client.send`` raises an
    exception.
    """
    client = dummy_client(headers={"content-type": "garbage"})
    req_path = "/fake-path"
    original_error = httpx.RequestError("BOOM!")

    respx_mock.get(f"{DEFAULT_DOMAIN}{req_path}").mock(
        side_effect=original_error)

    with pytest.raises(
            Abort,
            match="There was a big problem: Communication with the API failed"
    ) as err_info:
        make_request(client,
                     req_path,
                     "GET",
                     abort_message="There was a big problem",
                     abort_subject="BIG PROBLEM",
                     support=True)
    assert err_info.value.subject == "BIG PROBLEM"
    assert err_info.value.support is True
    assert err_info.value.log_message == "There was an error making the request to the API"
    assert err_info.value.original_error == original_error
def delete(
    ctx: typer.Context,
    id: int = typer.Option(
        ...,
        help=f"the specific id of the application to delete. {ID_NOTE}",
    ),
):
    """
    Delete an existing application.
    """
    jg_ctx: JobbergateContext = ctx.obj

    # Make static type checkers happy
    assert jg_ctx.client is not None

    # Delete the upload. The API will also remove the application data files
    make_request(
        jg_ctx.client,
        f"/applications/{id}",
        "DELETE",
        expected_status=204,
        expect_response=False,
        abort_message="Request to delete application was not accepted by the API",
        support=True,
    )
    terminal_message(
        """
        The application was successfully deleted.
        """,
        subject="Application delete succeeded",
    )
示例#7
0
def test_make_request__success(respx_mock, dummy_client):
    """
    Validate that the ``make_request()`` function will make a request to the supplied path for the
    client domain, check the status code, extract the data, validate it, and return an instance of the
    supplied response model.
    """

    client = dummy_client(headers={"content-type": "garbage"})
    req_path = "/fake-path"

    respx_mock.get(f"{DEFAULT_DOMAIN}{req_path}").mock(
        return_value=httpx.Response(
            httpx.codes.OK,
            json=dict(
                foo=1,
                bar="one",
            ),
        ), )
    dummy_response_instance = make_request(
        client,
        req_path,
        "GET",
        expected_status=200,
        response_model_cls=DummyResponseModel,
    )
    assert isinstance(dummy_response_instance, DummyResponseModel)
    assert dummy_response_instance.foo == 1
    assert dummy_response_instance.bar == "one"
def upload_application(
    jg_ctx: JobbergateContext,
    application_path: pathlib.Path,
    application_id: int,
) -> bool:
    """
    Upload an application given an application path and the application id.

    :param: jg_ctx:           The JobbergateContext. Needed to access the Httpx client with which to make API calls
    :param: application_path: The directory where the application files to upload may be found
    :param: application_id:   The id of the application for which to upload  data
    :returns: True if the upload was successful; False otherwise
    """

    # Make static type checkers happy
    assert jg_ctx.client is not None

    with tempfile.TemporaryDirectory() as temp_dir_str:
        build_path = pathlib.Path(temp_dir_str)
        logger.debug("Building application tar file at {build_path}")
        tar_path = build_application_tarball(application_path, build_path)
        response_code = cast(
            int,
            make_request(
                jg_ctx.client,
                f"/applications/{application_id}/upload",
                "POST",
                expect_response=False,
                abort_message=
                "Request to upload application files was not accepted by the API",
                support=True,
                files=dict(upload_file=open(tar_path, "rb")),
            ),
        )
        return response_code == 201
def _upload_application(jg_ctx: JobbergateContext, application_path: pathlib.Path, application_id: int) -> bool:
    """
    Utility function that uploads an application from a given path to the Jobbergate API.

    :param: jg_ctx:           The JobbergateContext. Needed to access the Httpx client to use for requests
    :param: application_path: The directory on the system where the application files are found
    :param: application_id:   The ``id`` of the Application to be uploaded
    """

    # Make static type checkers happy
    assert jg_ctx.client is not None

    with tempfile.TemporaryDirectory() as temp_dir_str:
        build_path = pathlib.Path(temp_dir_str)
        logger.debug("Building application tar file at {build_path}")
        tar_path = build_application_tarball(application_path, build_path)
        response_code = cast(
            int,
            make_request(
                jg_ctx.client,
                f"/applications/{application_id}/upload",
                "POST",
                expect_response=False,
                abort_message="Request to upload application files was not accepted by the API",
                support=True,
                files=dict(upload_file=open(tar_path, "rb")),
            ),
        )
        return response_code == 201
示例#10
0
def test_make_request__uses_request_model_instance_for_request_body_if_passed(
        respx_mock, dummy_client):
    """
    Validate that the ``make_request()`` function will use a pydantic model instance to build the body of the request if
    the ``request_model`` argument is passed.
    """

    client = dummy_client(headers={"content-type": "garbage"})
    req_path = "/fake-path"

    dummy_route = respx_mock.post(f"{DEFAULT_DOMAIN}{req_path}")
    dummy_route.mock(return_value=httpx.Response(
        httpx.codes.CREATED,
        json=dict(
            foo=1,
            bar="one",
        ),
    ), )
    dummy_response_instance = make_request(
        client,
        req_path,
        "POST",
        expected_status=201,
        response_model_cls=DummyResponseModel,
        request_model=DummyResponseModel(foo=1, bar="one"),
    )
    assert isinstance(dummy_response_instance, DummyResponseModel)
    assert dummy_response_instance.foo == 1
    assert dummy_response_instance.bar == "one"

    assert dummy_route.calls.last.request.content == json.dumps(
        dict(foo=1, bar="one")).encode("utf-8")
    assert dummy_route.calls.last.request.headers[
        "Content-Type"] == "application/json"
示例#11
0
def create_job_submission(
    jg_ctx: JobbergateContext,
    job_script_id: int,
    name: str,
    description: Optional[str] = None,
    cluster_id: Optional[str] = None,
    execution_directory: Optional[Path] = None,
) -> JobSubmissionResponse:
    """
    Create a Job Submission from the given Job Script.

    :param: jg_ctx:              The JobbergateContext. Used to retrieve the client for requests
                                 and the email of the submitting user
    :param: job_script_id:       The ``id`` of the Job Script to submit to Slurm
    :param: name:                The name to attach to the Job Submission
    :param: description:         An optional description that may be added to the Job Submission
    :param: cluster_id:          An optional cluster_id for the cluster where the job should be executed,
                                 if left off, it will default to the current cluster.
    :param: execution_directory: An optional directory where the job should be executed. If provided as a relative path,
                                 it will be constructed as an absolute path relative to the current working directory.
    :returns: The Job Submission data returned by the API after creating the new Job Submission
    """

    # Make static type checkers happy
    assert jg_ctx.client is not None, "jg_ctx.client is uninitialized"
    assert jg_ctx.persona is not None, "jg_ctx.persona is uninitialized"

    job_submission_data = JobSubmissionCreateRequestData(
        job_submission_name=name,
        job_submission_description=description,
        job_script_id=job_script_id,
        cluster_id=cluster_id,
    )

    if execution_directory is not None:
        if not execution_directory.is_absolute():
            execution_directory = Path.cwd() / execution_directory
        execution_directory.resolve()
        job_submission_data.execution_directory = execution_directory

    result = cast(
        JobSubmissionResponse,
        make_request(
            jg_ctx.client,
            "/job-submissions",
            "POST",
            expected_status=201,
            abort_message="Couldn't create job submission",
            support=True,
            request_model=job_submission_data,
            response_model_cls=JobSubmissionResponse,
        ),
    )
    return result
def fetch_application_data(
    jg_ctx: JobbergateContext,
    id: Optional[int] = None,
    identifier: Optional[str] = None,
) -> ApplicationResponse:
    """
    Retrieve an application from the API by ``id`` or ``identifier``.

    :param: jg_ctx:     The JobbergateContext. Needed to access the Httpx client with which to make API calls
    :param: id:         The id of the application to fetch
    :param: identifier: If supplied, look for an application instance with the provided identifier
    :returns: An instance of ApplicationResponse containing the application data
    """
    url = f"/applications/{id}"
    params = dict()
    if id is None and identifier is None:
        raise Abort(
            """
            You must supply either [yellow]id[/yellow] or [yellow]identifier[/yellow].
            """,
            subject="Invalid params",
            warn_only=True,
        )
    elif id is not None and identifier is not None:
        raise Abort(
            """
            You may not supply both [yellow]id[/yellow] and [yellow]identifier[/yellow].
            """,
            subject="Invalid params",
            warn_only=True,
        )
    elif identifier is not None:
        url = "/applications"
        params["identifier"] = identifier

    # Make static type checkers happy
    assert jg_ctx.client is not None

    stub = f"id={id}" if id is not None else f"identifier={identifier}"
    return cast(
        ApplicationResponse,
        make_request(
            jg_ctx.client,
            url,
            "GET",
            expected_status=200,
            abort_message=f"Couldn't retrieve application {stub} from API",
            response_model_cls=ApplicationResponse,
            support=True,
            params=params,
        ),
    )
示例#13
0
def test_make_request__returns_the_response_status_code_if_expect_response_is_False(
        respx_mock, dummy_client):
    """
    Validate that the ``make_request()`` function will return None if the ``expect_response`` arg is False and the
    request was successfull.
    """
    client = dummy_client(headers={"content-type": "garbage"})
    req_path = "/fake-path"

    respx_mock.post(f"{DEFAULT_DOMAIN}{req_path}").mock(
        return_value=httpx.Response(httpx.codes.BAD_REQUEST), )

    assert make_request(client, req_path, "POST",
                        expect_response=False) == httpx.codes.BAD_REQUEST
示例#14
0
def list_all(
    ctx: typer.Context,
    show_all: bool = typer.Option(
        False,
        "--all",
        help="Show all job scripts, even the ones owned by others"),
    search: Optional[str] = typer.Option(
        None, help="Apply a search term to results"),
    sort_order: SortOrder = typer.Option(SortOrder.UNSORTED,
                                         help="Specify sort order"),
    sort_field: Optional[str] = typer.Option(
        None, help="The field by which results should be sorted"),
):
    """
    Show available job scripts
    """
    jg_ctx: JobbergateContext = ctx.obj

    # Make static type checkers happy
    assert jg_ctx is not None
    assert jg_ctx.client is not None

    params: Dict[str, Any] = dict(all=show_all)
    if search is not None:
        params["search"] = search
    if sort_order is not SortOrder.UNSORTED:
        params["sort_ascending"] = SortOrder is SortOrder.ASCENDING
    if sort_field is not None:
        params["sort_field"] = sort_field

    envelope = cast(
        ListResponseEnvelope,
        make_request(
            jg_ctx.client,
            "/job-scripts",
            "GET",
            expected_status=200,
            abort_message="Couldn't retrieve job scripts list from API",
            support=True,
            response_model_cls=ListResponseEnvelope,
            params=params,
        ),
    )
    render_list_results(
        jg_ctx,
        envelope,
        title="Job Scripts List",
        style_mapper=style_mapper,
        hidden_fields=HIDDEN_FIELDS,
    )
示例#15
0
def test_make_request__returns_the_response_status_code_if_the_method_is_DELETE(
        respx_mock, dummy_client):
    """
    Validate that the ``make_request()`` function will return None if the ``method`` arg is DELETE and the request
    was successfull.
    """
    client = dummy_client(headers={"content-type": "garbage"})
    req_path = "/fake-path"

    respx_mock.delete(f"{DEFAULT_DOMAIN}{req_path}").mock(
        return_value=httpx.Response(
            httpx.codes.OK,
            json=dict(error="It blowed up"),
        ), )

    assert make_request(client, req_path, "DELETE") == httpx.codes.OK
示例#16
0
def test_make_request__returns_a_plain_dict_if_response_model_cls_is_None(
        respx_mock, dummy_client):
    """
    Validate that the ``make_request()`` function will return a plain dictionary containing the response data if the
    ``response_model_cls`` argument is not supplied.
    """
    client = dummy_client(headers={"content-type": "garbage"})
    req_path = "/fake-path"

    respx_mock.get(f"{DEFAULT_DOMAIN}{req_path}").mock(
        return_value=httpx.Response(
            httpx.codes.OK,
            json=dict(a=1, b=2, c=3),
        ), )

    assert make_request(client, req_path, "GET") == dict(a=1, b=2, c=3)
示例#17
0
def update(
    ctx: typer.Context,
    id: int = typer.Option(
        ...,
        help="The id of the job script to update",
    ),
    job_script: str = typer.Option(
        ...,
        help="""
            The data with which to update job script.

            Format: string form of dictionary with main script as entry "application.sh"

            Example: '{"application.sh":"#!/bin/bash \\n hostname"}'
        """,
    ),
):
    """
    Update an existing job script.
    """
    jg_ctx: JobbergateContext = ctx.obj

    # Make static type checkers happy
    assert jg_ctx.client is not None

    job_script_result = cast(
        JobScriptResponse,
        make_request(
            jg_ctx.client,
            f"/job-scripts/{id}",
            "PUT",
            expected_status=200,
            abort_message="Couldn't update job script",
            support=True,
            json=dict(job_script_data_as_string=job_script),
            response_model_cls=JobScriptResponse,
        ),
    )
    render_single_result(
        jg_ctx,
        job_script_result,
        hidden_fields=HIDDEN_FIELDS,
        title="Updated Job Script",
    )
示例#18
0
def test_make_request__does_not_raise_Abort_when_expected_status_is_None_and_response_status_is_a_fail_code(
        respx_mock, dummy_client):
    """
    Validate that the ``make_request()`` function will not raise an Abort if the ``expected_status`` arg is not set
    and the response is an error status code.
    """
    client = dummy_client(headers={"content-type": "garbage"})
    req_path = "/fake-path"

    respx_mock.get(f"{DEFAULT_DOMAIN}{req_path}").mock(
        return_value=httpx.Response(
            httpx.codes.BAD_REQUEST,
            json=dict(error="It blowed up"),
        ), )

    err = make_request(
        client,
        req_path,
        "GET",
        response_model_cls=ErrorResponseModel,
    )
    assert err.error == "It blowed up"
示例#19
0
def fetch_job_script_data(
    jg_ctx: JobbergateContext,
    id: int,
) -> JobScriptResponse:
    """
    Retrieve a job_script from the API by ``id``
    """
    # Make static type checkers happy
    assert jg_ctx.client is not None

    return cast(
        JobScriptResponse,
        make_request(
            jg_ctx.client,
            f"/job-scripts/{id}",
            "GET",
            expected_status=200,
            abort_message=f"Couldn't retrieve job script ({id}) from API",
            support=True,
            response_model_cls=JobScriptResponse,
        ),
    )
def refresh_access_token(ctx: JobbergateContext, token_set: TokenSet):
    """
    Attempt to fetch a new access token given a refresh token in a token_set.

    Sets the access token in-place.

    If refresh fails, notify the user that they need to log in again.
    """
    url = f"https://{settings.AUTH0_DOMAIN}/oauth/token"
    logger.debug(f"Requesting refreshed access token from {url}")

    JobbergateCliError.require_condition(
        ctx.client is not None,
        "Attempted to refresh with a null client. This should not happen",
    )

    # Make static type-checkers happy
    assert ctx.client is not None

    refreshed_token_set = cast(
        TokenSet,
        make_request(
            ctx.client,
            "/oauth/token",
            "POST",
            abort_message="The auth token could not be refreshed. Please try logging in again.",
            abort_subject="EXPIRED ACCESS TOKEN",
            support=True,
            response_model_cls=TokenSet,
            data=dict(
                client_id=settings.AUTH0_CLIENT_ID,
                audience=settings.AUTH0_AUDIENCE,
                grant_type="refresh_token",
                refresh_token=token_set.refresh_token,
            ),
        ),
    )

    token_set.access_token = refreshed_token_set.access_token
示例#21
0
def fetch_job_submission_data(
    jg_ctx: JobbergateContext,
    job_submission_id: int,
) -> JobSubmissionResponse:
    """
    Retrieve a job submission from the API by ``id``
    """
    # Make static type checkers happy
    assert jg_ctx.client is not None, "Client is uninitialized"

    return cast(
        JobSubmissionResponse,
        make_request(
            jg_ctx.client,
            f"/job-submissions/{job_submission_id}",
            "GET",
            expected_status=200,
            abort_message=
            f"Couldn't retrieve job submission {job_submission_id} from API",
            support=True,
            response_model_cls=JobSubmissionResponse,
        ),
    )
示例#22
0
def create(
    ctx: typer.Context,
    name: str = typer.Option(
        ...,
        help="The name of the job script to create.",
    ),
    application_id: Optional[int] = typer.Option(
        None,
        help="The id of the application from which to create the job script.",
    ),
    application_identifier: Optional[str] = typer.Option(
        None,
        help=
        "The identifier of the application from which to create the job script.",
    ),
    sbatch_params: Optional[List[str]] = typer.Option(
        None,
        help="Optional parameter to submit raw sbatch parameters.",
    ),
    param_file: Optional[pathlib.Path] = typer.Option(
        None,
        help=dedent("""
            Supply a yaml file that contains the parameters for populating templates.
            If this is not supplied, the question asking in the application is triggered.
            """),
    ),
    fast: bool = typer.Option(
        False,
        help="Use default answers (when available) instead of asking the user.",
    ),
    submit: Optional[bool] = typer.Option(
        None,
        help="Do not ask the user if they want to submit a job.",
    ),
):
    """
    Create a new job script.
    """
    jg_ctx: JobbergateContext = ctx.obj

    # Make static type checkers happy
    assert jg_ctx.client is not None

    app_data = fetch_application_data(jg_ctx,
                                      id=application_id,
                                      identifier=application_identifier)
    (app_config, app_module) = load_application_data(app_data)

    request_data = JobScriptCreateRequestData(
        application_id=app_data.id,
        job_script_name=name,
        sbatch_params=sbatch_params,
        param_dict=app_config,
    )

    supplied_params = validate_parameter_file(
        param_file) if param_file else dict()
    execute_application(app_module,
                        app_config,
                        supplied_params,
                        fast_mode=fast)

    if app_config.jobbergate_config.job_script_name is not None:
        request_data.job_script_name = app_config.jobbergate_config.job_script_name

    job_script_result = cast(
        JobScriptResponse,
        make_request(
            jg_ctx.client,
            "/job-scripts",
            "POST",
            expected_status=201,
            abort_message="Couldn't create job script",
            support=True,
            request_model=request_data,
            response_model_cls=JobScriptResponse,
        ),
    )
    render_single_result(
        jg_ctx,
        job_script_result,
        hidden_fields=HIDDEN_FIELDS,
        title="Created Job Script",
    )

    # `submit` will be `None` --submit/--no-submit flag was not set
    if submit is None:

        # If not running in "fast" mode, ask the user what to do.
        if not fast:
            submit = typer.confirm(
                "Would you like to submit this job immediately?")

        # Otherwise, assume that the job script should be submitted immediately
        else:
            submit = True

    if not submit:
        return

    try:
        job_submission_result = create_job_submission(
            jg_ctx, job_script_result.id, job_script_result.job_script_name)
    except Exception as err:
        raise Abort(
            "Failed to immediately submit the job after job script creation.",
            subject="Automatic job submission failed",
            support=True,
            log_message=
            f"There was an issue submitting the job immediately job_script_id={job_script_result.id}.",
            original_error=err,
        )

    render_single_result(
        jg_ctx,
        job_submission_result,
        hidden_fields=JOB_SUBMISSION_HIDDEN_FIELDS,
        title="Created Job Submission (Fast Mode)",
    )
def update(
    ctx: typer.Context,
    id: int = typer.Option(
        ...,
        help=f"The specific id of the application to update. {ID_NOTE}",
    ),
    application_path: Optional[pathlib.Path] = typer.Option(
        None,
        help="The path to the directory where the application files are located",
    ),
    identifier: Optional[str] = typer.Option(
        None,
        help="Optional new application identifier to be set",
    ),
    application_desc: Optional[str] = typer.Option(
        None,
        help="Optional new application description to be set",
    ),
):
    """
    Update an existing application.
    """
    req_data = dict()

    if identifier:
        req_data["application_identifier"] = identifier

    if application_desc:
        req_data["application_description"] = application_desc

    if application_path is not None:
        validate_application_files(application_path)
        req_data["application_config"] = dump_full_config(application_path)
        req_data["application_file"] = read_application_module(application_path)

    jg_ctx: JobbergateContext = ctx.obj

    # Make static type checkers happy
    assert jg_ctx.client is not None

    result = cast(
        Dict[str, Any],
        make_request(
            jg_ctx.client,
            f"/applications/{id}",
            "PUT",
            expected_status=200,
            abort_message="Request to update application was not accepted by the API",
            support=True,
            json=req_data,
        ),
    )

    if application_path is not None:
        successful_upload = _upload_application(jg_ctx, application_path, id)
        if not successful_upload:
            terminal_message(
                f"""
                The zipped application files could not be uploaded.

                [yellow]If the problem persists, please contact [bold]{OV_CONTACT}[/bold]
                for support and trouble-shooting[/yellow]
                """,
                subject="File upload failed",
                color="yellow",
            )
        else:
            result["application_uploaded"] = True

    render_single_result(
        jg_ctx,
        result,
        hidden_fields=HIDDEN_FIELDS,
        title="Updated Application",
    )
def create(
    ctx: typer.Context,
    name: str = typer.Option(
        ...,
        help="The name of the application to create",
    ),
    identifier: Optional[str] = typer.Option(
        None,
        help=f"The human-friendly identifier of the application. {IDENTIFIER_NOTE}",
    ),
    application_path: pathlib.Path = typer.Option(
        ...,
        help="The path to the directory where the application files are located",
    ),
    application_desc: Optional[str] = typer.Option(
        None,
        help="A helpful description of the application",
    ),
):
    """
    Create a new application.
    """
    req_data = load_default_config()
    req_data["application_name"] = name
    if identifier:
        req_data["application_identifier"] = identifier

    if application_desc:
        req_data["application_description"] = application_desc

    validate_application_files(application_path)

    req_data["application_config"] = dump_full_config(application_path)
    req_data["application_file"] = read_application_module(application_path)

    jg_ctx: JobbergateContext = ctx.obj

    # Make static type checkers happy
    assert jg_ctx.client is not None

    result = cast(
        Dict[str, Any],
        make_request(
            jg_ctx.client,
            "/applications",
            "POST",
            expected_status=201,
            abort_message="Request to create application was not accepted by the API",
            support=True,
            json=req_data,
        ),
    )
    application_id = result["id"]

    successful_upload = upload_application(jg_ctx, application_path, application_id)
    if not successful_upload:
        terminal_message(
            f"""
            The zipped application files could not be uploaded.

            Try running the `update` command including the application path to re-upload.

            [yellow]If the problem persists, please contact [bold]{OV_CONTACT}[/bold]
            for support and trouble-shooting[/yellow]
            """,
            subject="File upload failed",
            color="yellow",
        )
    else:
        result["application_uploaded"] = True

    render_single_result(
        jg_ctx,
        result,
        hidden_fields=HIDDEN_FIELDS,
        title="Created Application",
    )
def fetch_auth_tokens(ctx: JobbergateContext) -> TokenSet:
    """
    Fetch an access token (and possibly a refresh token) from Auth0.

    Prints out a URL for the user to use to authenticate and polls the token endpoint to fetch it when
    the browser-based process finishes
    """
    # Make static type-checkers happy
    assert ctx.client is not None

    device_code_data = cast(
        DeviceCodeData,
        make_request(
            ctx.client,
            "/oauth/device/code",
            "POST",
            expected_status=200,
            abort_message="There was a problem retrieving a device verification code from the auth provider",
            abort_subject="COULD NOT RETRIEVE TOKEN",
            support=True,
            response_model_cls=DeviceCodeData,
            data=dict(
                client_id=settings.AUTH0_CLIENT_ID,
                audience=settings.AUTH0_AUDIENCE,
                scope="offline_access",  # To get refresh token
            ),
        ),
    )

    terminal_message(
        f"""
        To complete login, please open the following link in a browser:

          {device_code_data.verification_uri_complete}

        Waiting up to {settings.AUTH0_MAX_POLL_TIME / 60} minutes for you to complete the process...
        """,
        subject="Waiting for login",
    )

    for tick in TimeLoop(
        settings.AUTH0_MAX_POLL_TIME,
        message="Waiting for web login",
    ):

        response_data = cast(
            Dict,
            make_request(
                ctx.client,
                "/oauth/token",
                "POST",
                abort_message="There was a problem retrieving a device verification code from the auth provider",
                abort_subject="COULD NOT FETCH ACCESS TOKEN",
                support=True,
                data=dict(
                    grant_type="urn:ietf:params:oauth:grant-type:device_code",
                    device_code=device_code_data.device_code,
                    client_id=settings.AUTH0_CLIENT_ID,
                ),
            ),
        )
        if "error" in response_data:
            if response_data["error"] == "authorization_pending":
                logger.debug(f"Token fetch attempt #{tick.counter} failed")
                sleep(device_code_data.interval)
            else:
                # TODO: Test this failure condition
                raise Abort(
                    unwrap(
                        """
                        There was a problem retrieving a device verification code from the auth provider:
                        Unexpected failure retrieving access token.
                        """
                    ),
                    subject="Unexpected error",
                    support=True,
                    log_message=f"Unexpected error response: {response_data}",
                )
        else:
            return TokenSet(**response_data)

    raise Abort(
        "Login process was not completed in time. Please try again.",
        subject="Timed out",
        log_message="Timed out while waiting for user to complete login",
    )