workspaces_app.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. # Copyright (c) Jupyter Development Team.
  2. # Distributed under the terms of the Modified BSD License.
  3. """A workspace management CLI"""
  4. from __future__ import annotations
  5. import json
  6. import sys
  7. import warnings
  8. from pathlib import Path
  9. from typing import Any
  10. from jupyter_core.application import JupyterApp
  11. from traitlets import Bool, Unicode
  12. from ._version import __version__
  13. from .config import LabConfig
  14. from .workspaces_handler import WorkspacesManager
  15. # Default workspace ID
  16. # Needs to match PageConfig.defaultWorkspace define in packages/coreutils/src/pageconfig.ts
  17. DEFAULT_WORKSPACE = "default"
  18. class WorkspaceListApp(JupyterApp, LabConfig):
  19. """An app to list workspaces."""
  20. version = __version__
  21. description = """
  22. Print all the workspaces available
  23. If '--json' flag is passed in, a single 'json' object is printed.
  24. If '--jsonlines' flag is passed in, 'json' object of each workspace separated by a new line is printed.
  25. If nothing is passed in, workspace ids list is printed.
  26. """
  27. flags = dict(
  28. jsonlines=(
  29. {"WorkspaceListApp": {"jsonlines": True}},
  30. ("Produce machine-readable JSON Lines output."),
  31. ),
  32. json=(
  33. {"WorkspaceListApp": {"json": True}},
  34. ("Produce machine-readable JSON object output."),
  35. ),
  36. )
  37. jsonlines = Bool(
  38. False,
  39. config=True,
  40. help=(
  41. "If True, the output will be a newline-delimited JSON (see https://jsonlines.org/) of objects, "
  42. "one per JupyterLab workspace, each with the details of the relevant workspace"
  43. ),
  44. )
  45. json = Bool(
  46. False,
  47. config=True,
  48. help=(
  49. "If True, each line of output will be a JSON object with the "
  50. "details of the workspace."
  51. ),
  52. )
  53. def initialize(self, *args: Any, **kwargs: Any) -> None:
  54. """Initialize the app."""
  55. super().initialize(*args, **kwargs)
  56. self.manager = WorkspacesManager(self.workspaces_dir)
  57. def start(self) -> None:
  58. """Start the app."""
  59. workspaces = self.manager.list_workspaces()
  60. if self.jsonlines:
  61. for workspace in workspaces:
  62. print(json.dumps(workspace))
  63. elif self.json:
  64. print(json.dumps(workspaces))
  65. else:
  66. for workspace in workspaces:
  67. print(workspace["metadata"]["id"])
  68. class WorkspaceExportApp(JupyterApp, LabConfig):
  69. """A workspace export app."""
  70. version = __version__
  71. description = """
  72. Export a JupyterLab workspace
  73. If no arguments are passed in, this command will export the default
  74. workspace.
  75. If a workspace name is passed in, this command will export that workspace.
  76. If no workspace is found, this command will export an empty workspace.
  77. """
  78. def initialize(self, *args: Any, **kwargs: Any) -> None:
  79. """Initialize the app."""
  80. super().initialize(*args, **kwargs)
  81. self.manager = WorkspacesManager(self.workspaces_dir)
  82. def start(self) -> None:
  83. """Start the app."""
  84. if len(self.extra_args) > 1: # pragma: no cover
  85. warnings.warn("Too many arguments were provided for workspace export.")
  86. self.exit(1)
  87. raw = DEFAULT_WORKSPACE if not self.extra_args else self.extra_args[0]
  88. try:
  89. workspace = self.manager.load(raw)
  90. print(json.dumps(workspace))
  91. except Exception: # pragma: no cover
  92. self.log.error(json.dumps(dict(data=dict(), metadata=dict(id=raw))))
  93. class WorkspaceImportApp(JupyterApp, LabConfig):
  94. """A workspace import app."""
  95. version = __version__
  96. description = """
  97. Import a JupyterLab workspace
  98. This command will import a workspace from a JSON file. The format of the
  99. file must be the same as what the export functionality emits.
  100. """
  101. workspace_name = Unicode(
  102. None,
  103. config=True,
  104. allow_none=True,
  105. help="""
  106. Workspace name. If given, the workspace ID in the imported
  107. file will be replaced with a new ID pointing to this
  108. workspace name.
  109. """,
  110. )
  111. aliases = {"name": "WorkspaceImportApp.workspace_name"}
  112. def initialize(self, *args: Any, **kwargs: Any) -> None:
  113. """Initialize the app."""
  114. super().initialize(*args, **kwargs)
  115. self.manager = WorkspacesManager(self.workspaces_dir)
  116. def start(self) -> None:
  117. """Start the app."""
  118. if len(self.extra_args) != 1: # pragma: no cover
  119. self.log.info("One argument is required for workspace import.")
  120. self.exit(1)
  121. with self._smart_open() as fid:
  122. try: # to load, parse, and validate the workspace file.
  123. workspace = self._validate(fid)
  124. except Exception as e: # pragma: no cover
  125. self.log.info("%s is not a valid workspace:\n%s", fid.name, e)
  126. self.exit(1)
  127. try:
  128. workspace_path = self.manager.save(workspace["metadata"]["id"], json.dumps(workspace))
  129. except Exception as e: # pragma: no cover
  130. self.log.info("Workspace could not be exported:\n%s", e)
  131. self.exit(1)
  132. self.log.info("Saved workspace: %s", workspace_path)
  133. def _smart_open(self) -> Any:
  134. file_name = self.extra_args[0]
  135. if file_name == "-": # pragma: no cover
  136. return sys.stdin
  137. file_path = Path(file_name).resolve()
  138. if not file_path.exists(): # pragma: no cover
  139. self.log.info("%s does not exist.", file_name)
  140. self.exit(1)
  141. return file_path.open(encoding="utf-8")
  142. def _validate(self, data: Any) -> Any:
  143. workspace = json.load(data)
  144. if "data" not in workspace:
  145. msg = "The `data` field is missing."
  146. raise Exception(msg)
  147. # If workspace_name is set in config, inject the
  148. # name into the workspace metadata.
  149. if self.workspace_name is not None and self.workspace_name:
  150. workspace["metadata"] = {"id": self.workspace_name}
  151. elif "id" not in workspace["metadata"]:
  152. msg = "The `id` field is missing in `metadata`."
  153. raise Exception(msg)
  154. return workspace