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/dsl/dsl.py CHANGED
@@ -3,9 +3,10 @@ import buildzr
3
3
  from .factory import GenerateId
4
4
  from typing_extensions import (
5
5
  Self,
6
- TypeGuard,
7
6
  TypeIs,
8
7
  )
8
+ from collections import deque
9
+ from contextvars import ContextVar
9
10
  from typing import (
10
11
  Any,
11
12
  Union,
@@ -16,35 +17,26 @@ from typing import (
16
17
  Optional,
17
18
  Generic,
18
19
  TypeVar,
19
- Protocol,
20
20
  Callable,
21
21
  Iterable,
22
22
  Literal,
23
23
  cast,
24
- overload,
25
- Sequence,
26
24
  Type,
27
25
  )
28
26
 
29
27
  from buildzr.sinks.interfaces import Sink
30
-
31
28
  from buildzr.dsl.interfaces import (
32
29
  DslWorkspaceElement,
33
30
  DslElement,
34
31
  DslViewElement,
35
32
  DslViewsElement,
36
- DslFluentSink,
37
- TSrc, TDst,
38
- TParent, TChild,
39
33
  )
40
34
  from buildzr.dsl.relations import (
41
- _is_software_fluent_relationship,
42
- _is_container_fluent_relationship,
43
- _Relationship,
44
- _RelationshipDescription,
45
- _FluentRelationship,
46
35
  DslElementRelationOverrides,
36
+ DslRelationship,
37
+ _Relationship,
47
38
  )
39
+ from buildzr.dsl.color import Color
48
40
 
49
41
  def _child_name_transform(name: str) -> str:
50
42
  return name.lower().replace(' ', '_')
@@ -58,6 +50,11 @@ class TypedDynamicAttribute(Generic[TypedModel]):
58
50
  def __getattr__(self, name: str) -> TypedModel:
59
51
  return cast(TypedModel, self._dynamic_attributes.get(name))
60
52
 
53
+ _current_workspace: ContextVar[Optional['Workspace']] = ContextVar('current_workspace', default=None)
54
+ _current_group_stack: ContextVar[List['Group']] = ContextVar('current_group', default=[])
55
+ _current_software_system: ContextVar[Optional['SoftwareSystem']] = ContextVar('current_software_system', default=None)
56
+ _current_container: ContextVar[Optional['Container']] = ContextVar('current_container', default=None)
57
+
61
58
  class Workspace(DslWorkspaceElement):
62
59
  """
63
60
  Represents a Structurizr workspace, which is a wrapper for a software architecture model, views, and documentation.
@@ -75,11 +72,21 @@ class Workspace(DslWorkspaceElement):
75
72
  def children(self) -> Optional[List[Union['Person', 'SoftwareSystem']]]:
76
73
  return self._children
77
74
 
78
- def __init__(self, name: str, description: str="", scope: Literal['landscape', 'software_system', None]='software_system') -> None:
75
+ def __init__(
76
+ self,
77
+ name: str,
78
+ description: str="",
79
+ scope: Literal['landscape', 'software_system', None]='software_system',
80
+ implied_relationships: bool=False,
81
+ group_separator: str='/',
82
+ ) -> None:
83
+
79
84
  self._m = buildzr.models.Workspace()
80
85
  self._parent = None
81
86
  self._children: Optional[List[Union['Person', 'SoftwareSystem']]] = []
82
87
  self._dynamic_attrs: Dict[str, Union['Person', 'SoftwareSystem']] = {}
88
+ self._use_implied_relationships = implied_relationships
89
+ self._group_separator = group_separator
83
90
  self.model.id = GenerateId.for_workspace()
84
91
  self.model.name = name
85
92
  self.model.description = description
@@ -102,58 +109,57 @@ class Workspace(DslWorkspaceElement):
102
109
  scope=scope_mapper[scope],
103
110
  )
104
111
 
105
- def _contains_group(
106
- self,
107
- name: str,
108
- *models: Union[
109
- 'Person',
110
- 'SoftwareSystem',
111
- _FluentRelationship['SoftwareSystem'],
112
- _FluentRelationship['Container'],
113
- ]
114
- ) -> None:
112
+ self.model.model.properties = {
113
+ 'structurizr.groupSeparator': group_separator,
114
+ }
115
115
 
116
- def recursive_group_name_assign(software_system: 'SoftwareSystem') -> None:
117
- software_system.model.group = name
118
- for container in software_system.children:
119
- container.model.group = name
120
- for component in container.children:
121
- component.model.group = name
122
-
123
- for model in models:
124
- if isinstance(model, Person):
125
- model.model.group = name
126
- elif isinstance(model, SoftwareSystem):
127
- recursive_group_name_assign(model)
128
- elif _is_software_fluent_relationship(model):
129
- recursive_group_name_assign(model._parent)
130
- elif _is_container_fluent_relationship(model):
131
- recursive_group_name_assign(model._parent._parent)
132
-
133
- self.contains(*models)
134
-
135
- def contains(
136
- self,
137
- *models: Union[
138
- 'Group',
139
- 'Person',
140
- 'SoftwareSystem',
141
- _FluentRelationship['SoftwareSystem'],
142
- _FluentRelationship['Container'],
143
- ]) -> _FluentRelationship['Workspace']:
144
-
145
- for model in models:
146
- if isinstance(model, Group):
147
- self._contains_group(model._name, *model._elements)
148
- elif isinstance(model, Person):
149
- self.add_element(model)
150
- elif isinstance(model, SoftwareSystem):
151
- self.add_element(model)
152
- elif _is_software_fluent_relationship(model):
153
- self.add_element(model._parent)
154
- elif _is_container_fluent_relationship(model):
155
- self.add_element(model._parent._parent)
156
- return _FluentRelationship['Workspace'](self)
116
+ def __enter__(self) -> Self:
117
+ self._token = _current_workspace.set(self)
118
+ return self
119
+
120
+ def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[Any]) -> None:
121
+
122
+ from buildzr.dsl.explorer import Explorer
123
+
124
+ # Process implied relationships:
125
+ # If we have relationship s >> do >> a.b, then create s >> do >> a.
126
+ # If we have relationship s.ss >> do >> a.b.c, then create s.ss >> do >> a.b and s.ss >> do >> a.
127
+ # And so on...
128
+ if self._use_implied_relationships:
129
+ explorer = Explorer(self)
130
+ relationships = list(explorer.walk_relationships())
131
+ for relationship in relationships:
132
+ source = relationship.source
133
+ destination = relationship.destination
134
+ destination_parent = destination.parent
135
+
136
+ while destination_parent is not None and \
137
+ isinstance(source, DslElement) and \
138
+ not isinstance(source.model, buildzr.models.Workspace) and \
139
+ not isinstance(destination_parent, DslWorkspaceElement):
140
+
141
+ if destination_parent is source.parent:
142
+ break
143
+
144
+ rels = source.model.relationships
145
+
146
+ if rels:
147
+ already_exists = any(
148
+ r.destinationId == destination_parent.model.id and
149
+ r.description == relationship.model.description and
150
+ r.technology == relationship.model.technology
151
+ for r in rels
152
+ )
153
+ if not already_exists:
154
+ r = source.uses(
155
+ destination_parent,
156
+ description=relationship.model.description,
157
+ technology=relationship.model.technology,
158
+ )
159
+ r.model.linkedRelationshipId = relationship.model.id
160
+ destination_parent = destination_parent.parent
161
+
162
+ _current_workspace.reset(self._token)
157
163
 
158
164
  def person(self) -> TypedDynamicAttribute['Person']:
159
165
  return TypedDynamicAttribute['Person'](self._dynamic_attrs)
@@ -161,34 +167,68 @@ class Workspace(DslWorkspaceElement):
161
167
  def software_system(self) -> TypedDynamicAttribute['SoftwareSystem']:
162
168
  return TypedDynamicAttribute['SoftwareSystem'](self._dynamic_attrs)
163
169
 
164
- def add_element(self, element: Union['Person', 'SoftwareSystem']) -> None:
165
- if isinstance(element, Person):
166
- self._m.model.people.append(element._m)
167
- element._parent = self
168
- self._dynamic_attrs[_child_name_transform(element.model.name)] = element
169
- if element._label:
170
- self._dynamic_attrs[_child_name_transform(element._label)] = element
171
- self._children.append(element)
172
- elif isinstance(element, SoftwareSystem):
173
- self._m.model.softwareSystems.append(element._m)
174
- element._parent = self
175
- self._dynamic_attrs[_child_name_transform(element.model.name)] = element
176
- if element._label:
177
- self._dynamic_attrs[_child_name_transform(element._label)] = element
178
- self._children.append(element)
170
+ def add_model(self, model: Union['Person', 'SoftwareSystem']) -> None:
171
+ if isinstance(model, Person):
172
+ self._m.model.people.append(model._m)
173
+ model._parent = self
174
+ self._add_dynamic_attr(model.model.name, model)
175
+ self._children.append(model)
176
+ elif isinstance(model, SoftwareSystem):
177
+ self._m.model.softwareSystems.append(model._m)
178
+ model._parent = self
179
+ self._add_dynamic_attr(model.model.name, model)
180
+ self._children.append(model)
179
181
  else:
180
- raise ValueError('Invalid element type: Trying to add an element of type {} to a workspace.'.format(type(element)))
182
+ raise ValueError('Invalid element type: Trying to add an element of type {} to a workspace.'.format(type(model)))
181
183
 
182
- def with_views(
183
- self,
184
- *views: Union[
185
- 'SystemLandscapeView',
184
+ def apply_views( self, *views: Union[ 'SystemLandscapeView',
186
185
  'SystemContextView',
187
186
  'ContainerView',
188
187
  'ComponentView',
189
188
  ]
190
- ) -> '_FluentSink':
191
- return Views(self).contains(*views)
189
+ ) -> None:
190
+ Views(self).add_views(*views)
191
+
192
+ def apply_style( self,
193
+ style: Union['StyleElements', 'StyleRelationships'],
194
+ ) -> None:
195
+
196
+ style._parent = self
197
+
198
+ if not self.model.views:
199
+ self.model.views = buildzr.models.Views()
200
+ if not self.model.views.configuration:
201
+ self.model.views.configuration = buildzr.models.Configuration()
202
+ if not self.model.views.configuration.styles:
203
+ self.model.views.configuration.styles = buildzr.models.Styles()
204
+
205
+ if isinstance(style, StyleElements):
206
+ if self.model.views.configuration.styles.elements:
207
+ self.model.views.configuration.styles.elements.extend(style.model)
208
+ else:
209
+ self.model.views.configuration.styles.elements = style.model
210
+ elif isinstance(style, StyleRelationships):
211
+ if self.model.views.configuration.styles.relationships:
212
+ self.model.views.configuration.styles.relationships.extend(style.model)
213
+ else:
214
+ self.model.views.configuration.styles.relationships = style.model
215
+
216
+ def to_json(self, path: str) -> None:
217
+ from buildzr.sinks.json_sink import JsonSink, JsonSinkConfig
218
+ sink = JsonSink()
219
+ sink.write(workspace=self.model, config=JsonSinkConfig(path=path))
220
+
221
+ def _add_dynamic_attr(self, name: str, model: Union['Person', 'SoftwareSystem']) -> None:
222
+ if isinstance(model, Person):
223
+ self._dynamic_attrs[_child_name_transform(name)] = model
224
+ if model._label:
225
+ self._dynamic_attrs[_child_name_transform(model._label)] = model
226
+ elif isinstance(model, SoftwareSystem):
227
+ self._dynamic_attrs[_child_name_transform(name)] = model
228
+ if model._label:
229
+ self._dynamic_attrs[_child_name_transform(model._label)] = model
230
+ else:
231
+ raise ValueError('Invalid element type: Trying to add an element of type {} to a workspace.'.format(type(model)))
192
232
 
193
233
  def __getattr__(self, name: str) -> Union['Person', 'SoftwareSystem']:
194
234
  return self._dynamic_attrs[name]
@@ -232,16 +272,22 @@ class SoftwareSystem(DslElementRelationOverrides[
232
272
  def destinations(self) -> List[DslElement]:
233
273
  return self._destinations
234
274
 
275
+ @property
276
+ def relationships(self) -> Set[_Relationship]:
277
+ return self._relationships
278
+
235
279
  @property
236
280
  def tags(self) -> Set[str]:
237
281
  return self._tags
238
282
 
239
283
  def __init__(self, name: str, description: str="", tags: Set[str]=set(), properties: Dict[str, Any]=dict()) -> None:
240
284
  self._m = buildzr.models.SoftwareSystem()
285
+ self.model.containers = []
241
286
  self._parent: Optional[Workspace] = None
242
287
  self._children: Optional[List['Container']] = []
243
288
  self._sources: List[DslElement] = []
244
289
  self._destinations: List[DslElement] = []
290
+ self._relationships: Set[_Relationship] = set()
245
291
  self._tags = {'Element', 'Software System'}.union(tags)
246
292
  self._dynamic_attrs: Dict[str, 'Container'] = {}
247
293
  self._label: Optional[str] = None
@@ -251,37 +297,41 @@ class SoftwareSystem(DslElementRelationOverrides[
251
297
  self.model.tags = ','.join(self._tags)
252
298
  self.model.properties = properties
253
299
 
254
- def contains(
255
- self,
256
- *containers: Union['Container', _FluentRelationship['Container']]
257
- ) -> _FluentRelationship['SoftwareSystem']:
258
- if not self.model.containers:
259
- self.model.containers = []
260
-
261
- for child in containers:
262
- if isinstance(child, Container):
263
- self.add_element(child)
264
- elif _is_container_fluent_relationship(child):
265
- self.add_element(child._parent)
266
- return _FluentRelationship['SoftwareSystem'](self)
300
+ workspace = _current_workspace.get()
301
+ if workspace is not None:
302
+ workspace.add_model(self)
303
+ workspace._add_dynamic_attr(self.model.name, self)
267
304
 
268
- def labeled(self, label: str) -> 'SoftwareSystem':
269
- self._label = label
305
+ stack = _current_group_stack.get()
306
+ if stack:
307
+ stack[-1].add_element(self)
308
+
309
+ def __enter__(self) -> Self:
310
+ self._token = _current_software_system.set(self)
270
311
  return self
271
312
 
313
+ def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[Any]) -> None:
314
+ _current_software_system.reset(self._token)
315
+
272
316
  def container(self) -> TypedDynamicAttribute['Container']:
273
317
  return TypedDynamicAttribute['Container'](self._dynamic_attrs)
274
318
 
275
- def add_element(self, element: 'Container') -> None:
276
- if isinstance(element, Container):
277
- self.model.containers.append(element.model)
278
- element._parent = self
279
- self._dynamic_attrs[_child_name_transform(element.model.name)] = element
280
- if element._label:
281
- self._dynamic_attrs[_child_name_transform(element._label)] = element
282
- self._children.append(element)
319
+ def add_container(self, container: 'Container') -> None:
320
+ if isinstance(container, Container):
321
+ self.model.containers.append(container.model)
322
+ container._parent = self
323
+ self._add_dynamic_attr(container.model.name, container)
324
+ self._children.append(container)
283
325
  else:
284
- raise ValueError('Invalid element type: Trying to add an element of type {} to a software system.'.format(type(element)))
326
+ raise ValueError('Invalid element type: Trying to add an element of type {} to a software system.'.format(type(container)))
327
+
328
+ def _add_dynamic_attr(self, name: str, model: 'Container') -> None:
329
+ if isinstance(model, Container):
330
+ self._dynamic_attrs[_child_name_transform(name)] = model
331
+ if model._label:
332
+ self._dynamic_attrs[_child_name_transform(model._label)] = model
333
+ else:
334
+ raise ValueError('Invalid element type: Trying to add an element of type {} to a software system.'.format(type(model)))
285
335
 
286
336
  def __getattr__(self, name: str) -> 'Container':
287
337
  return self._dynamic_attrs[name]
@@ -292,6 +342,13 @@ class SoftwareSystem(DslElementRelationOverrides[
292
342
  def __dir__(self) -> Iterable[str]:
293
343
  return list(super().__dir__()) + list(self._dynamic_attrs.keys())
294
344
 
345
+ def labeled(self, label: str) -> 'SoftwareSystem':
346
+ self._label = label
347
+ workspace = _current_workspace.get()
348
+ if workspace is not None:
349
+ workspace._add_dynamic_attr(label, self)
350
+ return self
351
+
295
352
  class Person(DslElementRelationOverrides[
296
353
  'Person',
297
354
  Union[
@@ -329,6 +386,10 @@ class Person(DslElementRelationOverrides[
329
386
  def destinations(self) -> List[DslElement]:
330
387
  return self._destinations
331
388
 
389
+ @property
390
+ def relationships(self) -> Set[_Relationship]:
391
+ return self._relationships
392
+
332
393
  @property
333
394
  def tags(self) -> Set[str]:
334
395
  return self._tags
@@ -338,6 +399,7 @@ class Person(DslElementRelationOverrides[
338
399
  self._parent: Optional[Workspace] = None
339
400
  self._sources: List[DslElement] = []
340
401
  self._destinations: List[DslElement] = []
402
+ self._relationships: Set[_Relationship] = set()
341
403
  self._tags = {'Element', 'Person'}.union(tags)
342
404
  self._label: Optional[str] = None
343
405
  self.model.id = GenerateId.for_element()
@@ -347,8 +409,19 @@ class Person(DslElementRelationOverrides[
347
409
  self.model.tags = ','.join(self._tags)
348
410
  self.model.properties = properties
349
411
 
412
+ workspace = _current_workspace.get()
413
+ if workspace is not None:
414
+ workspace.add_model(self)
415
+
416
+ stack = _current_group_stack.get()
417
+ if stack:
418
+ stack[-1].add_element(self)
419
+
350
420
  def labeled(self, label: str) -> 'Person':
351
421
  self._label = label
422
+ workspace = _current_workspace.get()
423
+ if workspace is not None:
424
+ workspace._add_dynamic_attr(label, self)
352
425
  return self
353
426
 
354
427
  class Container(DslElementRelationOverrides[
@@ -384,23 +457,22 @@ class Container(DslElementRelationOverrides[
384
457
  def destinations(self) -> List[DslElement]:
385
458
  return self._destinations
386
459
 
460
+ @property
461
+ def relationships(self) -> Set[_Relationship]:
462
+ return self._relationships
463
+
387
464
  @property
388
465
  def tags(self) -> Set[str]:
389
466
  return self._tags
390
467
 
391
- def contains(self, *components: 'Component') -> _FluentRelationship['Container']:
392
- if not self.model.components:
393
- self.model.components = []
394
- for component in components:
395
- self.add_element(component)
396
- return _FluentRelationship['Container'](self)
397
-
398
468
  def __init__(self, name: str, description: str="", technology: str="", tags: Set[str]=set(), properties: Dict[str, Any]=dict()) -> None:
399
469
  self._m = buildzr.models.Container()
470
+ self.model.components = []
400
471
  self._parent: Optional[SoftwareSystem] = None
401
472
  self._children: Optional[List['Component']] = []
402
473
  self._sources: List[DslElement] = []
403
474
  self._destinations: List[DslElement] = []
475
+ self._relationships: Set[_Relationship] = set()
404
476
  self._tags = {'Element', 'Container'}.union(tags)
405
477
  self._dynamic_attrs: Dict[str, 'Component'] = {}
406
478
  self._label: Optional[str] = None
@@ -412,23 +484,48 @@ class Container(DslElementRelationOverrides[
412
484
  self.model.tags = ','.join(self._tags)
413
485
  self.model.properties = properties
414
486
 
487
+ software_system = _current_software_system.get()
488
+ if software_system is not None:
489
+ software_system.add_container(self)
490
+ software_system._add_dynamic_attr(self.model.name, self)
491
+
492
+ stack = _current_group_stack.get()
493
+ if stack:
494
+ stack[-1].add_element(self)
495
+
496
+ def __enter__(self) -> Self:
497
+ self._token = _current_container.set(self)
498
+ return self
499
+
500
+ def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[Any]) -> None:
501
+ _current_container.reset(self._token)
502
+
415
503
  def labeled(self, label: str) -> 'Container':
416
504
  self._label = label
505
+ software_system = _current_software_system.get()
506
+ if software_system is not None:
507
+ software_system._add_dynamic_attr(label, self)
417
508
  return self
418
509
 
419
510
  def component(self) -> TypedDynamicAttribute['Component']:
420
511
  return TypedDynamicAttribute['Component'](self._dynamic_attrs)
421
512
 
422
- def add_element(self, element: 'Component') -> None:
423
- if isinstance(element, Component):
424
- self.model.components.append(element.model)
425
- element._parent = self
426
- self._dynamic_attrs[_child_name_transform(element.model.name)] = element
427
- if element._label:
428
- self._dynamic_attrs[_child_name_transform(element._label)] = element
429
- self._children.append(element)
513
+ def add_component(self, component: 'Component') -> None:
514
+ if isinstance(component, Component):
515
+ self.model.components.append(component.model)
516
+ component._parent = self
517
+ self._add_dynamic_attr(component.model.name, component)
518
+ self._children.append(component)
430
519
  else:
431
- raise ValueError('Invalid element type: Trying to add an element of type {} to a container.'.format(type(element)))
520
+ raise ValueError('Invalid element type: Trying to add an element of type {} to a container.'.format(type(component)))
521
+
522
+ def _add_dynamic_attr(self, name: str, model: 'Component') -> None:
523
+ if isinstance(model, Component):
524
+ self._dynamic_attrs[_child_name_transform(name)] = model
525
+ if model._label:
526
+ self._dynamic_attrs[_child_name_transform(model._label)] = model
527
+ else:
528
+ raise ValueError('Invalid element type: Trying to add an element of type {} to a container.'.format(type(model)))
432
529
 
433
530
  def __getattr__(self, name: str) -> 'Component':
434
531
  return self._dynamic_attrs[name]
@@ -472,6 +569,10 @@ class Component(DslElementRelationOverrides[
472
569
  def destinations(self) -> List[DslElement]:
473
570
  return self._destinations
474
571
 
572
+ @property
573
+ def relationships(self) -> Set[_Relationship]:
574
+ return self._relationships
575
+
475
576
  @property
476
577
  def tags(self) -> Set[str]:
477
578
  return self._tags
@@ -481,6 +582,7 @@ class Component(DslElementRelationOverrides[
481
582
  self._parent: Optional[Container] = None
482
583
  self._sources: List[DslElement] = []
483
584
  self._destinations: List[DslElement] = []
585
+ self._relationships: Set[_Relationship] = set()
484
586
  self._tags = {'Element', 'Component'}.union(tags)
485
587
  self._label: Optional[str] = None
486
588
  self.model.id = GenerateId.for_element()
@@ -491,8 +593,20 @@ class Component(DslElementRelationOverrides[
491
593
  self.model.tags = ','.join(self._tags)
492
594
  self.model.properties = properties
493
595
 
596
+ container = _current_container.get()
597
+ if container is not None:
598
+ container.add_component(self)
599
+ container._add_dynamic_attr(self.model.name, self)
600
+
601
+ stack = _current_group_stack.get()
602
+ if stack:
603
+ stack[-1].add_element(self)
604
+
494
605
  def labeled(self, label: str) -> 'Component':
495
606
  self._label = label
607
+ container = _current_container.get()
608
+ if container is not None:
609
+ container._add_dynamic_attr(label, self)
496
610
  return self
497
611
 
498
612
  class Group:
@@ -500,27 +614,60 @@ class Group:
500
614
  def __init__(
501
615
  self,
502
616
  name: str,
503
- *elements: Union[
504
- Person,
505
- SoftwareSystem,
506
- _FluentRelationship[SoftwareSystem],
507
- _FluentRelationship[Container],
508
- ]) -> None:
617
+ workspace: Optional[Workspace]=None,
618
+ ) -> None:
619
+
620
+ if not workspace:
621
+ workspace = _current_workspace.get()
622
+ if workspace is not None:
623
+ self._group_separator = workspace._group_separator
624
+
625
+ self._group_separator = workspace._group_separator
509
626
  self._name = name
510
- self._elements = elements
511
627
 
512
- class _FluentSink(DslFluentSink):
628
+ if len(self._group_separator) > 1:
629
+ raise ValueError('Group separator must be a single character.')
513
630
 
514
- def __init__(self, workspace: Workspace) -> None:
515
- self._workspace = workspace
631
+ if self._group_separator in self._name:
632
+ raise ValueError('Group name cannot contain the group separator.')
516
633
 
517
- def to_json(self, path: str) -> None:
518
- from buildzr.sinks.json_sink import JsonSink, JsonSinkConfig
519
- sink = JsonSink()
520
- sink.write(workspace=self._workspace.model, config=JsonSinkConfig(path=path))
634
+ stack = _current_group_stack.get()
635
+ new_stack = stack.copy()
636
+ new_stack.extend([self])
521
637
 
522
- def get_workspace(self) -> Workspace:
523
- return self._workspace
638
+ self._full_name = self._group_separator.join([group._name for group in new_stack])
639
+
640
+ def full_name(self) -> str:
641
+ return self._full_name
642
+
643
+ def add_element(
644
+ self,
645
+ model: Union[
646
+ 'Person',
647
+ 'SoftwareSystem',
648
+ 'Container',
649
+ 'Component',
650
+ ]
651
+ ) -> None:
652
+
653
+
654
+ model.model.group = self._full_name
655
+
656
+ def __enter__(self) -> Self:
657
+ stack = _current_group_stack.get() # stack: a/b
658
+ stack.extend([self]) # stack: a/b -> a/b/self
659
+ self._token = _current_group_stack.set(stack)
660
+ return self
661
+
662
+ def __exit__(
663
+ self,
664
+ exc_type: Optional[Type[BaseException]],
665
+ exc_value: Optional[BaseException],
666
+ traceback: Optional[Any]
667
+ ) -> None:
668
+ stack = _current_group_stack.get()
669
+ stack.pop() # stack: a/b/self -> a/b
670
+ _current_group_stack.reset(self._token)
524
671
 
525
672
  _RankDirection = Literal['tb', 'bt', 'lr', 'rl']
526
673
 
@@ -607,10 +754,10 @@ class SystemLandscapeView(DslViewElement):
607
754
  description: str,
608
755
  auto_layout: _AutoLayout='tb',
609
756
  title: Optional[str]=None,
610
- include_elements: List[Callable[[Workspace, Element], bool]]=[],
611
- exclude_elements: List[Callable[[Workspace, Element], bool]]=[],
612
- include_relationships: List[Callable[[Workspace, Relationship], bool]]=[],
613
- exclude_relationships: List[Callable[[Workspace, Relationship], bool]]=[],
757
+ include_elements: List[Union[DslElement, Callable[[Workspace, Element], bool]]]=[],
758
+ exclude_elements: List[Union[DslElement, Callable[[Workspace, Element], bool]]]=[],
759
+ include_relationships: List[Union[DslElement, Callable[[Workspace, Relationship], bool]]]=[],
760
+ exclude_relationships: List[Union[DslElement, Callable[[Workspace, Relationship], bool]]]=[],
614
761
  properties: Optional[Dict[str, str]]=None,
615
762
  ) -> None:
616
763
  self._m = buildzr.models.SystemLandscapeView()
@@ -628,6 +775,10 @@ class SystemLandscapeView(DslViewElement):
628
775
  self._include_relationships = include_relationships
629
776
  self._exclude_relationships = exclude_relationships
630
777
 
778
+ workspace = _current_workspace.get()
779
+ if workspace is not None:
780
+ workspace.apply_views(self)
781
+
631
782
  def _on_added(self) -> None:
632
783
 
633
784
  from buildzr.dsl.expression import Expression, Element, Relationship
@@ -642,17 +793,17 @@ class SystemLandscapeView(DslViewElement):
642
793
 
643
794
  workspace = self._parent._parent
644
795
 
645
- include_view_elements_filter: List[Callable[[Workspace, Element], bool]] = [
796
+ include_view_elements_filter: List[Union[DslElement, Callable[[Workspace, Element], bool]]] = [
646
797
  lambda w, e: e.type == Person,
647
798
  lambda w, e: e.type == SoftwareSystem
648
799
  ]
649
800
 
650
- exclude_view_elements_filter: List[Callable[[Workspace, Element], bool]] = [
801
+ exclude_view_elements_filter: List[Union[DslElement, Callable[[Workspace, Element], bool]]] = [
651
802
  lambda w, e: e.type == Container,
652
803
  lambda w, e: e.type == Component,
653
804
  ]
654
805
 
655
- include_view_relationships_filter: List[Callable[[Workspace, Relationship], bool]] = [
806
+ include_view_relationships_filter: List[Union[DslElement, Callable[[Workspace, Relationship], bool]]] = [
656
807
  lambda w, r: r.source.type == Person,
657
808
  lambda w, r: r.source.type == SoftwareSystem,
658
809
  lambda w, r: r.destination.type == Person,
@@ -703,15 +854,15 @@ class SystemContextView(DslViewElement):
703
854
 
704
855
  def __init__(
705
856
  self,
706
- software_system_selector: Callable[[Workspace], SoftwareSystem],
857
+ software_system_selector: Union[SoftwareSystem, Callable[[Workspace], SoftwareSystem]],
707
858
  key: str,
708
859
  description: str,
709
860
  auto_layout: _AutoLayout='tb',
710
861
  title: Optional[str]=None,
711
- include_elements: List[Callable[[Workspace, Element], bool]]=[],
712
- exclude_elements: List[Callable[[Workspace, Element], bool]]=[],
713
- include_relationships: List[Callable[[Workspace, Relationship], bool]]=[],
714
- exclude_relationships: List[Callable[[Workspace, Relationship], bool]]=[],
862
+ include_elements: List[Union[DslElement, Callable[[Workspace, Element], bool]]]=[],
863
+ exclude_elements: List[Union[DslElement, Callable[[Workspace, Element], bool]]]=[],
864
+ include_relationships: List[Union[DslElement, Callable[[Workspace, Relationship], bool]]]=[],
865
+ exclude_relationships: List[Union[DslElement, Callable[[Workspace, Relationship], bool]]]=[],
715
866
  properties: Optional[Dict[str, str]]=None,
716
867
  ) -> None:
717
868
  self._m = buildzr.models.SystemContextView()
@@ -730,20 +881,27 @@ class SystemContextView(DslViewElement):
730
881
  self._include_relationships = include_relationships
731
882
  self._exclude_relationships = exclude_relationships
732
883
 
884
+ workspace = _current_workspace.get()
885
+ if workspace is not None:
886
+ workspace.apply_views(self)
887
+
733
888
  def _on_added(self) -> None:
734
889
 
735
890
  from buildzr.dsl.expression import Expression, Element, Relationship
736
891
  from buildzr.models import ElementView, RelationshipView
737
892
 
738
- software_system = self._selector(self._parent._parent)
893
+ if isinstance(self._selector, SoftwareSystem):
894
+ software_system = self._selector
895
+ else:
896
+ software_system = self._selector(self._parent._parent)
739
897
  self._m.softwareSystemId = software_system.model.id
740
- view_elements_filter: List[Callable[[Workspace, Element], bool]] = [
898
+ view_elements_filter: List[Union[DslElement, Callable[[Workspace, Element], bool]]] = [
741
899
  lambda w, e: e == software_system,
742
900
  lambda w, e: software_system.model.id in e.sources.ids,
743
901
  lambda w, e: software_system.model.id in e.destinations.ids,
744
902
  ]
745
903
 
746
- view_relationships_filter: List[Callable[[Workspace, Relationship], bool]] = [
904
+ view_relationships_filter: List[Union[DslElement, Callable[[Workspace, Relationship], bool]]] = [
747
905
  lambda w, r: software_system == r.source,
748
906
  lambda w, r: software_system == r.destination,
749
907
  ]
@@ -789,15 +947,15 @@ class ContainerView(DslViewElement):
789
947
 
790
948
  def __init__(
791
949
  self,
792
- software_system_selector: Callable[[Workspace], SoftwareSystem],
950
+ software_system_selector: Union[SoftwareSystem, Callable[[Workspace], SoftwareSystem]],
793
951
  key: str,
794
952
  description: str,
795
953
  auto_layout: _AutoLayout='tb',
796
954
  title: Optional[str]=None,
797
- include_elements: List[Callable[[Workspace, Element], bool]]=[],
798
- exclude_elements: List[Callable[[Workspace, Element], bool]]=[],
799
- include_relationships: List[Callable[[Workspace, Relationship], bool]]=[],
800
- exclude_relationships: List[Callable[[Workspace, Relationship], bool]]=[],
955
+ include_elements: List[Union[DslElement, Callable[[Workspace, Element], bool]]]=[],
956
+ exclude_elements: List[Union[DslElement, Callable[[Workspace, Element], bool]]]=[],
957
+ include_relationships: List[Union[DslElement, Callable[[Workspace, Relationship], bool]]]=[],
958
+ exclude_relationships: List[Union[DslElement, Callable[[Workspace, Relationship], bool]]]=[],
801
959
  properties: Optional[Dict[str, str]]=None,
802
960
  ) -> None:
803
961
  self._m = buildzr.models.ContainerView()
@@ -816,23 +974,30 @@ class ContainerView(DslViewElement):
816
974
  self._include_relationships = include_relationships
817
975
  self._exclude_relationships = exclude_relationships
818
976
 
977
+ workspace = _current_workspace.get()
978
+ if workspace is not None:
979
+ workspace.apply_views(self)
980
+
819
981
  def _on_added(self) -> None:
820
982
 
821
983
  from buildzr.dsl.expression import Expression, Element, Relationship
822
984
  from buildzr.models import ElementView, RelationshipView
823
985
 
824
- software_system = self._selector(self._parent._parent)
986
+ if isinstance(self._selector, SoftwareSystem):
987
+ software_system = self._selector
988
+ else:
989
+ software_system = self._selector(self._parent._parent)
825
990
  self._m.softwareSystemId = software_system.model.id
826
991
 
827
992
  container_ids = { container.model.id for container in software_system.children}
828
993
 
829
- view_elements_filter: List[Callable[[Workspace, Element], bool]] = [
994
+ view_elements_filter: List[Union[DslElement, Callable[[Workspace, Element], bool]]] = [
830
995
  lambda w, e: e.parent == software_system,
831
996
  lambda w, e: any(container_ids.intersection({ id for id in e.sources.ids })),
832
997
  lambda w, e: any(container_ids.intersection({ id for id in e.destinations.ids })),
833
998
  ]
834
999
 
835
- view_relationships_filter: List[Callable[[Workspace, Relationship], bool]] = [
1000
+ view_relationships_filter: List[Union[DslElement, Callable[[Workspace, Relationship], bool]]] = [
836
1001
  lambda w, r: software_system == r.source.parent,
837
1002
  lambda w, r: software_system == r.destination.parent,
838
1003
  ]
@@ -878,15 +1043,15 @@ class ComponentView(DslViewElement):
878
1043
 
879
1044
  def __init__(
880
1045
  self,
881
- container_selector: Callable[[Workspace], Container],
1046
+ container_selector: Union[Container, Callable[[Workspace], Container]],
882
1047
  key: str,
883
1048
  description: str,
884
1049
  auto_layout: _AutoLayout='tb',
885
1050
  title: Optional[str]=None,
886
- include_elements: List[Callable[[Workspace, Element], bool]]=[],
887
- exclude_elements: List[Callable[[Workspace, Element], bool]]=[],
888
- include_relationships: List[Callable[[Workspace, Relationship], bool]]=[],
889
- exclude_relationships: List[Callable[[Workspace, Relationship], bool]]=[],
1051
+ include_elements: List[Union[DslElement, Callable[[Workspace, Element], bool]]]=[],
1052
+ exclude_elements: List[Union[DslElement, Callable[[Workspace, Element], bool]]]=[],
1053
+ include_relationships: List[Union[DslElement, Callable[[Workspace, Relationship], bool]]]=[],
1054
+ exclude_relationships: List[Union[DslElement, Callable[[Workspace, Relationship], bool]]]=[],
890
1055
  properties: Optional[Dict[str, str]]=None,
891
1056
  ) -> None:
892
1057
  self._m = buildzr.models.ComponentView()
@@ -905,23 +1070,30 @@ class ComponentView(DslViewElement):
905
1070
  self._include_relationships = include_relationships
906
1071
  self._exclude_relationships = exclude_relationships
907
1072
 
1073
+ workspace = _current_workspace.get()
1074
+ if workspace is not None:
1075
+ workspace.apply_views(self)
1076
+
908
1077
  def _on_added(self) -> None:
909
1078
 
910
1079
  from buildzr.dsl.expression import Expression, Element, Relationship
911
1080
  from buildzr.models import ElementView, RelationshipView
912
1081
 
913
- container = self._selector(self._parent._parent)
1082
+ if isinstance(self._selector, Container):
1083
+ container = self._selector
1084
+ else:
1085
+ container = self._selector(self._parent._parent)
914
1086
  self._m.containerId = container.model.id
915
1087
 
916
1088
  component_ids = { component.model.id for component in container.children }
917
1089
 
918
- view_elements_filter: List[Callable[[Workspace, Element], bool]] = [
1090
+ view_elements_filter: List[Union[DslElement, Callable[[Workspace, Element], bool]]] = [
919
1091
  lambda w, e: e.parent == container,
920
1092
  lambda w, e: any(component_ids.intersection({ id for id in e.sources.ids })),
921
1093
  lambda w, e: any(component_ids.intersection({ id for id in e.destinations.ids })),
922
1094
  ]
923
1095
 
924
- view_relationships_filter: List[Callable[[Workspace, Relationship], bool]] = [
1096
+ view_relationships_filter: List[Union[DslElement, Callable[[Workspace, Relationship], bool]]] = [
925
1097
  lambda w, r: container == r.source.parent,
926
1098
  lambda w, r: container == r.destination.parent,
927
1099
  ]
@@ -955,6 +1127,9 @@ class ComponentView(DslViewElement):
955
1127
 
956
1128
  class Views(DslViewsElement):
957
1129
 
1130
+ # TODO: Make this view a "hidden" class -- it's not a "first class citizen"
1131
+ # in buildzr DSL.
1132
+
958
1133
  @property
959
1134
  def model(self) -> buildzr.models.Views:
960
1135
  return self._m
@@ -971,10 +1146,10 @@ class Views(DslViewsElement):
971
1146
  self._parent = workspace
972
1147
  self._parent._m.views = self._m
973
1148
 
974
- def contains(
1149
+ def add_views(
975
1150
  self,
976
1151
  *views: DslViewElement
977
- ) -> _FluentSink:
1152
+ ) -> None:
978
1153
 
979
1154
  for view in views:
980
1155
  if isinstance(view, SystemLandscapeView):
@@ -1008,10 +1183,282 @@ class Views(DslViewsElement):
1008
1183
  else:
1009
1184
  raise NotImplementedError("The view {0} is currently not supported", type(view))
1010
1185
 
1011
- return _FluentSink(self._parent)
1012
-
1013
1186
  def get_workspace(self) -> Workspace:
1014
1187
  """
1015
1188
  Get the `Workspace` which contain this views definition.
1016
1189
  """
1017
- return self._parent
1190
+ return self._parent
1191
+
1192
+ class StyleElements:
1193
+
1194
+ from buildzr.dsl.expression import Element
1195
+
1196
+ Shapes = Union[
1197
+ Literal['Box'],
1198
+ Literal['RoundedBox'],
1199
+ Literal['Circle'],
1200
+ Literal['Ellipse'],
1201
+ Literal['Hexagon'],
1202
+ Literal['Cylinder'],
1203
+ Literal['Pipe'],
1204
+ Literal['Person'],
1205
+ Literal['Robot'],
1206
+ Literal['Folder'],
1207
+ Literal['WebBrowser'],
1208
+ Literal['MobileDevicePortrait'],
1209
+ Literal['MobileDeviceLandscape'],
1210
+ Literal['Component'],
1211
+ ]
1212
+
1213
+ @property
1214
+ def model(self) -> List[buildzr.models.ElementStyle]:
1215
+ return self._m
1216
+
1217
+ @property
1218
+ def parent(self) -> Optional[Workspace]:
1219
+ return self._parent
1220
+
1221
+ # TODO: Validate arguments with pydantic.
1222
+ def __init__(
1223
+ self,
1224
+ on: List[Union[
1225
+ DslElement,
1226
+ Group,
1227
+ Callable[[Workspace, Element], bool],
1228
+ Type[Union['Person', 'SoftwareSystem', 'Container', 'Component']],
1229
+ str
1230
+ ]],
1231
+ shape: Optional[Shapes]=None,
1232
+ icon: Optional[str]=None,
1233
+ width: Optional[int]=None,
1234
+ height: Optional[int]=None,
1235
+ background: Optional[Union['str', Tuple[int, int, int], Color]]=None,
1236
+ color: Optional[Union['str', Tuple[int, int, int], Color]]=None,
1237
+ stroke: Optional[Union[str, Tuple[int, int, int], Color]]=None,
1238
+ stroke_width: Optional[int]=None,
1239
+ font_size: Optional[int]=None,
1240
+ border: Optional[Literal['solid', 'dashed', 'dotted']]=None,
1241
+ opacity: Optional[int]=None,
1242
+ metadata: Optional[bool]=None,
1243
+ description: Optional[bool]=None,
1244
+ ) -> None:
1245
+
1246
+ # How the tag is populated depends on each element type in the
1247
+ # `elemenets`.
1248
+ # - If the element is a `DslElement`, then we create a unique tag
1249
+ # specifically to help the stylizer identify that specific element.
1250
+ # For example, if the element has an id `3`, then we should create a
1251
+ # tag, say, `style-element-3`.
1252
+ # - If the element is a `Group`, then we simply make create the tag
1253
+ # based on the group name and its nested path. For example,
1254
+ # `Group:Company 1/Department 1`.
1255
+ # - If the element is a `Callable[[Workspace, Element], bool]`, we just
1256
+ # run the function to filter out all the elements that matches the
1257
+ # description, and create a unique tag for all of the filtered
1258
+ # elements.
1259
+ # - If the element is a `Type[Union['Person', 'SoftwareSystem', 'Container', 'Component']]`,
1260
+ # we create a tag based on the class name. This is based on the fact
1261
+ # that the default tag for each element is the element's type.
1262
+ # - If the element is a `str`, we just use the string as the tag.
1263
+ # This is useful for when you want to apply a style to all elements
1264
+ # with a specific tag, just like in the original Structurizr DSL.
1265
+ #
1266
+ # Note that a new `buildzr.models.ElementStyle` is created for each
1267
+ # item, not for each of `StyleElements` instance. This makes the styling
1268
+ # makes more concise and flexible.
1269
+
1270
+ from buildzr.dsl.expression import Element
1271
+ from uuid import uuid4
1272
+
1273
+ if background:
1274
+ assert Color.is_valid_color(background), "Invalid background color: {}".format(background)
1275
+ if color:
1276
+ assert Color.is_valid_color(color), "Invalid color: {}".format(color)
1277
+ if stroke:
1278
+ assert Color.is_valid_color(stroke), "Invalid stroke color: {}".format(stroke)
1279
+
1280
+ self._m: List[buildzr.models.ElementStyle] = []
1281
+ self._parent: Optional[Workspace] = None
1282
+
1283
+ workspace = _current_workspace.get()
1284
+ if workspace is not None:
1285
+ self._parent = workspace
1286
+
1287
+ self._elements = on
1288
+
1289
+ border_enum: Dict[str, buildzr.models.Border] = {
1290
+ 'solid': buildzr.models.Border.Solid,
1291
+ 'dashed': buildzr.models.Border.Dashed,
1292
+ 'dotted': buildzr.models.Border.Dotted,
1293
+ }
1294
+
1295
+ shape_enum: Dict[str, buildzr.models.Shape] = {
1296
+ 'Box': buildzr.models.Shape.Box,
1297
+ 'RoundedBox': buildzr.models.Shape.RoundedBox,
1298
+ 'Circle': buildzr.models.Shape.Circle,
1299
+ 'Ellipse': buildzr.models.Shape.Ellipse,
1300
+ 'Hexagon': buildzr.models.Shape.Hexagon,
1301
+ 'Cylinder': buildzr.models.Shape.Cylinder,
1302
+ 'Pipe': buildzr.models.Shape.Pipe,
1303
+ 'Person': buildzr.models.Shape.Person,
1304
+ 'Robot': buildzr.models.Shape.Robot,
1305
+ 'Folder': buildzr.models.Shape.Folder,
1306
+ 'WebBrowser': buildzr.models.Shape.WebBrowser,
1307
+ 'MobileDevicePortrait': buildzr.models.Shape.MobileDevicePortrait,
1308
+ 'MobileDeviceLandscape': buildzr.models.Shape.MobileDeviceLandscape,
1309
+ 'Component': buildzr.models.Shape.Component,
1310
+ }
1311
+
1312
+ # A single unique element to be applied to all elements
1313
+ # affected by this style.
1314
+ element_tag = "buildzr-styleelements-{}".format(uuid4().hex)
1315
+
1316
+ for element in self._elements:
1317
+
1318
+ element_style = buildzr.models.ElementStyle()
1319
+ element_style.shape = shape_enum[shape] if shape else None
1320
+ element_style.icon = icon
1321
+ element_style.width = width
1322
+ element_style.height = height
1323
+ element_style.background = Color(background).to_hex() if background else None
1324
+ element_style.color = Color(color).to_hex() if color else None
1325
+ element_style.stroke = Color(stroke).to_hex() if stroke else None
1326
+ element_style.strokeWidth = stroke_width
1327
+ element_style.fontSize = font_size
1328
+ element_style.border = border_enum[border] if border else None
1329
+ element_style.opacity = opacity
1330
+ element_style.metadata = metadata
1331
+ element_style.description = description
1332
+
1333
+ if isinstance(element, DslElement) and not isinstance(element.model, buildzr.models.Workspace):
1334
+ element_style.tag = element_tag
1335
+ element.add_tags(element_tag)
1336
+ elif isinstance(element, Group):
1337
+ element_style.tag = f"Group:{element.full_name()}"
1338
+ elif isinstance(element, type):
1339
+ element_style.tag = f"{element.__name__}"
1340
+ elif isinstance(element, str):
1341
+ element_style.tag = element
1342
+ elif callable(element):
1343
+ from buildzr.dsl.expression import Element, Expression
1344
+ if self._parent:
1345
+ matched_elems = Expression(include_elements=[element]).elements(self._parent)
1346
+ for e in matched_elems:
1347
+ element_style.tag = element_tag
1348
+ e.add_tags(element_tag)
1349
+ else:
1350
+ raise ValueError("Cannot use callable to select elements to style without a Workspace.")
1351
+ self._m.append(element_style)
1352
+
1353
+ workspace = _current_workspace.get()
1354
+ if workspace is not None:
1355
+ workspace.apply_style(self)
1356
+
1357
+ class StyleRelationships:
1358
+
1359
+ from buildzr.dsl.expression import Relationship
1360
+
1361
+ @property
1362
+ def model(self) -> List[buildzr.models.RelationshipStyle]:
1363
+ return self._m
1364
+
1365
+ @property
1366
+ def parent(self) -> Optional[Workspace]:
1367
+ return self._parent
1368
+
1369
+ def __init__(
1370
+ self,
1371
+ on: Optional[List[Union[
1372
+ DslRelationship,
1373
+ Group,
1374
+ Callable[[Workspace, Relationship], bool],
1375
+ str
1376
+ ]]]=None,
1377
+ thickness: Optional[int]=None,
1378
+ color: Optional[Union[str, Tuple[int, int, int], Color]]=None,
1379
+ routing: Optional[Literal['Direct', 'Orthogonal', 'Curved']]=None,
1380
+ font_size: Optional[int]=None,
1381
+ width: Optional[int]=None,
1382
+ dashed: Optional[bool]=None,
1383
+ position: Optional[int]=None,
1384
+ opacity: Optional[int]=None,
1385
+ ) -> None:
1386
+
1387
+ from uuid import uuid4
1388
+
1389
+ if color is not None:
1390
+ assert Color.is_valid_color(color), "Invalid color: {}".format(color)
1391
+
1392
+ routing_enum: Dict[str, buildzr.models.Routing1] = {
1393
+ 'Direct': buildzr.models.Routing1.Direct,
1394
+ 'Orthogonal': buildzr.models.Routing1.Orthogonal,
1395
+ 'Curved': buildzr.models.Routing1.Curved,
1396
+ }
1397
+
1398
+ self._m: List[buildzr.models.RelationshipStyle] = []
1399
+ self._parent: Optional[Workspace] = None
1400
+
1401
+ workspace = _current_workspace.get()
1402
+ if workspace is not None:
1403
+ self._parent = workspace
1404
+
1405
+ # A single unique tag to be applied to all relationships
1406
+ # affected by this style.
1407
+ relation_tag = "buildzr-stylerelationships-{}".format(uuid4().hex)
1408
+
1409
+ if on is None:
1410
+ self._m.append(buildzr.models.RelationshipStyle(
1411
+ thickness=thickness,
1412
+ color=Color(color).to_hex() if color else None,
1413
+ routing=routing_enum[routing] if routing else None,
1414
+ fontSize=font_size,
1415
+ width=width,
1416
+ dashed=dashed,
1417
+ position=position,
1418
+ opacity=opacity,
1419
+ tag="Relationship",
1420
+ ))
1421
+ else:
1422
+ for relationship in on:
1423
+
1424
+ relationship_style = buildzr.models.RelationshipStyle()
1425
+ relationship_style.thickness = thickness
1426
+ relationship_style.color = Color(color).to_hex() if color else None
1427
+ relationship_style.routing = routing_enum[routing] if routing else None
1428
+ relationship_style.fontSize = font_size
1429
+ relationship_style.width = width
1430
+ relationship_style.dashed = dashed
1431
+ relationship_style.position = position
1432
+ relationship_style.opacity = opacity
1433
+
1434
+ if isinstance(relationship, DslRelationship):
1435
+ relationship.add_tags(relation_tag)
1436
+ relationship_style.tag = relation_tag
1437
+ elif isinstance(relationship, Group):
1438
+ from buildzr.dsl.expression import Expression
1439
+ if self._parent:
1440
+ rels = Expression(include_relationships=[
1441
+ lambda w, r: r.source.group == relationship.full_name() and \
1442
+ r.destination.group == relationship.full_name()
1443
+ ]).relationships(self._parent)
1444
+ for r in rels:
1445
+ r.add_tags(relation_tag)
1446
+ relationship_style.tag = relation_tag
1447
+ else:
1448
+ raise ValueError("Cannot use callable to select elements to style without a Workspace.")
1449
+ elif isinstance(relationship, str):
1450
+ relationship_style.tag = relationship
1451
+ elif callable(relationship):
1452
+ from buildzr.dsl.expression import Expression
1453
+ if self._parent:
1454
+ matched_rels = Expression(include_relationships=[relationship]).relationships(self._parent)
1455
+ for matched_rel in matched_rels:
1456
+ matched_rel.add_tags(relation_tag)
1457
+ relationship_style.tag = relation_tag
1458
+ else:
1459
+ raise ValueError("Cannot use callable to select elements to style without a Workspace.")
1460
+ self._m.append(relationship_style)
1461
+
1462
+ workspace = _current_workspace.get()
1463
+ if workspace is not None:
1464
+ workspace.apply_style(self)