Skip to content

Commit

Permalink
Implement more Beaker.workspace methods (#71)
Browse files Browse the repository at this point in the history
* Implement more `Beaker.workspace` methods

* Improve error docs

* Implement `Beaker.workspace.move()` method

* Add `Beaker.workspace.create()` method

* fix validate name

* Add some missing errors from doc strings
  • Loading branch information
epwalsh authored Apr 14, 2022
1 parent 518fe97 commit cb9493d
Show file tree
Hide file tree
Showing 11 changed files with 326 additions and 40 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Implemented `Beaker.workspace.archive()`, `.unarchive()`, `.rename()`, `.move()`, and `create()` methods.
- Implemented `Beaker.job.stop()` and `Beaker.job.finalize()` methods.
- Added `WorkspaceWriteError` for when you attempt to write to an archived workspace. Before this
would just result in an `HTTPError` with a 403 status code.

### Changed

- Allowed using workspace name without organization when `Config.default_org` is set.
Otherwise `OrganizationNotSet` error is raised.

## [v0.8.1](https://github.com/allenai/beaker-py/releases/tag/v0.8.1) - 2022-04-12

Expand Down
2 changes: 1 addition & 1 deletion beaker/data_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -686,13 +686,13 @@ class NodeUtilization(BaseModel):
class Workspace(BaseModel):
id: str
name: str
full_name: str
size: WorkspaceSize
owner: Account
author: Account
created: datetime
modified: datetime
archived: bool = False
full_name: str


class WorkspaceRef(BaseModel):
Expand Down
8 changes: 8 additions & 0 deletions beaker/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ class WorkspaceNotFound(BeakerError):
pass


class WorkspaceWriteError(BeakerError):
pass


class WorkspaceConflict(BeakerError):
pass


class ClusterNotFound(BeakerError):
pass

Expand Down
4 changes: 4 additions & 0 deletions beaker/services/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ def create(
:raises WorkspaceNotFound: If the workspace doesn't exist.
:raises WorkspaceNotSet: If neither ``workspace`` nor
:data:`Beaker.config.defeault_workspace <beaker.Config.default_workspace>` are set.
:raises WorkspaceWriteError: If the workspace has been archived.
:raises OrganizationNotFound: If the organization doesn't exist.
:raises OrganizationNotSet: If the workspace name doesn't start with
an organization and :data:`Config.default_org <beaker.Config.default_org>` is not set.
:raises HTTPError: Any other HTTP exception that can occur.
:raises UnexpectedEOFError: If a source file is an empty file, or if a source is a directory and
the contents of one of the directory's files changes while creating the dataset.
Expand Down
4 changes: 4 additions & 0 deletions beaker/services/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ def create(
:raises WorkspaceNotFound: If the workspace doesn't exist.
:raises WorkspaceNotSet: If neither ``workspace`` nor
:data:`Beaker.config.defeault_workspace <beaker.Config.default_workspace>` are set.
:raises WorkspaceWriteError: If the workspace has been archived.
:raises OrganizationNotFound: If the organization doesn't exist.
:raises OrganizationNotSet: If the workspace name doesn't start with
an organization and :data:`Config.default_org <beaker.Config.default_org>` is not set.
:raises ImageNotFound: If the image specified by the spec doesn't exist.
:raises DatasetNotFound: If a source dataset in the spec doesn't exist.
:raises SecretNotFound: If a source secret in the spec doesn't exist.
Expand Down
4 changes: 4 additions & 0 deletions beaker/services/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ def create(
:raises WorkspaceNotFound: If the workspace doesn't exist.
:raises WorkspaceNotSet: If neither ``workspace`` nor
:data:`Beaker.config.defeault_workspace <beaker.Config.default_workspace>` are set.
:raises WorkspaceWriteError: If the workspace has been archived.
:raises OrganizationNotFound: If the organization doesn't exist.
:raises OrganizationNotSet: If the workspace name doesn't start with
an organization and :data:`Config.default_org <beaker.Config.default_org>` is not set.
:raises HTTPError: Any other HTTP exception that can occur.
"""
Expand Down
15 changes: 13 additions & 2 deletions beaker/services/secret.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ def get(self, secret: str, workspace: Optional[Union[str, Workspace]] = None) ->
:raises WorkspaceNotFound: If the workspace doesn't exist.
:raises WorkspaceNotSet: If neither ``workspace`` nor
:data:`Beaker.config.defeault_workspace <beaker.Config.default_workspace>` are set.
:raises OrganizationNotFound: If the organization doesn't exist.
:raises OrganizationNotSet: If the workspace name doesn't start with
an organization and :data:`Config.default_org <beaker.Config.default_org>` is not set.
:raises SecretNotFound: If the secret doesn't exist.
:raises HTTPError: Any other HTTP exception that can occur.
"""
workspace: Workspace = self._resolve_workspace(workspace)
workspace: Workspace = self._resolve_workspace(workspace, read_only_ok=True)
return Secret.from_json(
self.request(
f"workspaces/{workspace.id}/secrets/{self._url_quote(secret)}",
Expand All @@ -46,10 +49,12 @@ def read(
:raises WorkspaceNotFound: If the workspace doesn't exist.
:raises WorkspaceNotSet: If neither ``workspace`` nor
:data:`Beaker.config.defeault_workspace <beaker.Config.default_workspace>` are set.
:raises OrganizationNotSet: If the workspace name doesn't start with
an organization and :data:`Config.default_org <beaker.Config.default_org>` is not set.
:raises SecretNotFound: If the secret doesn't exist.
:raises HTTPError: Any other HTTP exception that can occur.
"""
workspace: Workspace = self._resolve_workspace(workspace)
workspace: Workspace = self._resolve_workspace(workspace, read_only_ok=True)
name = secret.name if isinstance(secret, Secret) else secret
return self.request(
f"workspaces/{workspace.id}/secrets/{self._url_quote(name)}/value",
Expand All @@ -70,6 +75,9 @@ def write(
:raises WorkspaceNotFound: If the workspace doesn't exist.
:raises WorkspaceNotSet: If neither ``workspace`` nor
:data:`Beaker.config.defeault_workspace <beaker.Config.default_workspace>` are set.
:raises OrganizationNotSet: If the workspace name doesn't start with
an organization and :data:`Config.default_org <beaker.Config.default_org>` is not set.
:raises WorkspaceWriteError: If the workspace has been archived.
:raises HTTPError: Any other HTTP exception that can occur.
"""
workspace: Workspace = self._resolve_workspace(workspace)
Expand All @@ -92,6 +100,9 @@ def delete(self, secret: Union[str, Secret], workspace: Optional[Union[str, Work
:raises WorkspaceNotFound: If the workspace doesn't exist.
:raises WorkspaceNotSet: If neither ``workspace`` nor
:data:`Beaker.config.defeault_workspace <beaker.Config.default_workspace>` are set.
:raises WorkspaceWriteError: If the workspace has been archived.
:raises OrganizationNotSet: If the workspace name doesn't start with
an organization and :data:`Config.default_org <beaker.Config.default_org>` is not set.
:raises SecretNotFound: If the secret doesn't exist.
:raises HTTPError: Any other HTTP exception that can occur.
"""
Expand Down
30 changes: 27 additions & 3 deletions beaker/services/service_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,33 @@ def _resolve_workspace_name(self, workspace: Optional[str]) -> str:
workspace_name = workspace or self.config.default_workspace
if workspace_name is None:
raise WorkspaceNotSet("'workspace' argument required since default workspace not set")
elif "/" not in workspace_name:
if self.config.default_org is not None:
self._validate_workspace_name(workspace_name)
return f"{self.config.default_org}/{workspace_name}"
else:
raise OrganizationNotSet(
f"No default organization set and workspace name doesn't include "
f"an organization ('{workspace_name}')"
)
else:
org, name = workspace_name.split("/")
self._validate_workspace_name(name)
self.beaker.organization.get(org)
return workspace_name

def _resolve_workspace(self, workspace: Optional[Union[str, Workspace]]) -> Workspace:
def _resolve_workspace(
self, workspace: Optional[Union[str, Workspace]], read_only_ok: bool = False
) -> Workspace:
out: Workspace
if isinstance(workspace, Workspace):
return workspace
out = workspace
else:
workspace_name = self._resolve_workspace_name(workspace)
return self.beaker.workspace.get(workspace_name)
out = self.beaker.workspace.get(workspace_name)
if not read_only_ok and out.archived:
raise WorkspaceWriteError(f"Workspace {out.full_name} has been archived")
return out

def _resolve_org_name(self, org: Optional[str]) -> str:
org_name = org or self.config.default_org
Expand All @@ -108,3 +126,9 @@ def _resolve_org(self, org: Optional[Union[str, Organization]]) -> Organization:

def _url_quote(self, id: str) -> str:
return urllib.parse.quote(id, safe="")

def _validate_workspace_name(self, name: str):
if not name.replace("-", "").replace("_", "").isalnum():
raise ValueError(
f"Workspace name can only contain letters, digits, dashes, and underscores: '{name}'"
)
Loading

0 comments on commit cb9493d

Please sign in to comment.