buildzr 0.0.7__py3-none-any.whl → 0.0.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- buildzr/__about__.py +1 -1
- buildzr/dsl/__init__.py +4 -1
- buildzr/dsl/color.py +121 -0
- buildzr/dsl/dsl.py +626 -179
- buildzr/dsl/explorer.py +4 -25
- buildzr/dsl/expression.py +60 -22
- buildzr/dsl/interfaces/__init__.py +0 -4
- buildzr/dsl/interfaces/interfaces.py +23 -37
- buildzr/dsl/relations.py +11 -76
- buildzr/encoders/encoder.py +24 -4
- {buildzr-0.0.7.dist-info → buildzr-0.0.9.dist-info}/METADATA +51 -66
- buildzr-0.0.9.dist-info/RECORD +24 -0
- {buildzr-0.0.7.dist-info → buildzr-0.0.9.dist-info}/WHEEL +1 -1
- buildzr-0.0.7.dist-info/RECORD +0 -23
- {buildzr-0.0.7.dist-info → buildzr-0.0.9.dist-info}/licenses/LICENSE.md +0 -0
buildzr/dsl/explorer.py
CHANGED
@@ -33,35 +33,14 @@ class Explorer:
|
|
33
33
|
yield from explorer
|
34
34
|
|
35
35
|
def walk_relationships(self) -> Generator[_Relationship, None, None]:
|
36
|
-
import buildzr
|
37
|
-
from buildzr.dsl.factory.gen_id import GenerateId
|
38
36
|
|
39
37
|
if self._workspace_or_element.children:
|
40
|
-
for child in self._workspace_or_element.children:
|
41
|
-
# Relationships aren't materialized in the `Workspace` or in any
|
42
|
-
# of the `DslElement`s. As such, we need to recreate the `_Relationship` objects
|
43
|
-
# from the Structurizr model.
|
44
38
|
|
45
|
-
|
46
|
-
for relationship, destination in zip(child.model.relationships, child.destinations):
|
47
|
-
fake_relationship = _Relationship(
|
48
|
-
_UsesData(
|
49
|
-
relationship=buildzr.models.Relationship(
|
50
|
-
id=relationship.id,
|
51
|
-
description=relationship.description,
|
52
|
-
properties=relationship.properties,
|
53
|
-
technology=relationship.technology,
|
54
|
-
tags=relationship.tags,
|
55
|
-
sourceId=relationship.sourceId,
|
56
|
-
),
|
57
|
-
source=child,
|
58
|
-
),
|
59
|
-
destination=destination,
|
60
|
-
_include_in_model=False,
|
61
|
-
)
|
62
|
-
fake_relationship._tags = set(relationship.tags.split(','))
|
39
|
+
for child in self._workspace_or_element.children:
|
63
40
|
|
64
|
-
|
41
|
+
if child.relationships:
|
42
|
+
for relationship in child.relationships:
|
43
|
+
yield relationship
|
65
44
|
|
66
45
|
explorer = Explorer(child).walk_relationships()
|
67
46
|
yield from explorer
|
buildzr/dsl/expression.py
CHANGED
@@ -10,9 +10,10 @@ from buildzr.dsl.dsl import (
|
|
10
10
|
SoftwareSystem,
|
11
11
|
Container,
|
12
12
|
Component,
|
13
|
-
_Relationship,
|
14
13
|
)
|
15
14
|
|
15
|
+
from buildzr.dsl.relations import _Relationship
|
16
|
+
|
16
17
|
import buildzr
|
17
18
|
from typing import Set, Union, Optional, List, Dict, Any, Callable, Tuple, Sequence, Iterable, cast, Type
|
18
19
|
from typing_extensions import TypeIs
|
@@ -87,6 +88,16 @@ class Element:
|
|
87
88
|
def properties(self) -> Dict[str, Any]:
|
88
89
|
return self._element.model.properties
|
89
90
|
|
91
|
+
@property
|
92
|
+
def group(self) -> Optional[str]:
|
93
|
+
"""
|
94
|
+
Returns the group of the element. The group is a string that is used to
|
95
|
+
group elements in the Structurizr DSL.
|
96
|
+
"""
|
97
|
+
if not isinstance(self._element.model, buildzr.models.Workspace):
|
98
|
+
return self._element.model.group
|
99
|
+
return None
|
100
|
+
|
90
101
|
def __eq__(self, element: object) -> bool:
|
91
102
|
return isinstance(element, type(self._element)) and\
|
92
103
|
element.model.id == self._element.model.id
|
@@ -135,10 +146,10 @@ class Expression:
|
|
135
146
|
|
136
147
|
def __init__(
|
137
148
|
self,
|
138
|
-
include_elements: Iterable[Callable[[Workspace, Element], bool]]=[lambda w, e: True],
|
139
|
-
exclude_elements: Iterable[Callable[[Workspace, Element], bool]]=[],
|
140
|
-
include_relationships: Iterable[Callable[[Workspace, Relationship], bool]]=[lambda w, e: True],
|
141
|
-
exclude_relationships: Iterable[Callable[[Workspace, Relationship], bool]]=[],
|
149
|
+
include_elements: Iterable[Union[DslElement, Callable[[Workspace, Element], bool]]]=[lambda w, e: True],
|
150
|
+
exclude_elements: Iterable[Union[DslElement, Callable[[Workspace, Element], bool]]]=[],
|
151
|
+
include_relationships: Iterable[Union[DslElement, Callable[[Workspace, Relationship], bool]]]=[lambda w, e: True],
|
152
|
+
exclude_relationships: Iterable[Union[DslElement, Callable[[Workspace, Relationship], bool]]]=[],
|
142
153
|
) -> 'None':
|
143
154
|
self._include_elements = include_elements
|
144
155
|
self._exclude_elements = exclude_elements
|
@@ -154,9 +165,19 @@ class Expression:
|
|
154
165
|
|
155
166
|
workspace_elements = buildzr.dsl.Explorer(workspace).walk_elements()
|
156
167
|
for element in workspace_elements:
|
157
|
-
|
158
|
-
|
159
|
-
|
168
|
+
includes: List[bool] = []
|
169
|
+
excludes: List[bool] = []
|
170
|
+
for f in self._include_elements:
|
171
|
+
if isinstance(f, DslElement):
|
172
|
+
includes.append(f == element)
|
173
|
+
else:
|
174
|
+
includes.append(f(workspace, Element(element)))
|
175
|
+
for f in self._exclude_elements:
|
176
|
+
if isinstance(f, DslElement):
|
177
|
+
excludes.append(f == element)
|
178
|
+
else:
|
179
|
+
excludes.append(f(workspace, Element(element)))
|
180
|
+
if any(includes) and not any(excludes):
|
160
181
|
filtered_elements.append(element)
|
161
182
|
|
162
183
|
return filtered_elements
|
@@ -179,30 +200,47 @@ class Expression:
|
|
179
200
|
def _is_relationship_of_excluded_elements(
|
180
201
|
workspace: Workspace,
|
181
202
|
relationship: Relationship,
|
182
|
-
exclude_element_predicates: Iterable[Callable[[Workspace, Element], bool]],
|
203
|
+
exclude_element_predicates: Iterable[Union[DslElement, Callable[[Workspace, Element], bool]]],
|
183
204
|
) -> bool:
|
184
|
-
|
185
|
-
f
|
186
|
-
|
187
|
-
|
188
|
-
|
205
|
+
for f in exclude_element_predicates:
|
206
|
+
if isinstance(f, DslElement):
|
207
|
+
if f == relationship.source or f == relationship.destination:
|
208
|
+
return True
|
209
|
+
else:
|
210
|
+
if f(workspace, relationship.source) or f(workspace, relationship.destination):
|
211
|
+
return True
|
212
|
+
return False
|
189
213
|
|
190
214
|
workspace_relationships = buildzr.dsl.Explorer(workspace).walk_relationships()
|
215
|
+
|
191
216
|
for relationship in workspace_relationships:
|
192
|
-
any_includes = any([f(workspace, Relationship(relationship)) for f in self._include_relationships])
|
193
217
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
218
|
+
includes: List[bool] = []
|
219
|
+
excludes: List[bool] = []
|
220
|
+
|
221
|
+
for f in self._include_relationships:
|
222
|
+
if isinstance(f, DslElement):
|
223
|
+
includes.append(f == relationship)
|
224
|
+
else:
|
225
|
+
includes.append(f(workspace, Relationship(relationship)))
|
226
|
+
|
227
|
+
for f in self._exclude_relationships:
|
228
|
+
if isinstance(f, DslElement):
|
229
|
+
excludes.append(f == relationship)
|
230
|
+
else:
|
231
|
+
excludes.append(f(workspace, Relationship(relationship)))
|
232
|
+
|
233
|
+
# Also exclude relationships whose source or destination elements
|
234
|
+
# are excluded.
|
235
|
+
excludes.append(
|
199
236
|
_is_relationship_of_excluded_elements(
|
200
237
|
workspace,
|
201
238
|
Relationship(relationship),
|
202
239
|
self._exclude_elements,
|
203
240
|
)
|
204
|
-
|
205
|
-
|
241
|
+
)
|
242
|
+
|
243
|
+
if any(includes) and not any(excludes):
|
206
244
|
filtered_relationships.append(relationship)
|
207
245
|
|
208
246
|
return filtered_relationships
|
@@ -11,6 +11,7 @@ from typing import (
|
|
11
11
|
Callable,
|
12
12
|
overload,
|
13
13
|
Sequence,
|
14
|
+
MutableSet,
|
14
15
|
cast,
|
15
16
|
)
|
16
17
|
from typing_extensions import (
|
@@ -29,9 +30,6 @@ Model = Union[
|
|
29
30
|
TSrc = TypeVar('TSrc', bound='DslElement', contravariant=True)
|
30
31
|
TDst = TypeVar('TDst', bound='DslElement', contravariant=True)
|
31
32
|
|
32
|
-
TParent = TypeVar('TParent', bound=Union['DslWorkspaceElement', 'DslElement'], covariant=True)
|
33
|
-
TChild = TypeVar('TChild', bound='DslElement', contravariant=True)
|
34
|
-
|
35
33
|
class BindLeftLate(ABC, Generic[TDst]):
|
36
34
|
|
37
35
|
@abstractmethod
|
@@ -128,11 +126,24 @@ class DslElement(BindRight[TSrc, TDst]):
|
|
128
126
|
def destinations(self) -> List['DslElement']:
|
129
127
|
pass
|
130
128
|
|
129
|
+
@property
|
130
|
+
@abstractmethod
|
131
|
+
def relationships(self) -> MutableSet['DslRelationship']:
|
132
|
+
pass
|
133
|
+
|
131
134
|
@property
|
132
135
|
@abstractmethod
|
133
136
|
def tags(self) -> Set[str]:
|
134
137
|
pass
|
135
138
|
|
139
|
+
def add_tags(self, *tags: str) -> None:
|
140
|
+
"""
|
141
|
+
Add tags to the element.
|
142
|
+
"""
|
143
|
+
self.tags.update(tags)
|
144
|
+
if not isinstance(self.model, buildzr.models.Workspace):
|
145
|
+
self.model.tags = ','.join(self.tags)
|
146
|
+
|
136
147
|
def uses(
|
137
148
|
self,
|
138
149
|
other: 'DslElement',
|
@@ -170,41 +181,16 @@ class DslRelationship(ABC, Generic[TSrc, TDst]):
|
|
170
181
|
def destination(self) -> DslElement:
|
171
182
|
pass
|
172
183
|
|
184
|
+
def add_tags(self, *tags: str) -> None:
|
185
|
+
"""
|
186
|
+
Adds tags to the relationship.
|
187
|
+
"""
|
188
|
+
self.tags.update(tags)
|
189
|
+
self.model.tags = ','.join(self.tags)
|
190
|
+
|
173
191
|
def __contains__(self, other: 'DslElement') -> bool:
|
174
192
|
return self.source.model.id == other.model.id or self.destination.model.id == other.model.id
|
175
193
|
|
176
|
-
class DslFluentRelationship(ABC, Generic[TParent]):
|
177
|
-
|
178
|
-
"""
|
179
|
-
The abstract class that defines the interface for the fluent relationship
|
180
|
-
definition, where the one or more relationships between elements accessible
|
181
|
-
from the `TParent` element.
|
182
|
-
"""
|
183
|
-
|
184
|
-
@abstractmethod
|
185
|
-
def where(
|
186
|
-
self,
|
187
|
-
func: Callable[
|
188
|
-
[TParent],
|
189
|
-
Sequence[
|
190
|
-
Union[
|
191
|
-
DslRelationship,
|
192
|
-
Sequence[DslRelationship]
|
193
|
-
]
|
194
|
-
]
|
195
|
-
]) -> TParent:
|
196
|
-
pass
|
197
|
-
|
198
|
-
@abstractmethod
|
199
|
-
def get(self) -> TParent:
|
200
|
-
pass
|
201
|
-
|
202
|
-
class DslFluentSink(ABC):
|
203
|
-
|
204
|
-
@abstractmethod
|
205
|
-
def to_json(self, path: str) -> None:
|
206
|
-
pass
|
207
|
-
|
208
194
|
class DslViewElement(ABC):
|
209
195
|
|
210
196
|
ViewModel = Union[
|
@@ -242,8 +228,8 @@ class DslViewsElement(ABC):
|
|
242
228
|
pass
|
243
229
|
|
244
230
|
@abstractmethod
|
245
|
-
def
|
231
|
+
def add_views(
|
246
232
|
self,
|
247
233
|
*views: Union[DslViewElement],
|
248
|
-
) ->
|
234
|
+
) -> None:
|
249
235
|
pass
|
buildzr/dsl/relations.py
CHANGED
@@ -18,33 +18,13 @@ from buildzr.dsl.interfaces import (
|
|
18
18
|
BindLeft,
|
19
19
|
BindLeftLate,
|
20
20
|
DslRelationship,
|
21
|
-
DslFluentRelationship,
|
22
21
|
DslElement,
|
23
22
|
DslWorkspaceElement,
|
24
23
|
TSrc, TDst,
|
25
|
-
TParent, TChild,
|
26
24
|
)
|
27
25
|
from buildzr.dsl.factory import GenerateId
|
28
26
|
import buildzr
|
29
27
|
|
30
|
-
def _is_software_fluent_relationship(
|
31
|
-
obj: '_FluentRelationship[Any]'
|
32
|
-
) -> TypeIs['_FluentRelationship[buildzr.dsl.SoftwareSystem]']:
|
33
|
-
return isinstance(obj._parent, buildzr.dsl.SoftwareSystem)
|
34
|
-
|
35
|
-
def _is_container_fluent_relationship(
|
36
|
-
obj: '_FluentRelationship[Any]'
|
37
|
-
) -> TypeIs['_FluentRelationship[buildzr.dsl.Container]']:
|
38
|
-
return isinstance(obj._parent, buildzr.dsl.Container)
|
39
|
-
|
40
|
-
def _is_list_of_dslelements_or_usesfromlates(
|
41
|
-
obj: list
|
42
|
-
) -> TypeIs[List[Union[DslElement, '_UsesFromLate[DslElement]']]]:
|
43
|
-
return all(
|
44
|
-
isinstance(item, DslElement) or isinstance(item, _UsesFromLate)
|
45
|
-
for item in obj
|
46
|
-
)
|
47
|
-
|
48
28
|
@dataclass
|
49
29
|
class With:
|
50
30
|
tags: Optional[Set[str]] = None
|
@@ -115,8 +95,17 @@ class _Relationship(DslRelationship[TSrc, TDst]):
|
|
115
95
|
uses_data.relationship.destinationId = str(destination.model.id)
|
116
96
|
|
117
97
|
if not isinstance(uses_data.source.model, buildzr.models.Workspace):
|
118
|
-
|
119
|
-
|
98
|
+
|
99
|
+
# Prevent any duplicate sources/destinations, especially when creating implied relationships.
|
100
|
+
if not any([self._dst.model.id == dest.model.id for dest in uses_data.source.destinations]):
|
101
|
+
uses_data.source.destinations.append(self._dst)
|
102
|
+
if not any([self._src.model.id == src.model.id for src in self._dst.sources]):
|
103
|
+
self._dst.sources.append(self._src)
|
104
|
+
|
105
|
+
# Make this relationship accessible from the source element.
|
106
|
+
if not any([r.model.id == self.model.id for r in self._src.relationships]):
|
107
|
+
self._src.relationships.add(self)
|
108
|
+
|
120
109
|
if _include_in_model:
|
121
110
|
if uses_data.source.model.relationships:
|
122
111
|
uses_data.source.model.relationships.append(uses_data.relationship)
|
@@ -155,60 +144,6 @@ class _Relationship(DslRelationship[TSrc, TDst]):
|
|
155
144
|
self._ref[0].relationship.url = url
|
156
145
|
return self
|
157
146
|
|
158
|
-
class _FluentRelationship(DslFluentRelationship[TParent]):
|
159
|
-
|
160
|
-
"""
|
161
|
-
A hidden class used in the fluent DSL syntax after specifying a model (i.e.,
|
162
|
-
Person, Software System, Container) to define relationship(s) within the
|
163
|
-
specified model.
|
164
|
-
"""
|
165
|
-
|
166
|
-
def __init__(self, parent: TParent) -> None:
|
167
|
-
self._parent: TParent = parent
|
168
|
-
|
169
|
-
def where(
|
170
|
-
self,
|
171
|
-
func: Callable[
|
172
|
-
[TParent],
|
173
|
-
Sequence[
|
174
|
-
Union[
|
175
|
-
DslRelationship,
|
176
|
-
Sequence[DslRelationship]
|
177
|
-
]
|
178
|
-
]
|
179
|
-
], implied: bool=False) -> TParent:
|
180
|
-
|
181
|
-
relationships: Sequence[DslRelationship] = []
|
182
|
-
|
183
|
-
func = cast(Callable[[TParent], Sequence[Union[DslRelationship, Sequence[DslRelationship]]]], func)
|
184
|
-
|
185
|
-
# Flatten the resulting relationship list.
|
186
|
-
relationships = [
|
187
|
-
rel for sublist in func(self._parent)
|
188
|
-
for rel in (
|
189
|
-
sublist if isinstance(sublist, list) else [sublist]
|
190
|
-
)
|
191
|
-
]
|
192
|
-
|
193
|
-
# If we have relationship s >> do >> a.b, then create s >> do >> a.
|
194
|
-
# If we have relationship s.ss >> do >> a.b.c, then create s.ss >> do >> a.b and s.ss >> do >> a.
|
195
|
-
# And so on...
|
196
|
-
if implied:
|
197
|
-
for relationship in relationships:
|
198
|
-
source = relationship.source
|
199
|
-
parent = relationship.destination.parent
|
200
|
-
while parent is not None and not isinstance(parent, DslWorkspaceElement):
|
201
|
-
r = source.uses(parent, description=relationship.model.description, technology=relationship.model.technology)
|
202
|
-
r.model.linkedRelationshipId = relationship.model.id
|
203
|
-
parent = parent.parent
|
204
|
-
|
205
|
-
return self._parent
|
206
|
-
|
207
|
-
# TODO: Remove this and replace with something better.
|
208
|
-
# Doesn't feel "fluent."
|
209
|
-
def get(self) -> TParent:
|
210
|
-
return self._parent
|
211
|
-
|
212
147
|
class _RelationshipDescription(Generic[TDst]):
|
213
148
|
|
214
149
|
def __init__(self, description: str, technology: Optional[str]=None) -> None:
|
buildzr/encoders/encoder.py
CHANGED
@@ -3,7 +3,7 @@ import dataclasses, json
|
|
3
3
|
import enum
|
4
4
|
import humps
|
5
5
|
from buildzr.dsl.interfaces import DslElement, DslWorkspaceElement
|
6
|
-
from typing import Union, List, TYPE_CHECKING, Type, Any
|
6
|
+
from typing import Union, List, TYPE_CHECKING, Type, Any, Dict, cast
|
7
7
|
from typing_extensions import TypeGuard
|
8
8
|
|
9
9
|
if TYPE_CHECKING:
|
@@ -47,15 +47,35 @@ class JsonEncoder(json.JSONEncoder):
|
|
47
47
|
def default(self, obj: JsonEncodable) -> Union[str, list, dict]:
|
48
48
|
# Handle the default encoder the nicely wrapped DSL elements.
|
49
49
|
if isinstance(obj, DslElement) or isinstance(obj, DslWorkspaceElement):
|
50
|
-
return humps.camelize(_remove_nones(dataclasses.asdict(obj.model)))
|
50
|
+
return cast(Union[str, list, dict], humps.camelize(_remove_nones(dataclasses.asdict(obj.model))))
|
51
51
|
|
52
52
|
# Handle the default encoder for those `dataclass`es models generated in
|
53
53
|
# `buildzr.model`
|
54
54
|
elif _is_dataclass(obj):
|
55
|
-
|
55
|
+
d = dataclasses.asdict(obj)
|
56
|
+
# Special handling for 'properties' fields of type Dict[str, Any]
|
57
|
+
if 'properties' in d and isinstance(d['properties'], dict):
|
58
|
+
d['properties'] = self._encode_properties(d['properties'])
|
59
|
+
return cast(Union[str, list, dict], humps.camelize(_remove_nones(d)))
|
56
60
|
|
57
61
|
# Handle the enums
|
58
62
|
elif isinstance(obj, enum.Enum):
|
59
63
|
return str(obj.value)
|
60
64
|
|
61
|
-
return super().default(obj) #type: ignore[no-any-return]
|
65
|
+
return super().default(obj) #type: ignore[no-any-return]
|
66
|
+
|
67
|
+
def _encode_properties(self, props: dict) -> dict:
|
68
|
+
# Recursively encode values in the properties dict
|
69
|
+
result: Dict[str, Any] = {}
|
70
|
+
for k, v in props.items():
|
71
|
+
if _is_dataclass(v):
|
72
|
+
result[k] = humps.camelize(_remove_nones(dataclasses.asdict(v)))
|
73
|
+
elif isinstance(v, enum.Enum):
|
74
|
+
result[k] = str(v.value)
|
75
|
+
elif isinstance(v, dict):
|
76
|
+
result[k] = self._encode_properties(v)
|
77
|
+
elif isinstance(v, list):
|
78
|
+
result[k] = [self._encode_properties(i) if isinstance(i, dict) else i for i in v]
|
79
|
+
else:
|
80
|
+
result[k] = v
|
81
|
+
return result
|
@@ -1,10 +1,11 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: buildzr
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.9
|
4
4
|
Summary: Structurizr for the `buildzr`s 🧱⚒️
|
5
5
|
Project-URL: homepage, https://github.com/amirulmenjeni/buildzr
|
6
6
|
Project-URL: issues, https://github.com/amirulmenjeni/buildzr/issues
|
7
7
|
Author-email: Amirul Menjeni <amirulmenjeni@pm.me>
|
8
|
+
License-File: LICENSE.md
|
8
9
|
Keywords: architecture,buildzr,c4model,design,diagram,structurizr
|
9
10
|
Classifier: Development Status :: 3 - Alpha
|
10
11
|
Classifier: License :: OSI Approved :: MIT License
|
@@ -29,13 +30,11 @@ Description-Content-Type: text/markdown
|
|
29
30
|
|
30
31
|
# Structurizr for the `buildzr`s 🧱⚒️
|
31
32
|
|
32
|
-
`buildzr` is a [Structurizr](https://structurizr.com/) authoring tool for Python programmers.
|
33
|
+
`buildzr` is a [Structurizr](https://structurizr.com/) authoring tool for Python programmers. It allows you to declaratively or procedurally author Structurizr models and diagrams.
|
33
34
|
|
34
|
-
If you're not familiar with Structurizr, it is both an open standard (see [Structurizr JSON schema](https://github.com/structurizr/json)) and a [set of tools](https://docs.structurizr.com/usage) for building software architecture diagrams as code. Structurizr derive its architecture modeling paradigm based on the [C4 model](https://c4model.com/), the modeling language for
|
35
|
+
If you're not familiar with Structurizr, it is both an open standard (see [Structurizr JSON schema](https://github.com/structurizr/json)) and a [set of tools](https://docs.structurizr.com/usage) for building software architecture diagrams as code. Structurizr derive its architecture modeling paradigm based on the [C4 model](https://c4model.com/), the modeling language for describing software architectures and the relationships.
|
35
36
|
|
36
|
-
|
37
|
-
leveraging the standard Structurizr JSON schema for interoperability with
|
38
|
-
various rendering and authoring tools.
|
37
|
+
In Structurizr, you define architecture models and their relationships first. And then, you can re-use the models to present multiple perspectives, views, and stories about your architecture.
|
39
38
|
|
40
39
|
# Quick Start 🚀
|
41
40
|
|
@@ -52,15 +51,10 @@ pip install buildzr
|
|
52
51
|
The module `buildzr.dsl` contains all the classes you need to create a workspace containing all the architecture models.
|
53
52
|
|
54
53
|
Below is an example, where we:
|
55
|
-
1. Create the models (`Person`, `SoftwareSystem`s, the `Container`s inside the `SoftwareSystem`, and the relationships between them)
|
54
|
+
1. Create the models (`Person`, `SoftwareSystem`s, the `Container`s inside the `SoftwareSystem`, and the relationships -- `>>` -- between them)
|
56
55
|
2. Define multiple views using the models we've created before.
|
57
56
|
|
58
57
|
```python
|
59
|
-
import os
|
60
|
-
import json
|
61
|
-
|
62
|
-
from buildzr.encoders import JsonEncoder
|
63
|
-
|
64
58
|
from buildzr.dsl import (
|
65
59
|
Workspace,
|
66
60
|
SoftwareSystem,
|
@@ -72,74 +66,65 @@ from buildzr.dsl import (
|
|
72
66
|
Group,
|
73
67
|
)
|
74
68
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
),
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
),
|
111
|
-
ContainerView(
|
112
|
-
lambda w: w.software_system().webapp,
|
113
|
-
key='web_app_container_view_00',
|
114
|
-
auto_layout='lr',
|
115
|
-
description="Web App Container View",
|
116
|
-
)
|
117
|
-
)\
|
118
|
-
.get_workspace()
|
119
|
-
|
120
|
-
# Save workspace to a JSON file following the Structurizr JSON schema.
|
121
|
-
w.to_json('workspace.json')
|
69
|
+
with Workspace('w') as w:
|
70
|
+
with Group("My Company"):
|
71
|
+
u = Person('Web Application User')
|
72
|
+
webapp = SoftwareSystem('Corporate Web App')
|
73
|
+
with webapp:
|
74
|
+
database = Container('database')
|
75
|
+
api = Container('api')
|
76
|
+
api >> ("Reads and writes data from/to", "http/api") >> database
|
77
|
+
with Group("Microsoft"):
|
78
|
+
email_system = SoftwareSystem('Microsoft 365')
|
79
|
+
|
80
|
+
u >> [
|
81
|
+
desc("Reads and writes email using") >> email_system,
|
82
|
+
desc("Create work order using") >> webapp,
|
83
|
+
]
|
84
|
+
webapp >> "sends notification using" >> email_system
|
85
|
+
|
86
|
+
SystemContextView(
|
87
|
+
software_system_selector=webapp,
|
88
|
+
key='web_app_system_context_00',
|
89
|
+
description="Web App System Context",
|
90
|
+
auto_layout='lr',
|
91
|
+
exclude_elements=[
|
92
|
+
u,
|
93
|
+
]
|
94
|
+
)
|
95
|
+
|
96
|
+
ContainerView(
|
97
|
+
software_system_selector=webapp,
|
98
|
+
key='web_app_container_view_00',
|
99
|
+
auto_layout='lr',
|
100
|
+
description="Web App Container View",
|
101
|
+
)
|
102
|
+
|
103
|
+
w.to_json('workspace.json')
|
122
104
|
```
|
123
105
|
|
124
106
|
Here's a short breakdown on what's happening:
|
125
|
-
- In `Workspace(
|
126
|
-
-
|
127
|
-
-
|
107
|
+
- In the `with Workspace('w') as w:` block, we've created a `Workspace` named `w`.
|
108
|
+
- Inside this block, we're in the context of the `w` workspace, so any models, relationships, and views we declared in this block will belong to that workspace. We've declared a few models: `Person`, the `SoftwareSystems`, their `Container`s, and their relationships with each other. Oh, we've separated them into different `Group`s, too!
|
109
|
+
- Showing the who's and the what's in an architecture model is good, but an architecture model is incomplete without the arrows that describes the relationships between the systems. In `buildzr`, we can define relationships with the `>>` operator.
|
110
|
+
- Once we have all the models and their relationships defined, we use (and re-use!) the static models to create multiple views to tell different stories and show various narrative to help document your software architecture. In this case, we've created one `SystemContextView` and one `ContainerView` for the `webapp`.
|
128
111
|
- Finally, we write the workspace definitions into a JSON file, which can be consumed by rendering tools, or used for further processing.
|
129
112
|
|
130
113
|
The JSON output can be found [here](examples/system_context_and_container_view.json). You can also try out https://structurizr.com/json to see how this workspace will be rendered.
|
131
114
|
|
132
115
|
# Why use `buildzr`?
|
133
116
|
|
134
|
-
✅ Uses
|
117
|
+
✅ Uses `buildzr`'s declarative DSL syntax to help you create C4 model architecture diagrams in Python concisely.
|
118
|
+
|
119
|
+
✅ Uses `buildzr`'s DSL APIs to programmatically create C4 model architecture diagrams. Good if you need to automate things!
|
135
120
|
|
136
121
|
✅ Write Structurizr diagrams more securely with extensive type hints and [mypy](https://mypy-lang.org) support.
|
137
122
|
|
138
123
|
✅ Stays true to the [Structurizr JSON schema](https://mypy-lang.org/) standards. `buildzr` uses the [datamodel-code-generator](https://github.com/koxudaxi/datamodel-code-generator) to automatically generate the "low-level" [representation](buildzr/models/models.py) of the Workspace model. This reduces deprecancy between `buildzr` and the Structurizr JSON schema.
|
139
124
|
|
140
|
-
✅ Writing architecture diagrams in Python allows you to integrate programmability and automation into your software architecture diagramming and documentation workflow.
|
125
|
+
✅ Writing architecture models and diagrams in Python allows you to integrate programmability and automation into your software architecture diagramming and documentation workflow. For example, you might want to programmatically automate the creation of architecture models from metadata pulled from your IT asset management system, but still want to declaratively define how to present them.
|
141
126
|
|
142
|
-
✅ Uses the familiar Python programming language to write software architecture diagrams!
|
127
|
+
✅ Uses the familiar Python programming language and its rich toolchains to write software architecture models and diagrams!
|
143
128
|
|
144
129
|
# Contributing
|
145
130
|
|
@@ -0,0 +1,24 @@
|
|
1
|
+
buildzr/__about__.py,sha256=DgAnctESS7a-r7bKUfHvaK2XtP5BpQpkO_wLESIfHjc,17
|
2
|
+
buildzr/__init__.py,sha256=hY-cOdjBQcz0v2m8cBF1oEJFIbcR3sWI-xww--0RKSo,99
|
3
|
+
buildzr/dsl/__init__.py,sha256=k0G9blhA3NSzCBs1dSfBNzCh0iDS_ylkzS_5a3pqrSY,375
|
4
|
+
buildzr/dsl/color.py,sha256=at5lo3WgLEDCjrnbu37ra1p1TjzdB51sxeW7pBMC_7U,4019
|
5
|
+
buildzr/dsl/dsl.py,sha256=47_O2OqN6XR7MNZnmJK2EaX8JPZrGu-h5YEVEaLUwdM,54154
|
6
|
+
buildzr/dsl/explorer.py,sha256=vLAgQEYd0h22QuVfWfBdk4zyDpGaE1T67Pn9V7P1C-I,1238
|
7
|
+
buildzr/dsl/expression.py,sha256=EMAtaZQA0O_zwENCZs3l4u_w9wUuO09XA0LgnvsomfA,8256
|
8
|
+
buildzr/dsl/relations.py,sha256=GBs5epr9uuExU_H6VcP4XY76iJPQ__rz_d8tZlhhWQ4,11891
|
9
|
+
buildzr/dsl/factory/__init__.py,sha256=niaYqvNPUWJejoPyRyABUtzVsoxaV8eSjzS9dta4bMI,30
|
10
|
+
buildzr/dsl/factory/gen_id.py,sha256=LnaeOCMngSvYkcGnuARjQYoUVWdcOoNHO2EHe6PMGco,538
|
11
|
+
buildzr/dsl/interfaces/__init__.py,sha256=ncYARIPB4ARcCCRObgV1b4jluEubpm2s46mp1ZC8Urk,197
|
12
|
+
buildzr/dsl/interfaces/interfaces.py,sha256=j8UCPCRegmFZ2Jl4qCz0uW0OxBS2mRe0VjPcNExH8mc,5553
|
13
|
+
buildzr/encoders/__init__.py,sha256=suID63Ay-023T0uKD25EAoGYmAMTa9AKxIjioccpiPM,32
|
14
|
+
buildzr/encoders/encoder.py,sha256=n8WLVMrisykBTLTr1z6PAxgqhqW2dFRZhSupOuMdx_A,3235
|
15
|
+
buildzr/models/__init__.py,sha256=SRfF7oDVlOOAi6nGKiJIUK6B_arqYLO9iSMp-2IZZps,21
|
16
|
+
buildzr/models/generate.sh,sha256=924UoEXr5WBZ926TZfRgEQGHwBqtLGU5k0G2UfExLEg,1061
|
17
|
+
buildzr/models/models.py,sha256=0LhLG1wmbt4dvROV5MEBZLLoxPbMpkUsOqNz525cynE,42489
|
18
|
+
buildzr/sinks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
19
|
+
buildzr/sinks/interfaces.py,sha256=LOZekP4WNjomD5J5f3FnZTwGj0aXMr6RbrvyFV5zn0E,383
|
20
|
+
buildzr/sinks/json_sink.py,sha256=onKOZTpwOQfeMEj1ONkuIEHBAQhx4yQSqqI_lgZBaP8,777
|
21
|
+
buildzr-0.0.9.dist-info/METADATA,sha256=qjmMwahyMKpFXfFQZj4p_j_X0VVC-j3Lknwm_7NRJLQ,6578
|
22
|
+
buildzr-0.0.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
23
|
+
buildzr-0.0.9.dist-info/licenses/LICENSE.md,sha256=e8e6W6tL4MbBY-c-gXMgDbaMf_BnaQDQv4Yoy42b-CI,1070
|
24
|
+
buildzr-0.0.9.dist-info/RECORD,,
|