Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ History
Next Release
------------
* Feat: Add implied relationships to entities (#42)
* Feat: Add ``dump()``, ``dumps()`` and ``loads()`` methods to ``Workspace`` (#48)

0.1.1 (2020-10-19)
------------------
Expand Down
2 changes: 1 addition & 1 deletion src/structurizr/api/structurizr_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ def get_workspace(self) -> Workspace:
)
logger.debug(response.text)
self._archive_workspace(response.text)
return Workspace.hydrate(WorkspaceIO.parse_raw(response.text))
return Workspace.loads(response.text)

def put_workspace(self, workspace: Workspace) -> None:
"""
Expand Down
48 changes: 45 additions & 3 deletions src/structurizr/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from typing import Any, Optional, Union

from pydantic import Field
from pydantic.types import StrBytes

from .abstract_base import AbstractBase
from .base_model import BaseModel
Expand Down Expand Up @@ -195,18 +196,59 @@ def __init__(

@classmethod
def load(cls, filename: Union[str, Path]) -> "Workspace":
"""Load a workspace from a JSON file (which may optionally be zipped)."""
"""Load a workspace from a JSON file (which may optionally be gzipped)."""
filename = Path(filename)
try:
with gzip.open(filename, "rt") as handle:
ws_io = WorkspaceIO.parse_raw(handle.read())
return cls.loads(handle.read())
except FileNotFoundError as error:
raise error
except OSError:
with filename.open() as handle:
ws_io = WorkspaceIO.parse_raw(handle.read())
return cls.loads(handle.read())

@classmethod
def loads(cls, json: StrBytes) -> "Workspace":
"""Load a workspace from a JSON string or bytes."""
ws_io = WorkspaceIO.parse_raw(json)
return cls.hydrate(ws_io)

def dump(
self,
filename: Union[str, Path],
*,
zip: Optional[bool] = None,
indent: Optional[int] = None,
**kwargs
):
"""
Save a workspace as JSON to a file, optionally gzipped.

By default, filenames ending with `.gz` will be zipped and anything else won't,
however this can be overridden by explicitly passing the `zip` argument.

Arguments:
filename: filename to write to.
zip: if specified then controls whether the contents are gzipped.
indent: if specified then pretty-print the JSON with given indent.
kwargs: other arguments to pass through to `json.dumps()`.
"""
filename = Path(filename)
if zip is None:
zip = str(filename).endswith(".gz")
with gzip.open(filename, "wt") if zip else filename.open("wt") as handle:
handle.write(self.dumps(indent=indent, **kwargs))

def dumps(self, indent: Optional[int] = None, **kwargs):
"""
Export a workspace as a JSON string.

Args:
indent (int): if specified then pretty-print the JSON with given indent.
kwargs: other arguments to pass through to `json.dumps()`.
"""
return WorkspaceIO.from_orm(self).json(indent=indent, **kwargs)

@classmethod
def hydrate(cls, workspace_io: WorkspaceIO) -> "Workspace":
"""Create a new instance of Workspace from its IO."""
Expand Down
79 changes: 79 additions & 0 deletions tests/integration/test_workspace_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,82 @@ def test_serialize_workspace(example, filename, monkeypatch):
# TODO (Midnighter): This should be equivalent to the above. Why is it not?
# Is `.json` not using the same default arguments as `.dict`?
# assert actual.dict() == expected.dict()


def test_save_and_load_workspace_to_string(monkeypatch):
"""Test saving as a JSON string and reloading."""
monkeypatch.syspath_prepend(EXAMPLES)
example = import_module("getting_started")
workspace = example.main()

json_string: str = workspace.dumps(indent=2)
workspace2 = Workspace.loads(json_string)

expected = WorkspaceIO.from_orm(workspace)
actual = WorkspaceIO.from_orm(workspace2)
assert json.loads(actual.json()) == json.loads(expected.json())


def test_load_workspace_from_bytes(monkeypatch):
"""Test loading from bytes rather than string."""
path = DEFINITIONS / "GettingStarted.json"
with open(path, mode="rb") as file:
binary_content = file.read()

workspace = Workspace.loads(binary_content)

assert workspace.model.software_systems != set()


def test_save_and_load_workspace_to_file(monkeypatch, tmp_path: Path):
"""Test saving as a JSON file and reloading."""
monkeypatch.syspath_prepend(EXAMPLES)
example = import_module("getting_started")
workspace = example.main()

filepath = tmp_path / "test_workspace.json"

workspace.dump(filepath, indent=2)
workspace2 = Workspace.load(filepath)

expected = WorkspaceIO.from_orm(workspace)
actual = WorkspaceIO.from_orm(workspace2)
assert json.loads(actual.json()) == json.loads(expected.json())


def test_save_and_load_workspace_to_gzipped_file(monkeypatch, tmp_path: Path):
"""Test saving as a zipped JSON file and reloading."""
monkeypatch.syspath_prepend(EXAMPLES)
example = import_module("getting_started")
workspace = example.main()

filepath = tmp_path / "test_workspace.json.gz"

workspace.dump(filepath)
workspace2 = Workspace.load(filepath)

expected = WorkspaceIO.from_orm(workspace)
actual = WorkspaceIO.from_orm(workspace2)
assert json.loads(actual.json()) == json.loads(expected.json())


def test_workspace_overridding_zip_flag(monkeypatch, tmp_path: Path):
"""Test that default zipping can be overridden explicitly."""
monkeypatch.syspath_prepend(EXAMPLES)
example = import_module("getting_started")
workspace = example.main()

filepath = tmp_path / "test_workspace.json.gz"

workspace.dump(filepath, zip=False)
contents = filepath.read_text()
assert "My software system" in contents

# Make sure can be loaded even though its not zipped and ends with .gz
Workspace.load(filepath)


def test_load_unknown_file_raises_file_not_found():
"""Test that attempting to load a non-existent file raises FileNotFound."""
with pytest.raises(FileNotFoundError):
Workspace.load("foobar.json")