Skip to content

Commit faae1d8

Browse files
yt-msMidnighter
authored andcommitted
feat: add loads(), dump() and dumps() to Workspace for import/export.
1 parent 3732199 commit faae1d8

File tree

3 files changed

+92
-4
lines changed

3 files changed

+92
-4
lines changed

src/structurizr/api/structurizr_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ def get_workspace(self) -> Workspace:
142142
)
143143
logger.debug(response.text)
144144
self._archive_workspace(response.text)
145-
return Workspace.hydrate(WorkspaceIO.parse_raw(response.text))
145+
return Workspace.loads(response.text)
146146

147147
def put_workspace(self, workspace: Workspace) -> None:
148148
"""

src/structurizr/workspace.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,18 +195,54 @@ def __init__(
195195

196196
@classmethod
197197
def load(cls, filename: Union[str, Path]) -> "Workspace":
198-
"""Load a workspace from a JSON file (which may optionally be zipped)."""
198+
"""Load a workspace from a file (which may optionally be gzipped)."""
199199
filename = Path(filename)
200200
try:
201201
with gzip.open(filename, "rt") as handle:
202-
ws_io = WorkspaceIO.parse_raw(handle.read())
202+
return cls.loads(handle.read())
203203
except FileNotFoundError as error:
204204
raise error
205205
except OSError:
206206
with filename.open() as handle:
207-
ws_io = WorkspaceIO.parse_raw(handle.read())
207+
return cls.loads(handle.read())
208+
209+
@classmethod
210+
def loads(cls, json_string: str) -> "Workspace":
211+
"""Load a workspace from a JSON string."""
212+
ws_io = WorkspaceIO.parse_raw(json_string)
208213
return cls.hydrate(ws_io)
209214

215+
def dump(
216+
self,
217+
filename: Union[str, Path],
218+
*,
219+
zip: bool = False,
220+
indent: Optional[int] = None,
221+
**kwargs
222+
):
223+
"""
224+
Save a workspace to a file, optionally zipped.
225+
226+
Arguments:
227+
filename (str/Path): filename to write to.
228+
zip (bool): if true then contents will be zipped with GZip.
229+
indent (int): if specified then pretty-print the JSON with given indent.
230+
kwargs: other arguments to pass through to `json.dumps()`.
231+
"""
232+
filename = Path(filename)
233+
with gzip.open(filename, "wt") if zip else open(filename, "wt") as handle:
234+
handle.write(self.dumps(indent=indent, **kwargs))
235+
236+
def dumps(self, indent: Optional[int] = None, **kwargs):
237+
"""
238+
Export a workspace as a JSON string.
239+
240+
Args:
241+
indent (int): if specified then pretty-print the JSON with given indent.
242+
kwargs: other arguments to pass through to `json.dumps()`.
243+
"""
244+
return WorkspaceIO.from_orm(self).json(indent=indent, **kwargs)
245+
210246
@classmethod
211247
def hydrate(cls, workspace_io: WorkspaceIO) -> "Workspace":
212248
"""Create a new instance of Workspace from its IO."""

tests/integration/test_workspace_io.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,55 @@ def test_serialize_workspace(example, filename, monkeypatch):
8585
# TODO (Midnighter): This should be equivalent to the above. Why is it not?
8686
# Is `.json` not using the same default arguments as `.dict`?
8787
# assert actual.dict() == expected.dict()
88+
89+
90+
def test_save_and_load_workspace_to_string(monkeypatch):
91+
"""Test saving as a JSON string and reloading."""
92+
monkeypatch.syspath_prepend(EXAMPLES)
93+
example = import_module("getting_started")
94+
workspace = example.main()
95+
96+
json_string: str = workspace.dumps(indent=2)
97+
workspace2 = Workspace.loads(json_string)
98+
99+
expected = WorkspaceIO.from_orm(workspace)
100+
actual = WorkspaceIO.from_orm(workspace2)
101+
assert json.loads(actual.json()) == json.loads(expected.json())
102+
103+
104+
def test_save_and_load_workspace_to_file(monkeypatch, tmp_path: Path):
105+
"""Test saving as a JSON file and reloading."""
106+
monkeypatch.syspath_prepend(EXAMPLES)
107+
example = import_module("getting_started")
108+
workspace = example.main()
109+
110+
filepath = tmp_path / "test_workspace.json"
111+
112+
workspace.dump(filepath, indent=2)
113+
workspace2 = Workspace.load(filepath)
114+
115+
expected = WorkspaceIO.from_orm(workspace)
116+
actual = WorkspaceIO.from_orm(workspace2)
117+
assert json.loads(actual.json()) == json.loads(expected.json())
118+
119+
120+
def test_save_and_load_workspace_to_zipped_file(monkeypatch, tmp_path: Path):
121+
"""Test saving as a zipped JSON file and reloading."""
122+
monkeypatch.syspath_prepend(EXAMPLES)
123+
example = import_module("getting_started")
124+
workspace = example.main()
125+
126+
filepath = tmp_path / "test_workspace.json.gz"
127+
128+
workspace.dump(filepath, zip=True)
129+
workspace2 = Workspace.load(filepath)
130+
131+
expected = WorkspaceIO.from_orm(workspace)
132+
actual = WorkspaceIO.from_orm(workspace2)
133+
assert json.loads(actual.json()) == json.loads(expected.json())
134+
135+
136+
def test_load_unknown_file_raises_file_not_found():
137+
"""Test that attempting to load a non-existent file raises FileNotFound."""
138+
with pytest.raises(FileNotFoundError):
139+
Workspace.load("foobar.json")

0 commit comments

Comments
 (0)