async def launch(
        self,
        image,
        username,
        server_name="",
        repo_url="",
        extra_args=None,
        event_callback=None,
    ):
        """Launch a server for a given image

        - creates a temporary user on the Hub if authentication is not enabled
        - spawns a server for temporary/authenticated user
        - generates a token
        - returns a dict containing:
          - `url`: the URL of the server
          - `image`: image spec
          - `repo_url`: the url of the repo
          - `extra_args`: Dictionary of extra arguments passed to the server
          - `token`: the token for the server
        """
        # TODO: validate the image argument?

        # Matches the escaping that JupyterHub does https://github.com/jupyterhub/jupyterhub/blob/c00c3fa28703669b932eb84549654238ff8995dc/jupyterhub/user.py#L427
        escaped_username = quote(username, safe="@~")
        if self.create_user:
            # create a new user
            app_log.info("Creating user %s for image %s", username, image)
            try:
                await self.api_request(
                    f"users/{escaped_username}", body=b"", method="POST"
                )
            except HTTPError as e:
                if e.response:
                    body = e.response.body
                else:
                    body = ""
                app_log.error(
                    "Error creating user %s: %s\n%s",
                    username,
                    e,
                    body,
                )
                raise web.HTTPError(500, f"Failed to create temporary user for {image}")
        elif server_name == "":
            # authentication is enabled but not named servers
            # check if user has a running server ('')
            user_data = await self.get_user_data(escaped_username)
            if server_name in user_data["servers"]:
                raise web.HTTPError(
                    409, f"User {username} already has a running server."
                )
        elif self.named_server_limit_per_user > 0:
            # authentication is enabled with named servers
            # check if user has already reached to the limit of named servers
            user_data = await self.get_user_data(escaped_username)
            len_named_spawners = len([s for s in user_data["servers"] if s != ""])
            if self.named_server_limit_per_user <= len_named_spawners:
                raise web.HTTPError(
                    409,
                    "User {} already has the maximum of {} named servers."
                    "  One must be deleted before a new server can be created".format(
                        username, self.named_server_limit_per_user
                    ),
                )

        if self.pre_launch_hook:
            await maybe_future(
                self.pre_launch_hook(self, image, username, server_name, repo_url)
            )

        # data to be passed into spawner's user_options during launch
        # and also to be returned into 'ready' state
        data = {
            "image": image,
            "repo_url": repo_url,
            "token": base64.urlsafe_b64encode(uuid.uuid4().bytes)
            .decode("ascii")
            .rstrip("=\n"),
        }
        if extra_args:
            data.update(extra_args)

        # server name to be used in logs
        _server_name = f" {server_name}" if server_name else ""

        # start server
        app_log.info(
            f"Starting server{_server_name} for user {username} with image {image}"
        )
        ready_event_future = asyncio.Future()

        def _cancel_ready_event(f=None):
            if not ready_event_future.done():
                if f and f.exception():
                    ready_event_future.set_exception(f.exception())
                else:
                    ready_event_future.cancel()

        try:
            await self.api_request(
                f"users/{escaped_username}/servers/{server_name}",
                method="POST",
                body=json.dumps(data).encode("utf8"),
            )
            # listen for pending spawn (launch) events until server is ready
            # do this even if previous request finished!
            buffer_list = []

            async def handle_chunk(chunk):
                lines = b"".join(buffer_list + [chunk]).split(b"\n\n")
                # the last item in the list is usually an empty line ('')
                # but it can be the partial line after the last `\n\n`,
                # so put it back on the buffer to handle with the next chunk
                buffer_list[:] = [lines[-1]]
                for line in lines[:-1]:
                    if line:
                        line = line.decode("utf8", "replace")
                    if line and line.startswith("data:"):
                        event = json.loads(line.split(":", 1)[1])
                        if event_callback:
                            await event_callback(event)

                        # stream ends when server is ready or fails
                        if event.get("ready", False):
                            if not ready_event_future.done():
                                ready_event_future.set_result(event)
                        elif event.get("failed", False):
                            if not ready_event_future.done():
                                ready_event_future.set_exception(
                                    web.HTTPError(
                                        500, event.get("message", "unknown error")
                                    )
                                )

            url_parts = ["users", escaped_username]
            if server_name:
                url_parts.extend(["servers", server_name, "progress"])
            else:
                url_parts.extend(["server/progress"])
            progress_api_url = url_path_join(*url_parts)
            self.log.debug(f"Requesting progress for {username}: {progress_api_url}")
            resp_future = self.api_request(
                progress_api_url,
                streaming_callback=lambda chunk: asyncio.ensure_future(
                    handle_chunk(chunk)
                ),
                request_timeout=self.launch_timeout,
            )
            try:
                await gen.with_timeout(
                    timedelta(seconds=self.launch_timeout), resp_future
                )
            except (gen.TimeoutError, TimeoutError):
                _cancel_ready_event()
                raise web.HTTPError(
                    500,
                    f"Image {image} for user {username} took too long to launch",
                )

        except HTTPError as e:
            _cancel_ready_event()
            if e.response:
                body = e.response.body
            else:
                body = ""

            app_log.error(
                f"Error starting server{_server_name} for user {username}: {e}\n{body}"
            )
            raise web.HTTPError(500, f"Failed to launch image {image}")
        except Exception:
            _cancel_ready_event()
            raise

        # verify that the server is running!
        try:
            # this should already be done, but it's async so wait a finite time
            await gen.with_timeout(timedelta(seconds=5), ready_event_future)
        except (gen.TimeoutError, TimeoutError):
            raise web.HTTPError(
                500, f"Image {image} for user {username} failed to launch"
            )

        data["url"] = self.hub_url + f"user/{escaped_username}/{server_name}"
        self.log.debug(data["url"])
        return data
Launch a server for a given image

        - creates a temporary user on the Hub if authentication is not enabled
        - spawns a server for temporary/authenticated user
        - generates a token
        - returns a dict containing:
          - `url`: the URL of the server
          - `image`: image spec
          - `repo_url`: the url of the repo
          - `extra_args`: Dictionary of extra arguments passed to the server
          - `token`: the token for the server
        
binderhub/launcher.py