Skip to content

DSL API Reference

This page contains auto-generated API documentation from the source code.

Core Classes

Workspace

Bases: DslWorkspaceElement

Represents a Structurizr workspace, which is a wrapper for a software architecture model, views, and documentation.

Source code in buildzr/dsl/dsl.py
  74
  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
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
class Workspace(DslWorkspaceElement):
    """
    Represents a Structurizr workspace, which is a wrapper for a software architecture model, views, and documentation.
    """

    @property
    def model(self) -> buildzr.models.Workspace:
        return self._m

    @property
    def parent(self) -> None:
        return None

    @property
    def children(self) -> Optional[List[Union['Person', 'SoftwareSystem', 'DeploymentNode', 'Element']]]:
        return self._children

    def __init__(
            self,
            name: str,
            description: str="",
            scope: Literal['landscape', 'software_system', None]='software_system',
            implied_relationships: bool=False,
            group_separator: str='/',
            extend: Optional[str]=None,
        ) -> None:

        self._m = buildzr.models.Workspace()
        self._parent = None
        self._children: Optional[List[Union['Person', 'SoftwareSystem', 'DeploymentNode', 'Element']]] = []
        self._dynamic_attrs: Dict[str, Union['Person', 'SoftwareSystem', 'Element']] = {}
        self._use_implied_relationships = implied_relationships
        self._group_separator = group_separator

        # Workspace extension support - store extended model for merging
        self._extended_model: Optional[buildzr.models.Workspace] = None

        if extend:
            from buildzr.loaders import JsonLoader
            loader = JsonLoader()
            self._extended_model = loader.load(extend)
            # Set ID counter to avoid collisions with extended workspace IDs
            max_id = loader.get_max_element_id(self._extended_model)
            GenerateId.set_offset(max_id)

            # Wrap parent elements with DSL classes for direct access on workspace
            self._wrap_parent_elements()

        self.model.id = GenerateId.for_workspace()
        self.model.name = name
        self.model.description = description
        self.model.model = buildzr.models.Model(
            people=[],
            softwareSystems=[],
            deploymentNodes=[],
        )

        # Add documentation object (required by Structurizr for rendering)
        self.model.documentation = buildzr.models.Documentation()

        scope_mapper: Dict[
            str,
            Literal[buildzr.models.Scope.Landscape, buildzr.models.Scope.SoftwareSystem, None]
        ] = {
            'landscape': buildzr.models.Scope.Landscape,
            'software_system': buildzr.models.Scope.SoftwareSystem,
            None: None
        }

        self.model.configuration = buildzr.models.WorkspaceConfiguration(
            scope=scope_mapper[scope],
        )

        self.model.model.properties = {
            'structurizr.groupSeparator': group_separator,
        }

    def _wrap_parent_elements(self) -> None:
        """Wrap parent workspace elements with DSL classes for direct access."""
        if self._extended_model is None or self._extended_model.model is None:
            return

        # Wrap software systems from parent
        if self._extended_model.model.softwareSystems:
            for ss_model in self._extended_model.model.softwareSystems:
                ss = SoftwareSystem._from_model(ss_model)
                ss._parent = self
                self._children.append(ss)
                self._add_dynamic_attr(ss_model.name or '', ss)

        # Wrap people from parent
        if self._extended_model.model.people:
            for person_model in self._extended_model.model.people:
                person = Person._from_model(person_model)
                person._parent = self
                self._children.append(person)
                self._add_dynamic_attr(person_model.name or '', person)

    def __enter__(self) -> Self:
        """Enter the workspace context."""
        self._token = _current_workspace.set(self)
        return self

    def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[Any]) -> None:

        if self._use_implied_relationships:
            self._imply_relationships()

        _current_workspace.reset(self._token)

    def _is_descendant_of(self, element: 'DslElement', potential_ancestor: 'DslElement') -> bool:
        """Check if element is a descendant (child, grandchild, etc.) of potential_ancestor."""
        current = element.parent
        while current is not None:
            if current is potential_ancestor:
                return True
            current = current.parent
        return False

    def _imply_relationships( self,
    ) -> None:

        """
        Process implied relationships:
        If we have relationship s >> do >> a.b, then create s >> do >> a.
        If we have relationship s.ss >> do >> a.b.c, then create s.ss >> do >> a.b and s.ss >> do >> a.
        If we have relationship s.ss >> do >> a, then create s >> do >> a.
        And so on...

        Relationships of `SoftwareSystemInstance`s and `ContainerInstance`s are
        skipped.

        This process is idempotent, which means this can be called multiple times
        without duplicating similar relationships.

        The algorithm runs in a loop until no new relationships are created,
        which allows transitive implied relationships (e.g., Container A -> Container B
        implies System A -> System B when Container A is in System A and
        Container B is in System B).
        """

        if not self._use_implied_relationships:
            return

        from buildzr.dsl.explorer import Explorer

        # Keep processing until no new relationships are created
        # This allows transitive implied relationships to be created
        while True:
            explorer = Explorer(self)
            # Take a snapshot of relationships to avoid processing newly created ones
            relationships = list(explorer.walk_relationships())
            initial_count = len(relationships)

            for relationship in relationships:
                source = relationship.source
                destination = relationship.destination
                destination_parent = destination.parent

                if isinstance(source, (SoftwareSystemInstance, ContainerInstance)) or \
                   isinstance(destination, (SoftwareSystemInstance, ContainerInstance)):
                    continue

                # Note: We do NOT skip implied relationships (those with linkedRelationshipId)
                # because we need to process them to create transitive implied relationships.
                # For example, Container A -> Container B creates:
                # - Container A -> System B (implied from original)
                # - System A -> Container B (implied from original)
                # And then processing System A -> Container B creates:
                # - System A -> System B (implied from the implied relationship)

                # Handle case: s >> a.b => s >> a (destination is child)
                while destination_parent is not None and \
                    isinstance(source, DslElement) and \
                    not isinstance(source.model, buildzr.models.Workspace) and \
                    not isinstance(destination_parent, DslWorkspaceElement):

                    # Stop if source is a descendant of destination_parent (parent-child relationship)
                    if self._is_descendant_of(source, destination_parent):
                        break

                    rels = source.model.relationships

                    if rels:
                        already_exists = any(
                            r.destinationId == destination_parent.model.id and
                            r.description == relationship.model.description and
                            r.technology == relationship.model.technology
                            for r in rels
                        )
                        if not already_exists:
                            r = source.uses(
                                destination_parent,
                                description=relationship.model.description,
                                technology=relationship.model.technology,
                            )
                            r.model.linkedRelationshipId = relationship.model.id
                    destination_parent = destination_parent.parent

                # Handle inverse case: s.ss >> a => s >> a (source is child)
                source_parent = source.parent
                while source_parent is not None and \
                    isinstance(destination, DslElement) and \
                    not isinstance(destination.model, buildzr.models.Workspace) and \
                    not isinstance(source_parent.model, buildzr.models.Workspace) and \
                    not isinstance(source_parent, DslWorkspaceElement):

                    # Stop if destination is a descendant of source_parent (parent-child relationship)
                    if self._is_descendant_of(destination, source_parent):
                        break

                    rels = source_parent.model.relationships

                    # The parent source relationship might be empty
                    # (i.e., []).
                    if rels is not None:
                        already_exists = any(
                            r.destinationId == destination.model.id and
                            r.description == relationship.model.description and
                            r.technology == relationship.model.technology
                            for r in rels
                        )
                        if not already_exists:
                            r = source_parent.uses(
                                destination,
                                description=relationship.model.description,
                                technology=relationship.model.technology,
                            )
                            r.model.linkedRelationshipId = relationship.model.id
                    source_parent = source_parent.parent

            # Check if any new relationships were created in this pass
            new_count = len(list(Explorer(self).walk_relationships()))
            if new_count == initial_count:
                break  # No new relationships created, we're done

    def person(self) -> TypedDynamicAttribute['Person']:
        return TypedDynamicAttribute['Person'](self._dynamic_attrs)

    def software_system(self) -> TypedDynamicAttribute['SoftwareSystem']:
        return TypedDynamicAttribute['SoftwareSystem'](self._dynamic_attrs)

    def add_model(
        self, model: Union[
            'Person',
            'SoftwareSystem',
            'DeploymentNode',
            'Element',
        ]) -> None:
        if isinstance(model, Person):
            self._m.model.people.append(model._m)
            model._parent = self
            self._add_dynamic_attr(model.model.name, model)
            self._children.append(model)
        elif isinstance(model, SoftwareSystem):
            self._m.model.softwareSystems.append(model._m)
            model._parent = self
            self._add_dynamic_attr(model.model.name, model)
            self._children.append(model)
        elif isinstance(model, DeploymentNode):
            self._m.model.deploymentNodes.append(model._m)
            model._parent = self
            self._children.append(model)
        elif isinstance(model, Element):
            if self._m.model.customElements is None:
                self._m.model.customElements = []
            self._m.model.customElements.append(model._m)
            model._parent = self
            self._add_dynamic_attr(model.model.name, model)
            self._children.append(model)
        else:
            raise ValueError('Invalid element type: Trying to add an element of type {} to a workspace.'.format(type(model)))

    def apply_view( self,
        view: Union[
            'SystemLandscapeView',
            'SystemContextView',
            'ContainerView',
            'ComponentView',
            'DeploymentView',
            'DynamicView',
            'CustomView',
        ]
    ) -> None:

        self._validate_view_key(view.model)
        self._imply_relationships()

        view._on_added(self)

        if not self.model.views:
            self.model.views = buildzr.models.Views()
            # Add configuration object (required by Structurizr for rendering)
            self.model.views.configuration = buildzr.models.Configuration(
                branding=buildzr.models.Branding(),
                styles=buildzr.models.Styles(),
                terminology=buildzr.models.Terminology(),
            )

        if isinstance(view, SystemLandscapeView):
            if not self.model.views.systemLandscapeViews:
                self.model.views.systemLandscapeViews = [view.model]
            else:
                self.model.views.systemLandscapeViews.append(view.model)
        elif isinstance(view, SystemContextView):
            if not self.model.views.systemContextViews:
                self.model.views.systemContextViews = [view.model]
            else:
                self.model.views.systemContextViews.append(view.model)
        elif isinstance(view, ContainerView):
            if not self.model.views.containerViews:
                self.model.views.containerViews = [view.model]
            else:
                self.model.views.containerViews.append(view.model)
        elif isinstance(view, ComponentView):
            if not self.model.views.componentViews:
                self.model.views.componentViews = [view.model]
            else:
                self.model.views.componentViews.append(view.model)
        elif isinstance(view, DeploymentView):
            if not self.model.views.deploymentViews:
                self.model.views.deploymentViews = [view.model]
            else:
                self.model.views.deploymentViews.append(view.model)
        elif isinstance(view, DynamicView):
            if not self.model.views.dynamicViews:
                self.model.views.dynamicViews = [view.model]
            else:
                self.model.views.dynamicViews.append(view.model)
        elif isinstance(view, CustomView):
            if not self.model.views.customViews:
                self.model.views.customViews = [view.model]
            else:
                self.model.views.customViews.append(view.model)
        else:
            raise NotImplementedError("The view {0} is currently not supported", type(view))

    @staticmethod
    def _iter_view_models(views: Optional[Any]) -> Iterable[Any]:
        """Yield every view model in a Structurizr views collection."""
        if views is None:
            return

        for view_type in (
            'systemLandscapeViews',
            'systemContextViews',
            'containerViews',
            'componentViews',
            'deploymentViews',
            'dynamicViews',
            'customViews',
        ):
            view_models = getattr(views, view_type, None)
            if view_models:
                yield from view_models

    def _validate_view_key(self, view: Any) -> None:
        """Reject a view key that is already used in this workspace."""
        if not view.key:
            return

        for existing_view in self._iter_view_models(self.model.views):
            if existing_view.key == view.key:
                raise ValueError(
                    f"Duplicate view key '{view.key}' is used by "
                    f"{type(existing_view).__name__} and {type(view).__name__}."
                )

    def _validate_unique_view_keys(self, views: Optional[Any]) -> None:
        """Reject duplicate view keys in a workspace model."""
        seen: Dict[str, Any] = {}
        for view in self._iter_view_models(views):
            if not view.key:
                continue
            if view.key in seen:
                raise ValueError(
                    f"Duplicate view key '{view.key}' is used by "
                    f"{type(seen[view.key]).__name__} and {type(view).__name__}."
                )
            seen[view.key] = view

    def _configuration(self) -> buildzr.models.Configuration:
        if not self.model.views:
            self.model.views = buildzr.models.Views()
        if not self.model.views.configuration:
            self.model.views.configuration = buildzr.models.Configuration()
        return self.model.views.configuration

    def apply_theme(self, theme: Union[str, type]) -> None:
        """
        Use a Structurizr theme: its element and relationship styles come before
        the workspace's own styles. Themes apply in the order they are added.

        Args:
            theme: The URL of the theme's `theme.json`, or a theme of
                `buildzr.themes` (for example `AWS` or `KUBERNETES_V0_3`).
        """
        url = theme if isinstance(theme, str) else getattr(theme, 'THEME_URL', None)
        if not isinstance(url, str) or not url:
            raise ValueError(f"{theme!r} is not a theme URL or a theme of buildzr.themes")
        configuration = self._configuration()
        configuration.themes = (configuration.themes or []) + [url]

    def apply_branding(
        self,
        logo: Optional[str]=None,
        font: Optional[str]=None,
        font_url: Optional[str]=None,
    ) -> None:
        """
        Set the workspace's branding. Arguments left as `None` keep their value.

        Args:
            logo: The logo, as a data URI of a PNG, JPEG, GIF, WebP or SVG image.
            font: The font of the diagram text: a family name or a CSS font
                stack, such as `"Georgia, serif"`.
            font_url: The URL of a web font, kept in the workspace for other
                Structurizr tools; the buildzr viewer does not load it.
        """
        configuration = self._configuration()
        if not configuration.branding:
            configuration.branding = buildzr.models.Branding()
        branding = configuration.branding
        if logo is not None:
            branding.logo = logo
        if font is not None or font_url is not None:
            if not branding.font:
                branding.font = buildzr.models.Font()
            if font is not None:
                branding.font.name = font
            if font_url is not None:
                branding.font.url = font_url

    def apply_style( self,
        style: Union['StyleElements', 'StyleRelationships'],
    ) -> None:

        style._parent = self

        if not self.model.views:
            self.model.views = buildzr.models.Views()
        if not self.model.views.configuration:
            self.model.views.configuration = buildzr.models.Configuration()
        if not self.model.views.configuration.styles:
            self.model.views.configuration.styles = buildzr.models.Styles()

        if isinstance(style, StyleElements):
            if self.model.views.configuration.styles.elements:
                self.model.views.configuration.styles.elements.extend(style.model)
            else:
                self.model.views.configuration.styles.elements = style.model
        elif isinstance(style, StyleRelationships):
            if self.model.views.configuration.styles.relationships:
                self.model.views.configuration.styles.relationships.extend(style.model)
            else:
                self.model.views.configuration.styles.relationships = style.model

    def _merged_workspace(self) -> 'buildzr.models.Workspace':
        """
        Get the merged workspace model, combining extended workspace if present.

        This method handles implied relationships and workspace extension merging.

        Returns:
            The merged workspace model ready for export.
        """
        self._imply_relationships()

        if self._extended_model:
            merged = self._merge_models(self._extended_model, self._m)
        else:
            merged = self._m

        self._validate_unique_view_keys(merged.views)
        return merged
    def to_dict(self) -> Dict[str, Any]:
        """
        Return workspace as a JSON-serializable dictionary.

        This method is useful for programmatic access to the workspace data
        and for Jupyter notebook display.

        Returns:
            Dictionary representation of the workspace with camelCase keys.

        Example:
            >>> data = workspace.to_dict()
            >>> print(data['name'])
        """
        import json
        from buildzr.encoders.encoder import JsonEncoder
        merged = self._merged_workspace()
        return cast(Dict[str, Any], json.loads(JsonEncoder().encode(merged)))

    def _sanitize_name(self, name: str) -> str:
        """Sanitize workspace name for use as filename."""
        return name.lower().replace(' ', '_')

    def save(
        self,
        format: SaveFormat = 'json',
        path: Optional[Union[str, Path]] = None,
        pretty: bool = False,
        layout: PlantUmlLayout = 'dot',
    ) -> Union[str, List[str]]:
        """
        Save workspace to file(s) in the specified format.

        .. deprecated::
            Use the ``buildzr export`` command instead, which writes every
            workspace of a model file in JSON, PlantUML, SVG and PNG. This
            method keeps working and emits a :class:`DeprecationWarning`.
            To get the content in memory, use :meth:`to_json`,
            :meth:`to_plantuml` and :meth:`to_svg`.

        Args:
            format: Output format. One of:
                - 'json': Single JSON file (default)
                - 'plantuml': PlantUML .puml files (one per view)
                - 'svg': SVG image files (one per view)
                - 'png': PNG image files (one per view)
            path: Output path. Behavior depends on format:
                - For 'json': file path (defaults to '{cwd}/{workspace_name}.json')
                - For diagram formats: directory path (defaults to '{cwd}/')
            pretty: For 'json' format only, whether to indent output.
            layout: PlantUML layout engine for 'plantuml'/'svg'/'png'; ignored
                for 'json'. One of:
                - 'dot': Use Graphviz for layout (default). svg/png need Graphviz.
                - 'smetana': Pure-Java layout, no Graphviz needed (layout quality
                  is somewhat worse). Adds ``!pragma layout smetana`` to the
                  written .puml files as well.

        Returns:
            - For 'json': The path to the written file (str)
            - For diagram formats: List of paths to written files

        Raises:
            ImportError: If 'plantuml'/'svg'/'png' format requested but jpype1
                not installed. Install with: pip install buildzr[export-plantuml]
            PlantUmlRenderError: If 'svg'/'png' format requested with layout
                'dot' but Graphviz (the `dot` executable) is not available, or
                PlantUML fails to render a view. A missing Graphviz is reported
                before any file is written.
            ValueError: If format or layout is not recognized.

        Example:
            >>> w.save()  # Saves to ./my_workspace.json
            >>> w.save(format='json', path='output/arch.json', pretty=True)
            >>> w.save(format='plantuml', path='diagrams/')
            >>> w.save(format='svg')  # Saves to ./
            >>> w.save(format='svg', path='out/', layout='smetana')  # No Graphviz
        """
        warnings.warn(
            "Workspace.save() is deprecated and will be removed in a future "
            "release; use the `buildzr export` command instead "
            "(see https://buildzr.dev/user-guide/export/)",
            DeprecationWarning,
            stacklevel=2,
        )
        return self._save(format, path, pretty, layout)

    def _save(
        self,
        format: SaveFormat = 'json',
        path: Optional[Union[str, Path]] = None,
        pretty: bool = False,
        layout: PlantUmlLayout = 'dot',
    ) -> Union[str, List[str]]:
        """What :meth:`save` does, without the deprecation warning; ``buildzr export`` uses it."""
        if layout not in ('dot', 'smetana'):
            raise ValueError(
                f"Unsupported layout: {layout!r}. Use one of: 'dot', 'smetana'"
            )

        merged = self._merged_workspace()
        workspace_name = self._sanitize_name(self.model.name or 'workspace')

        if format == 'json':
            return self._save_json(merged, path, workspace_name, pretty)
        elif format in ('plantuml', 'svg', 'png'):
            return self._save_diagrams(merged, path, format, layout)
        else:
            raise ValueError(
                f"Unsupported format: {format}. "
                f"Use one of: 'json', 'plantuml', 'svg', 'png'"
            )

    def _save_json(
        self,
        workspace: 'buildzr.models.Workspace',
        path: Optional[Union[str, Path]],
        workspace_name: str,
        pretty: bool,
    ) -> str:
        """Save workspace as JSON file."""
        import os
        from buildzr.sinks.json_sink import JsonSink, JsonSinkConfig

        if path is None:
            path = Path.cwd() / f"{workspace_name}.json"

        path = Path(path)

        # Create parent directories if needed
        if path.parent and not path.parent.exists():
            os.makedirs(path.parent, exist_ok=True)

        sink = JsonSink()
        sink.write(workspace=workspace, config=JsonSinkConfig(
            path=str(path),
            pretty=pretty
        ))
        return str(path)

    def _save_diagrams(
        self,
        workspace: 'buildzr.models.Workspace',
        path: Optional[Union[str, Path]],
        format: Literal['plantuml', 'svg', 'png'],
        layout: PlantUmlLayout = 'dot',
    ) -> List[str]:
        """Save workspace views as diagram files."""
        import os

        try:
            from buildzr.sinks.plantuml_sink import PlantUmlSink, PlantUmlSinkConfig
        except ImportError as e:
            raise ImportError(
                "jpype1 is required for diagram export. "
                "Install with: pip install buildzr[export-plantuml]"
            ) from e

        if path is None:
            path = Path.cwd()

        path = Path(path)

        # Create directory if needed
        if not path.exists():
            os.makedirs(path, exist_ok=True)

        # Map format to PlantUmlSinkConfig format
        puml_format: Literal['puml', 'svg', 'png']
        if format == 'plantuml':
            puml_format = 'puml'
        else:
            puml_format = format  # 'svg' or 'png'

        sink = PlantUmlSink()
        config = PlantUmlSinkConfig(path=str(path), format=puml_format, layout=layout)
        sink.write(workspace=workspace, config=config)

        # Build list of created files based on views
        ext = 'puml' if format == 'plantuml' else format
        created_files: List[str] = []

        if workspace.views:
            views = workspace.views
            # Collect all views that have keys
            all_views: List[Any] = []
            if views.systemLandscapeViews:
                all_views.extend(views.systemLandscapeViews)
            if views.systemContextViews:
                all_views.extend(views.systemContextViews)
            if views.containerViews:
                all_views.extend(views.containerViews)
            if views.componentViews:
                all_views.extend(views.componentViews)
            if views.deploymentViews:
                all_views.extend(views.deploymentViews)
            if views.dynamicViews:
                all_views.extend(views.dynamicViews)
            if views.customViews:
                all_views.extend(views.customViews)

            for view in all_views:
                if view.key:
                    created_files.append(str(path / f"{view.key}.{ext}"))

        return created_files

    def to_json(self, pretty: bool = True) -> str:
        """
        Return workspace as a JSON string.

        Args:
            pretty: If True (default), format with 2-space indentation.

        Returns:
            JSON string representation of the workspace.

        Example:
            >>> json_str = workspace.to_json()
            >>> print(json_str)
        """
        from buildzr.encoders.encoder import JsonEncoder
        merged = self._merged_workspace()
        indent = 2 if pretty else None
        return JsonEncoder(indent=indent).encode(merged)

    def _repr_json_(self) -> Tuple[Dict[str, Any], Dict[str, Any]]:
        """
        Jupyter notebook JSON representation.

        Returns a tuple of (data, metadata) for Jupyter's JSON display.
        The data is the workspace as a dictionary, and metadata controls display.

        Returns:
            Tuple of (workspace_dict, display_metadata)
        """
        return self.to_dict(), {"expanded": False, "root": "workspace"}

    def to_plantuml(self) -> Dict[str, str]:
        """
        Return PlantUML source for all views as a dictionary.

        Uses the official structurizr-export Java library via JPype to generate
        C4-PlantUML diagrams from workspace views.

        Returns:
            Dictionary mapping view keys to PlantUML source strings.

        Raises:
            ImportError: If jpype1 is not installed (install with: pip install buildzr[export-plantuml])
            FileNotFoundError: If structurizr-export JAR cannot be found

        Example:
            >>> diagrams = workspace.to_plantuml()
            >>> for key, puml in diagrams.items():
            ...     print(f"{key}: {len(puml)} chars")
        """
        from buildzr.sinks.plantuml_sink import PlantUmlSink
        merged = self._merged_workspace()
        sink = PlantUmlSink()
        return sink.export_to_dict(merged)

    def to_svg(self, layout: PlantUmlLayout = 'dot') -> Dict[str, str]:
        """
        Return SVG content for all views as a dictionary.

        Uses the official structurizr-export Java library and PlantUML to
        render workspace views as SVG diagrams.

        Args:
            layout: PlantUML layout engine. 'dot' (default) needs Graphviz;
                'smetana' is pure Java and needs no Graphviz.

        Returns:
            Dictionary mapping view keys to SVG content strings.

        Raises:
            ImportError: If jpype1 is not installed (install with: pip install buildzr[export-plantuml])
            FileNotFoundError: If structurizr-export JAR cannot be found
            PlantUmlRenderError: If layout is 'dot' and Graphviz (the `dot`
                executable) is not available, or PlantUML fails to render a view.
            ValueError: If layout is not 'dot' or 'smetana'.

        Example:
            >>> svgs = workspace.to_svg()
            >>> with open('diagram.svg', 'w') as f:
            ...     f.write(svgs['SystemContext'])
        """
        from buildzr.sinks.plantuml_sink import PlantUmlSink
        merged = self._merged_workspace()
        sink = PlantUmlSink()
        return sink.render_to_svg_dict(merged, layout=layout)

    def _repr_html_(self) -> str:
        """
        Jupyter notebook HTML representation with embedded SVG diagrams.

        Displays all workspace views as SVG diagrams stacked vertically,
        each with a heading showing the view key.

        Returns:
            HTML string with embedded SVG diagrams.

        Raises:
            ImportError: If jpype1 is not installed. Install with:
                pip install buildzr[export-plantuml]
        """
        try:
            svgs = self.to_svg()
        except ImportError as e:
            raise ImportError(
                "PlantUML export dependencies not installed. "
                "Install with: pip install buildzr[export-plantuml]"
            ) from e

        if not svgs:
            return "<p><em>No views defined in workspace.</em></p>"

        html_parts = []
        for view_key, svg_content in svgs.items():
            html_parts.append(f'<div style="margin-bottom: 2em;">')
            html_parts.append(f'<h3 style="font-family: sans-serif; color: #333;">{view_key}</h3>')
            html_parts.append(svg_content)
            html_parts.append('</div>')

        return '\n'.join(html_parts)

    def _merge_models(
        self,
        parent: buildzr.models.Workspace,
        child: buildzr.models.Workspace
    ) -> buildzr.models.Workspace:
        """
        Merge parent and child workspace models.

        The merged model contains:
        - All elements from parent workspace (with any modifications from child)
        - New elements added in child workspace
        - Relationships from both workspaces

        Args:
            parent: The extended (parent) workspace model
            child: The current (child) workspace model

        Returns:
            A new merged Workspace model
        """
        import copy

        # Start with a copy of the parent model
        merged = copy.deepcopy(parent)

        # Use child's name and description
        merged.name = child.name
        merged.description = child.description

        # Merge software systems
        if child.model and child.model.softwareSystems:
            if merged.model is None:
                merged.model = buildzr.models.Model()
            if merged.model.softwareSystems is None:
                merged.model.softwareSystems = []

            # Get existing IDs from parent
            existing_ids = {ss.id for ss in merged.model.softwareSystems}

            for ss in child.model.softwareSystems:
                if ss.id not in existing_ids:
                    merged.model.softwareSystems.append(ss)

        # Merge people
        if child.model and child.model.people:
            if merged.model is None:
                merged.model = buildzr.models.Model()
            if merged.model.people is None:
                merged.model.people = []

            existing_ids = {p.id for p in merged.model.people}

            for person in child.model.people:
                if person.id not in existing_ids:
                    merged.model.people.append(person)

        # Merge deployment nodes
        if child.model and child.model.deploymentNodes:
            if merged.model is None:
                merged.model = buildzr.models.Model()
            if merged.model.deploymentNodes is None:
                merged.model.deploymentNodes = []

            existing_ids = {dn.id for dn in merged.model.deploymentNodes}

            for dn in child.model.deploymentNodes:
                if dn.id not in existing_ids:
                    merged.model.deploymentNodes.append(dn)

        # Merge views if present
        if child.views:
            if merged.views is None:
                merged.views = child.views
            else:
                # Merge individual view types
                if child.views.systemLandscapeViews:
                    if merged.views.systemLandscapeViews is None:
                        merged.views.systemLandscapeViews = []
                    merged.views.systemLandscapeViews.extend(child.views.systemLandscapeViews)

                if child.views.systemContextViews:
                    if merged.views.systemContextViews is None:
                        merged.views.systemContextViews = []
                    merged.views.systemContextViews.extend(child.views.systemContextViews)

                if child.views.containerViews:
                    if merged.views.containerViews is None:
                        merged.views.containerViews = []
                    merged.views.containerViews.extend(child.views.containerViews)

                if child.views.componentViews:
                    if merged.views.componentViews is None:
                        merged.views.componentViews = []
                    merged.views.componentViews.extend(child.views.componentViews)

                if child.views.deploymentViews:
                    if merged.views.deploymentViews is None:
                        merged.views.deploymentViews = []
                    merged.views.deploymentViews.extend(child.views.deploymentViews)

        return merged


    def _add_dynamic_attr(self, name: str, model: Union['Person', 'SoftwareSystem', 'Element']) -> None:
        if isinstance(model, Person):
            self._dynamic_attrs[_child_name_transform(name)] = model
            if model._label:
                self._dynamic_attrs[_child_name_transform(model._label)] = model
        elif isinstance(model, SoftwareSystem):
            self._dynamic_attrs[_child_name_transform(name)] = model
            if model._label:
                self._dynamic_attrs[_child_name_transform(model._label)] = model
        elif isinstance(model, Element):
            self._dynamic_attrs[_child_name_transform(name)] = model
            if model._label:
                self._dynamic_attrs[_child_name_transform(model._label)] = model
        else:
            raise ValueError('Invalid element type: Trying to add an element of type {} to a workspace.'.format(type(model)))

    def __getattr__(self, name: str) -> Union['Person', 'SoftwareSystem', 'Element']:
        try:
            return self._dynamic_attrs[name]
        except KeyError:
            raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")

    def __getitem__(self, name: str) -> Union['Person', 'SoftwareSystem', 'Element']:
        # Handle integer keys from failed tuple unpacking attempts
        if isinstance(name, int):
            raise TypeError(
                "Cannot unpack workspace - not extending another workspace. "
                "Use extend='path/to/workspace.json' to enable tuple unpacking."
            )
        return self._dynamic_attrs[_child_name_transform(name)]

    def __dir__(self) -> Iterable[str]:
        return list(super().__dir__()) + list(self._dynamic_attrs.keys())

__enter__

__enter__() -> Self

Enter the workspace context.

Source code in buildzr/dsl/dsl.py
def __enter__(self) -> Self:
    """Enter the workspace context."""
    self._token = _current_workspace.set(self)
    return self

apply_theme

apply_theme(theme: Union[str, type]) -> None

Use a Structurizr theme: its element and relationship styles come before the workspace’s own styles. Themes apply in the order they are added.

Parameters:

Name Type Description Default
theme Union[str, type]

The URL of the theme’s theme.json, or a theme of buildzr.themes (for example AWS or KUBERNETES_V0_3).

required
Source code in buildzr/dsl/dsl.py
def apply_theme(self, theme: Union[str, type]) -> None:
    """
    Use a Structurizr theme: its element and relationship styles come before
    the workspace's own styles. Themes apply in the order they are added.

    Args:
        theme: The URL of the theme's `theme.json`, or a theme of
            `buildzr.themes` (for example `AWS` or `KUBERNETES_V0_3`).
    """
    url = theme if isinstance(theme, str) else getattr(theme, 'THEME_URL', None)
    if not isinstance(url, str) or not url:
        raise ValueError(f"{theme!r} is not a theme URL or a theme of buildzr.themes")
    configuration = self._configuration()
    configuration.themes = (configuration.themes or []) + [url]

apply_branding

apply_branding(logo: Optional[str] = None, font: Optional[str] = None, font_url: Optional[str] = None) -> None

Set the workspace’s branding. Arguments left as None keep their value.

Parameters:

Name Type Description Default
logo Optional[str]

The logo, as a data URI of a PNG, JPEG, GIF, WebP or SVG image.

None
font Optional[str]

The font of the diagram text: a family name or a CSS font stack, such as "Georgia, serif".

None
font_url Optional[str]

The URL of a web font, kept in the workspace for other Structurizr tools; the buildzr viewer does not load it.

None
Source code in buildzr/dsl/dsl.py
def apply_branding(
    self,
    logo: Optional[str]=None,
    font: Optional[str]=None,
    font_url: Optional[str]=None,
) -> None:
    """
    Set the workspace's branding. Arguments left as `None` keep their value.

    Args:
        logo: The logo, as a data URI of a PNG, JPEG, GIF, WebP or SVG image.
        font: The font of the diagram text: a family name or a CSS font
            stack, such as `"Georgia, serif"`.
        font_url: The URL of a web font, kept in the workspace for other
            Structurizr tools; the buildzr viewer does not load it.
    """
    configuration = self._configuration()
    if not configuration.branding:
        configuration.branding = buildzr.models.Branding()
    branding = configuration.branding
    if logo is not None:
        branding.logo = logo
    if font is not None or font_url is not None:
        if not branding.font:
            branding.font = buildzr.models.Font()
        if font is not None:
            branding.font.name = font
        if font_url is not None:
            branding.font.url = font_url

to_dict

to_dict() -> Dict[str, Any]

Return workspace as a JSON-serializable dictionary.

This method is useful for programmatic access to the workspace data and for Jupyter notebook display.

Returns:

Type Description
Dict[str, Any]

Dictionary representation of the workspace with camelCase keys.

Example

data = workspace.to_dict() print(data[‘name’])

Source code in buildzr/dsl/dsl.py
def to_dict(self) -> Dict[str, Any]:
    """
    Return workspace as a JSON-serializable dictionary.

    This method is useful for programmatic access to the workspace data
    and for Jupyter notebook display.

    Returns:
        Dictionary representation of the workspace with camelCase keys.

    Example:
        >>> data = workspace.to_dict()
        >>> print(data['name'])
    """
    import json
    from buildzr.encoders.encoder import JsonEncoder
    merged = self._merged_workspace()
    return cast(Dict[str, Any], json.loads(JsonEncoder().encode(merged)))

save

save(format: SaveFormat = 'json', path: Optional[Union[str, Path]] = None, pretty: bool = False, layout: PlantUmlLayout = 'dot') -> Union[str, List[str]]

Save workspace to file(s) in the specified format.

.. deprecated:: Use the buildzr export command instead, which writes every workspace of a model file in JSON, PlantUML, SVG and PNG. This method keeps working and emits a :class:DeprecationWarning. To get the content in memory, use :meth:to_json, :meth:to_plantuml and :meth:to_svg.

Parameters:

Name Type Description Default
format SaveFormat

Output format. One of: - ‘json’: Single JSON file (default) - ‘plantuml’: PlantUML .puml files (one per view) - ‘svg’: SVG image files (one per view) - ‘png’: PNG image files (one per view)

'json'
path Optional[Union[str, Path]]

Output path. Behavior depends on format: - For ‘json’: file path (defaults to ‘{cwd}/{workspace_name}.json’) - For diagram formats: directory path (defaults to ‘{cwd}/’)

None
pretty bool

For ‘json’ format only, whether to indent output.

False
layout PlantUmlLayout

PlantUML layout engine for ‘plantuml’/’svg’/’png’; ignored for ‘json’. One of: - ‘dot’: Use Graphviz for layout (default). svg/png need Graphviz. - ‘smetana’: Pure-Java layout, no Graphviz needed (layout quality is somewhat worse). Adds !pragma layout smetana to the written .puml files as well.

'dot'

Returns:

Type Description
Union[str, List[str]]
  • For ‘json’: The path to the written file (str)
Union[str, List[str]]
  • For diagram formats: List of paths to written files

Raises:

Type Description
ImportError

If ‘plantuml’/’svg’/’png’ format requested but jpype1 not installed. Install with: pip install buildzr[export-plantuml]

PlantUmlRenderError

If ‘svg’/’png’ format requested with layout ‘dot’ but Graphviz (the dot executable) is not available, or PlantUML fails to render a view. A missing Graphviz is reported before any file is written.

ValueError

If format or layout is not recognized.

Example

w.save() # Saves to ./my_workspace.json w.save(format=’json’, path=’output/arch.json’, pretty=True) w.save(format=’plantuml’, path=’diagrams/’) w.save(format=’svg’) # Saves to ./ w.save(format=’svg’, path=’out/’, layout=’smetana’) # No Graphviz

Source code in buildzr/dsl/dsl.py
def save(
    self,
    format: SaveFormat = 'json',
    path: Optional[Union[str, Path]] = None,
    pretty: bool = False,
    layout: PlantUmlLayout = 'dot',
) -> Union[str, List[str]]:
    """
    Save workspace to file(s) in the specified format.

    .. deprecated::
        Use the ``buildzr export`` command instead, which writes every
        workspace of a model file in JSON, PlantUML, SVG and PNG. This
        method keeps working and emits a :class:`DeprecationWarning`.
        To get the content in memory, use :meth:`to_json`,
        :meth:`to_plantuml` and :meth:`to_svg`.

    Args:
        format: Output format. One of:
            - 'json': Single JSON file (default)
            - 'plantuml': PlantUML .puml files (one per view)
            - 'svg': SVG image files (one per view)
            - 'png': PNG image files (one per view)
        path: Output path. Behavior depends on format:
            - For 'json': file path (defaults to '{cwd}/{workspace_name}.json')
            - For diagram formats: directory path (defaults to '{cwd}/')
        pretty: For 'json' format only, whether to indent output.
        layout: PlantUML layout engine for 'plantuml'/'svg'/'png'; ignored
            for 'json'. One of:
            - 'dot': Use Graphviz for layout (default). svg/png need Graphviz.
            - 'smetana': Pure-Java layout, no Graphviz needed (layout quality
              is somewhat worse). Adds ``!pragma layout smetana`` to the
              written .puml files as well.

    Returns:
        - For 'json': The path to the written file (str)
        - For diagram formats: List of paths to written files

    Raises:
        ImportError: If 'plantuml'/'svg'/'png' format requested but jpype1
            not installed. Install with: pip install buildzr[export-plantuml]
        PlantUmlRenderError: If 'svg'/'png' format requested with layout
            'dot' but Graphviz (the `dot` executable) is not available, or
            PlantUML fails to render a view. A missing Graphviz is reported
            before any file is written.
        ValueError: If format or layout is not recognized.

    Example:
        >>> w.save()  # Saves to ./my_workspace.json
        >>> w.save(format='json', path='output/arch.json', pretty=True)
        >>> w.save(format='plantuml', path='diagrams/')
        >>> w.save(format='svg')  # Saves to ./
        >>> w.save(format='svg', path='out/', layout='smetana')  # No Graphviz
    """
    warnings.warn(
        "Workspace.save() is deprecated and will be removed in a future "
        "release; use the `buildzr export` command instead "
        "(see https://buildzr.dev/user-guide/export/)",
        DeprecationWarning,
        stacklevel=2,
    )
    return self._save(format, path, pretty, layout)

to_json

to_json(pretty: bool = True) -> str

Return workspace as a JSON string.

Parameters:

Name Type Description Default
pretty bool

If True (default), format with 2-space indentation.

True

Returns:

Type Description
str

JSON string representation of the workspace.

Example

json_str = workspace.to_json() print(json_str)

Source code in buildzr/dsl/dsl.py
def to_json(self, pretty: bool = True) -> str:
    """
    Return workspace as a JSON string.

    Args:
        pretty: If True (default), format with 2-space indentation.

    Returns:
        JSON string representation of the workspace.

    Example:
        >>> json_str = workspace.to_json()
        >>> print(json_str)
    """
    from buildzr.encoders.encoder import JsonEncoder
    merged = self._merged_workspace()
    indent = 2 if pretty else None
    return JsonEncoder(indent=indent).encode(merged)

to_plantuml

to_plantuml() -> Dict[str, str]

Return PlantUML source for all views as a dictionary.

Uses the official structurizr-export Java library via JPype to generate C4-PlantUML diagrams from workspace views.

Returns:

Type Description
Dict[str, str]

Dictionary mapping view keys to PlantUML source strings.

Raises:

Type Description
ImportError

If jpype1 is not installed (install with: pip install buildzr[export-plantuml])

FileNotFoundError

If structurizr-export JAR cannot be found

Example

diagrams = workspace.to_plantuml() for key, puml in diagrams.items(): … print(f”{key}: {len(puml)} chars”)

Source code in buildzr/dsl/dsl.py
def to_plantuml(self) -> Dict[str, str]:
    """
    Return PlantUML source for all views as a dictionary.

    Uses the official structurizr-export Java library via JPype to generate
    C4-PlantUML diagrams from workspace views.

    Returns:
        Dictionary mapping view keys to PlantUML source strings.

    Raises:
        ImportError: If jpype1 is not installed (install with: pip install buildzr[export-plantuml])
        FileNotFoundError: If structurizr-export JAR cannot be found

    Example:
        >>> diagrams = workspace.to_plantuml()
        >>> for key, puml in diagrams.items():
        ...     print(f"{key}: {len(puml)} chars")
    """
    from buildzr.sinks.plantuml_sink import PlantUmlSink
    merged = self._merged_workspace()
    sink = PlantUmlSink()
    return sink.export_to_dict(merged)

to_svg

to_svg(layout: PlantUmlLayout = 'dot') -> Dict[str, str]

Return SVG content for all views as a dictionary.

Uses the official structurizr-export Java library and PlantUML to render workspace views as SVG diagrams.

Parameters:

Name Type Description Default
layout PlantUmlLayout

PlantUML layout engine. ‘dot’ (default) needs Graphviz; ‘smetana’ is pure Java and needs no Graphviz.

'dot'

Returns:

Type Description
Dict[str, str]

Dictionary mapping view keys to SVG content strings.

Raises:

Type Description
ImportError

If jpype1 is not installed (install with: pip install buildzr[export-plantuml])

FileNotFoundError

If structurizr-export JAR cannot be found

PlantUmlRenderError

If layout is ‘dot’ and Graphviz (the dot executable) is not available, or PlantUML fails to render a view.

ValueError

If layout is not ‘dot’ or ‘smetana’.

Example

svgs = workspace.to_svg() with open(‘diagram.svg’, ‘w’) as f: … f.write(svgs[‘SystemContext’])

Source code in buildzr/dsl/dsl.py
def to_svg(self, layout: PlantUmlLayout = 'dot') -> Dict[str, str]:
    """
    Return SVG content for all views as a dictionary.

    Uses the official structurizr-export Java library and PlantUML to
    render workspace views as SVG diagrams.

    Args:
        layout: PlantUML layout engine. 'dot' (default) needs Graphviz;
            'smetana' is pure Java and needs no Graphviz.

    Returns:
        Dictionary mapping view keys to SVG content strings.

    Raises:
        ImportError: If jpype1 is not installed (install with: pip install buildzr[export-plantuml])
        FileNotFoundError: If structurizr-export JAR cannot be found
        PlantUmlRenderError: If layout is 'dot' and Graphviz (the `dot`
            executable) is not available, or PlantUML fails to render a view.
        ValueError: If layout is not 'dot' or 'smetana'.

    Example:
        >>> svgs = workspace.to_svg()
        >>> with open('diagram.svg', 'w') as f:
        ...     f.write(svgs['SystemContext'])
    """
    from buildzr.sinks.plantuml_sink import PlantUmlSink
    merged = self._merged_workspace()
    sink = PlantUmlSink()
    return sink.render_to_svg_dict(merged, layout=layout)

Person

Bases: DslElementRelationOverrides['Person', Union['Person', 'SoftwareSystem', 'Container', 'Component', 'Element']]

A person who uses a software system.

Source code in buildzr/dsl/dsl.py
class Person(DslElementRelationOverrides[
    'Person',
    Union[
        'Person',
        'SoftwareSystem',
        'Container',
        'Component',
        'Element',
    ]
]):
    """
    A person who uses a software system.
    """

    @property
    def model(self) -> buildzr.models.Person:
        return self._m

    @property
    def parent(self) -> Optional[Workspace]:
        return self._parent

    @property
    def children(self) -> None:
        """
        The `Person` element does not have any children, and will always return
        `None`.
        """
        return None

    @property
    def sources(self) -> List[DslElement]:
        return self._sources

    @property
    def destinations(self) -> List[DslElement]:
        return self._destinations

    @property
    def relationships(self) -> Set[DslRelationship]:
        return self._relationships

    @property
    def tags(self) -> Set[str]:
        return self._tags

    def __init__(self, name: str, description: str="", tags: Set[str]=set(), properties: Dict[str, Any]=dict()) -> None:
        self._m = buildzr.models.Person()
        self._parent: Optional[Workspace] = None
        self._sources: List[DslElement] = []
        self._destinations: List[DslElement] = []
        self._relationships: Set[DslRelationship] = OrderedSet()
        self._tags = OrderedTags(['Element', 'Person'], tags)
        self._label: Optional[str] = None
        self.model.id = GenerateId.for_element()
        self.model.name = name
        self.model.description = description
        self.model.relationships = []
        self.model.tags = ','.join(self._tags)
        self.model.properties = properties
        # Note: location is deprecated in Structurizr - use tags instead for styling
        self.model.location = buildzr.models.Location.Unspecified

        workspace = _current_workspace.get()
        if workspace is not None:
            workspace.add_model(self)

        stack = _current_group_stack.get()
        if stack:
            stack[-1].add_element(self)

    def labeled(self, label: str) -> 'Person':
        self._label = label
        workspace = _current_workspace.get()
        if workspace is not None:
            workspace._add_dynamic_attr(label, self)
        return self

    @classmethod
    def _from_model(cls, model: buildzr.models.Person) -> 'Person':
        """Create DSL wrapper from existing model (for workspace extension)."""
        instance = object.__new__(cls)
        instance._m = model
        instance._parent = None
        instance._sources = []
        instance._destinations = []
        instance._relationships = OrderedSet()
        instance._tags = OrderedTags(model.tags.split(',') if model.tags else ['Element', 'Person'])
        instance._label = None
        return instance

children property

children: None

The Person element does not have any children, and will always return None.

SoftwareSystem

Bases: DslElementRelationOverrides['SoftwareSystem', Union['Person', 'SoftwareSystem', 'Container', 'Component', 'Element']]

A software system.

Source code in buildzr/dsl/dsl.py
class SoftwareSystem(DslElementRelationOverrides[
    'SoftwareSystem',
    Union[
        'Person',
        'SoftwareSystem',
        'Container',
        'Component',
        'Element',
    ]
]):
    """
    A software system.
    """

    @property
    def model(self) -> buildzr.models.SoftwareSystem:
        return self._m

    @property
    def parent(self) -> Optional[Workspace]:
        return self._parent

    @property
    def children(self) -> Optional[List['Container']]:
        return self._children

    @property
    def sources(self) -> List[DslElement]:
        return self._sources

    @property
    def destinations(self) -> List[DslElement]:
        return self._destinations

    @property
    def relationships(self) -> Set[DslRelationship]:
        return self._relationships

    @property
    def tags(self) -> Set[str]:
        return self._tags

    def __init__(self, name: str, description: str="", tags: Set[str]=set(), properties: Dict[str, Any]=dict()) -> None:
        self._m = buildzr.models.SoftwareSystem()
        self.model.containers = []
        self._parent: Optional[Workspace] = None
        self._children: Optional[List['Container']] = []
        self._sources: List[DslElement] = []
        self._destinations: List[DslElement] = []
        self._relationships: Set[DslRelationship] = OrderedSet()
        self._tags = OrderedTags(['Element', 'Software System'], tags)
        self._dynamic_attrs: Dict[str, 'Container'] = {}
        self._label: Optional[str] = None
        self.model.id = GenerateId.for_element()
        self.model.name = name
        self.model.description = description
        self.model.relationships = []
        self.model.tags = ','.join(self._tags)
        self.model.properties = properties
        # Note: location is deprecated in Structurizr - use tags instead for styling
        self.model.location = buildzr.models.Location1.Unspecified
        self.model.documentation = buildzr.models.Documentation()

        workspace = _current_workspace.get()
        if workspace is not None:
            workspace.add_model(self)
            workspace._add_dynamic_attr(self.model.name, self)

        stack = _current_group_stack.get()
        if stack:
            stack[-1].add_element(self)

    @classmethod
    def _from_model(cls, model: buildzr.models.SoftwareSystem) -> 'SoftwareSystem':
        """Create DSL wrapper from existing model (for workspace extension)."""
        instance = object.__new__(cls)
        instance._m = model
        instance._parent = None
        instance._children = []
        instance._sources = []
        instance._destinations = []
        instance._relationships = OrderedSet()
        instance._tags = OrderedTags(model.tags.split(',') if model.tags else ['Element', 'Software System'])
        instance._dynamic_attrs = {}
        instance._label = None

        # Ensure containers list is initialized for adding new containers
        if instance._m.containers is None:
            instance._m.containers = []

        # Wrap child containers
        if model.containers:
            for container_model in model.containers:
                container = Container._from_model(container_model, instance)
                instance._children.append(container)
                instance._dynamic_attrs[_child_name_transform(container_model.name or '')] = container

        return instance

    def __enter__(self) -> Self:
        self._token = _current_software_system.set(self)
        return self

    def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[Any]) -> None:
        _current_software_system.reset(self._token)

    def container(self) -> TypedDynamicAttribute['Container']:
        return TypedDynamicAttribute['Container'](self._dynamic_attrs)

    def add_container(self, container: 'Container') -> None:
        if isinstance(container, Container):
            self.model.containers.append(container.model)
            container._parent = self
            self._add_dynamic_attr(container.model.name, container)
            self._children.append(container)
        else:
            raise ValueError('Invalid element type: Trying to add an element of type {} to a software system.'.format(type(container)))

    def _add_dynamic_attr(self, name: str, model: 'Container') -> None:
        if isinstance(model, Container):
            self._dynamic_attrs[_child_name_transform(name)] = model
            if model._label:
                self._dynamic_attrs[_child_name_transform(model._label)] = model
        else:
            raise ValueError('Invalid element type: Trying to add an element of type {} to a software system.'.format(type(model)))

    def __getattr__(self, name: str) -> 'Container':
        return self._dynamic_attrs[name]

    def __getitem__(self, name: str) -> 'Container':
        return self._dynamic_attrs[_child_name_transform(name)]

    def __dir__(self) -> Iterable[str]:
        return list(super().__dir__()) + list(self._dynamic_attrs.keys())

    def labeled(self, label: str) -> 'SoftwareSystem':
        self._label = label
        workspace = _current_workspace.get()
        if workspace is not None:
            workspace._add_dynamic_attr(label, self)
        return self

Container

Bases: DslElementRelationOverrides['Container', Union['Person', 'SoftwareSystem', 'Container', 'Component', 'Element']]

A container (something that can execute code or host data).

Source code in buildzr/dsl/dsl.py
class Container(DslElementRelationOverrides[
    'Container',
    Union[
        'Person',
        'SoftwareSystem',
        'Container',
        'Component',
        'Element',
    ]
]):
    """
    A container (something that can execute code or host data).
    """

    @property
    def model(self) -> buildzr.models.Container:
        return self._m

    @property
    def parent(self) -> Optional[SoftwareSystem]:
        return self._parent

    @property
    def children(self) -> Optional[List['Component']]:
        return self._children

    @property
    def sources(self) -> List[DslElement]:
        return self._sources

    @property
    def destinations(self) -> List[DslElement]:
        return self._destinations

    @property
    def relationships(self) -> Set[DslRelationship]:
        return self._relationships

    @property
    def tags(self) -> Set[str]:
        return self._tags

    def __init__(self, name: str, description: str="", technology: str="", tags: Set[str]=set(), properties: Dict[str, Any]=dict()) -> None:
        self._m = buildzr.models.Container()
        self.model.components = []
        self._parent: Optional[SoftwareSystem] = None
        self._children: Optional[List['Component']] = []
        self._sources: List[DslElement] = []
        self._destinations: List[DslElement] = []
        self._relationships: Set[DslRelationship] = OrderedSet()
        self._tags = OrderedTags(['Element', 'Container'], tags)
        self._dynamic_attrs: Dict[str, 'Component'] = {}
        self._label: Optional[str] = None
        self.model.id = GenerateId.for_element()
        self.model.name = name
        self.model.description = description
        self.model.relationships = []
        self.model.technology = technology
        self.model.tags = ','.join(self._tags)
        self.model.properties = properties

        software_system = _current_software_system.get()
        if software_system is not None:
            software_system.add_container(self)
            software_system._add_dynamic_attr(self.model.name, self)

        stack = _current_group_stack.get()
        if stack:
            stack[-1].add_element(self)

    @classmethod
    def _from_model(cls, model: buildzr.models.Container, parent: 'SoftwareSystem') -> 'Container':
        """Create DSL wrapper from existing model (for workspace extension)."""
        instance = object.__new__(cls)
        instance._m = model
        instance._parent = parent
        instance._children = []
        instance._sources = []
        instance._destinations = []
        instance._relationships = OrderedSet()
        instance._tags = OrderedTags(model.tags.split(',') if model.tags else ['Element', 'Container'])
        instance._dynamic_attrs = {}
        instance._label = None

        # Ensure components list is initialized for adding new components
        if instance._m.components is None:
            instance._m.components = []

        # Wrap child components
        if model.components:
            for component_model in model.components:
                component = Component._from_model(component_model, instance)
                instance._children.append(component)
                instance._dynamic_attrs[_child_name_transform(component_model.name or '')] = component

        return instance

    def __enter__(self) -> Self:
        self._token = _current_container.set(self)
        return self

    def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[Any]) -> None:
        _current_container.reset(self._token)

    def labeled(self, label: str) -> 'Container':
        self._label = label
        software_system = _current_software_system.get()
        if software_system is not None:
            software_system._add_dynamic_attr(label, self)
        return self

    def component(self) -> TypedDynamicAttribute['Component']:
        return TypedDynamicAttribute['Component'](self._dynamic_attrs)

    def add_component(self, component: 'Component') -> None:
        if isinstance(component, Component):
            self.model.components.append(component.model)
            component._parent = self
            self._add_dynamic_attr(component.model.name, component)
            self._children.append(component)
        else:
            raise ValueError('Invalid element type: Trying to add an element of type {} to a container.'.format(type(component)))

    def _add_dynamic_attr(self, name: str, model: 'Component') -> None:
        if isinstance(model, Component):
            self._dynamic_attrs[_child_name_transform(name)] = model
            if model._label:
                self._dynamic_attrs[_child_name_transform(model._label)] = model
        else:
            raise ValueError('Invalid element type: Trying to add an element of type {} to a container.'.format(type(model)))

    def __getattr__(self, name: str) -> 'Component':
        return self._dynamic_attrs[name]

    def __getitem__(self, name: str) -> 'Component':
        return self._dynamic_attrs[_child_name_transform(name)]

    def __dir__(self) -> Iterable[str]:
        return list(super().__dir__()) + list(self._dynamic_attrs.keys())

Component

Bases: DslElementRelationOverrides['Component', Union['Person', 'SoftwareSystem', 'Container', 'Component', 'Element']]

A component (a grouping of related functionality behind an interface that runs inside a container).

Source code in buildzr/dsl/dsl.py
class Component(DslElementRelationOverrides[
    'Component',
    Union[
        'Person',
        'SoftwareSystem',
        'Container',
        'Component',
        'Element',
    ]
]):
    """
    A component (a grouping of related functionality behind an interface that runs inside a container).
    """

    @property
    def model(self) -> buildzr.models.Component:
        return self._m

    @property
    def parent(self) -> Optional[Container]:
        return self._parent

    @property
    def children(self) -> None:
        return None

    @property
    def sources(self) -> List[DslElement]:
        return self._sources

    @property
    def destinations(self) -> List[DslElement]:
        return self._destinations

    @property
    def relationships(self) -> Set[DslRelationship]:
        return self._relationships

    @property
    def tags(self) -> Set[str]:
        return self._tags

    def __init__(self, name: str, description: str="", technology: str="", tags: Set[str]=set(), properties: Dict[str, Any]=dict()) -> None:
        self._m = buildzr.models.Component()
        self._parent: Optional[Container] = None
        self._sources: List[DslElement] = []
        self._destinations: List[DslElement] = []
        self._relationships: Set[DslRelationship] = OrderedSet()
        self._tags = OrderedTags(['Element', 'Component'], tags)
        self._label: Optional[str] = None
        self.model.id = GenerateId.for_element()
        self.model.name = name
        self.model.description = description
        self.model.technology = technology
        self.model.relationships = []
        self.model.tags = ','.join(self._tags)
        self.model.properties = properties

        container = _current_container.get()
        if container is not None:
            container.add_component(self)
            container._add_dynamic_attr(self.model.name, self)

        stack = _current_group_stack.get()
        if stack:
            stack[-1].add_element(self)

    def labeled(self, label: str) -> 'Component':
        self._label = label
        container = _current_container.get()
        if container is not None:
            container._add_dynamic_attr(label, self)
        return self

    @classmethod
    def _from_model(cls, model: buildzr.models.Component, parent: 'Container') -> 'Component':
        """Create DSL wrapper from existing model (for workspace extension)."""
        instance = object.__new__(cls)
        instance._m = model
        instance._parent = parent
        instance._sources = []
        instance._destinations = []
        instance._relationships = OrderedSet()
        instance._tags = OrderedTags(model.tags.split(',') if model.tags else ['Element', 'Component'])
        instance._label = None
        return instance

Group

Source code in buildzr/dsl/dsl.py
class Group:

    def __init__(
        self,
        name: str,
        workspace: Optional[Workspace]=None,
    ) -> None:

        if not workspace:
            workspace = _current_workspace.get()
            if workspace is not None:
                self._group_separator = workspace._group_separator

        self._group_separator = workspace._group_separator
        self._name = name

        if len(self._group_separator) > 1:
            raise ValueError('Group separator must be a single character.')

        if self._group_separator in self._name:
            raise ValueError('Group name cannot contain the group separator.')

        stack = _current_group_stack.get()
        new_stack = stack.copy()
        new_stack.extend([self])

        self._full_name = self._group_separator.join([group._name for group in new_stack])

    def full_name(self) -> str:
        return self._full_name

    def add_element(
        self,
        model: Union[
            'Person',
            'SoftwareSystem',
            'Container',
            'Component',
        ]
    ) -> None:


        model.model.group = self._full_name

    def __enter__(self) -> Self:
        stack = _current_group_stack.get() # stack: a/b
        stack.extend([self]) # stack: a/b -> a/b/self
        self._token = _current_group_stack.set(stack)
        return self

    def __exit__(
        self,
        exc_type: Optional[Type[BaseException]],
        exc_value: Optional[BaseException],
        traceback: Optional[Any]
    ) -> None:
        stack = _current_group_stack.get()
        stack.pop() # stack: a/b/self -> a/b
        _current_group_stack.reset(self._token)

Custom Elements

Element

Bases: DslElementRelationOverrides['Element', Union['Person', 'SoftwareSystem', 'Container', 'Component', 'Element']]

A custom element that sits outside the C4 model.

Custom elements can be used to represent components that don’t fit into the standard C4 model hierarchy (e.g., hardware systems, business processes, external services). Custom elements can only be displayed in CustomView.

DSL class name: Element (matches Structurizr DSL syntax) Model class: buildzr.models.CustomElement (matches JSON field name)

Source code in buildzr/dsl/dsl.py
class Element(DslElementRelationOverrides[
    'Element',
    Union[
        'Person',
        'SoftwareSystem',
        'Container',
        'Component',
        'Element',
    ]
]):
    """
    A custom element that sits outside the C4 model.

    Custom elements can be used to represent components that don't fit into
    the standard C4 model hierarchy (e.g., hardware systems, business processes,
    external services). Custom elements can only be displayed in CustomView.

    DSL class name: Element (matches Structurizr DSL syntax)
    Model class: buildzr.models.CustomElement (matches JSON field name)
    """

    @property
    def model(self) -> buildzr.models.CustomElement:
        return self._m

    @property
    def parent(self) -> Optional[Workspace]:
        return self._parent

    @property
    def children(self) -> None:
        """
        The `Element` does not have any children, and will always return
        `None`.
        """
        return None

    @property
    def sources(self) -> List[DslElement]:
        return self._sources

    @property
    def destinations(self) -> List[DslElement]:
        return self._destinations

    @property
    def relationships(self) -> Set[DslRelationship]:
        return self._relationships

    @property
    def tags(self) -> Set[str]:
        return self._tags

    def __init__(
        self,
        name: str,
        metadata: str = "",
        description: str = "",
        tags: Set[str] = set(),
        properties: Dict[str, Any] = dict(),
    ) -> None:
        self._m = buildzr.models.CustomElement()
        self._parent: Optional[Workspace] = None
        self._sources: List[DslElement] = []
        self._destinations: List[DslElement] = []
        self._relationships: Set[DslRelationship] = OrderedSet()
        self._tags = OrderedTags(['Element'], tags)
        self._label: Optional[str] = None
        self.model.id = GenerateId.for_element()
        self.model.name = name
        self.model.metadata = metadata
        self.model.description = description
        self.model.relationships = []
        self.model.tags = ','.join(self._tags)
        self.model.properties = properties

        workspace = _current_workspace.get()
        if workspace is not None:
            workspace.add_model(self)

        # Note: Custom elements (Element) do not support groups in Structurizr,
        # so we don't add them to the group stack

    def labeled(self, label: str) -> 'Element':
        self._label = label
        workspace = _current_workspace.get()
        if workspace is not None:
            workspace._add_dynamic_attr(label, self)
        return self

    @classmethod
    def _from_model(cls, model: buildzr.models.CustomElement) -> 'Element':
        """Create DSL wrapper from existing model (for workspace extension)."""
        instance = object.__new__(cls)
        instance._m = model
        instance._parent = None
        instance._sources = []
        instance._destinations = []
        instance._relationships = OrderedSet()
        instance._tags = OrderedTags(model.tags.split(',') if model.tags else ['Element'])
        instance._label = None
        return instance

children property

children: None

The Element does not have any children, and will always return None.

Deployment Elements

DeploymentEnvironment

Bases: DslDeploymentEnvironment

Source code in buildzr/dsl/dsl.py
class DeploymentEnvironment(DslDeploymentEnvironment):

    def __init__(self, name: str) -> None:
        self._name = name
        self._parent: Optional[Workspace] = None
        self._children: Optional[List['DeploymentNode']] = []

        workspace = _current_workspace.get()
        if workspace is not None:
            self._parent = workspace

    @property
    def name(self) -> str:
        return self._name

    @property
    def parent(self) -> Optional[Workspace]:
        return self._parent

    @property
    def children(self) -> Optional[List['DeploymentNode']]:
        return self._children

    def add_deployment_node(self, node: 'DeploymentNode') -> None:
        node._m.environment = self._name

    def __enter__(self) -> Self:
        self._token = _current_deployment_environment.set(self)
        return self

    def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[Any]) -> None:
        _current_deployment_environment.reset(self._token)

        if self._parent is not None:
            self._imply_software_system_instance_relationships(self._parent)
            self._imply_container_instance_relationships(self._parent)

    def _imply_software_system_instance_relationships(self, workspace: Workspace) -> None:

        from buildzr.dsl.expression import Expression

        """
        Process implied instance relationships. For example, if we have a
        relationship between two software systems, and the software system
        instances of those software systems exists, then we need to create a
        new relationship between those software system instances.

        These implied relationships are used in `DeploymentView`.

        Relationships are only created between instances that share at least
        one common deployment group. If no deployment groups are specified,
        instances are considered to be in the same default group.
        """

        software_instances = [
            cast('SoftwareSystemInstance', e) for e in Expression(include_elements=[
                lambda w, e: e.type == SoftwareSystemInstance,
            ]).elements(workspace)
        ]

        software_instance_map: Dict[str, List['SoftwareSystemInstance']] = {}
        for software_instance in software_instances:
            software_id = software_instance.model.softwareSystemId
            if software_id not in software_instance_map:
                software_instance_map[software_id] = []
            software_instance_map[software_id].append(software_instance)

        softwares = [
            cast('SoftwareSystem', e) for e in Expression(include_elements=[
                lambda w, e: e.type == SoftwareSystem,
            ]).elements(workspace)
        ]

        for software in softwares:

            other_softwares_ids = {
                s.model.id for s in softwares
                if s.model.id != software.model.id
            }

            if not software.model.relationships:
                continue

            for relationship in software.model.relationships:
                if not relationship.destinationId in other_softwares_ids:
                    continue

                if software.model.id not in software_instance_map:
                    continue

                if relationship.destinationId not in software_instance_map:
                    continue

                this_software_instances = software_instance_map[software.model.id]
                other_software_instances = software_instance_map[relationship.destinationId]

                for this_software_instance in this_software_instances:
                    for other_software_instance in other_software_instances:

                        # Only create relationship if instances share a deployment group
                        if not self._instances_share_deployment_group(
                            this_software_instance,
                            other_software_instance
                        ):
                            continue

                        already_exists = this_software_instance.model.relationships is not None and any(
                            r.sourceId == this_software_instance.model.id and
                            r.destinationId == other_software_instance.model.id and
                            r.description == relationship.description and
                            r.technology == relationship.technology
                            for r in this_software_instance.model.relationships
                        )

                        if not already_exists:
                            # Note: tags aren't carried over.
                            r = this_software_instance.uses(
                                other_software_instance,
                                description=relationship.description,
                                technology=relationship.technology,
                            )
                            r.model.linkedRelationshipId = relationship.id

    def _instances_share_deployment_group(
        self,
        instance1: Union['ContainerInstance', 'SoftwareSystemInstance'],
        instance2: Union['ContainerInstance', 'SoftwareSystemInstance']
    ) -> bool:
        """
        Check if two deployment instances share at least one common deployment group.

        If either instance has no deployment groups specified, they are considered
        to be in the "default" group and can relate to all other instances without
        deployment groups.

        Args:
            instance1: First deployment instance
            instance2: Second deployment instance

        Returns:
            True if instances share at least one deployment group or if both have
            no deployment groups specified, False otherwise.
        """
        groups1 = set(instance1.model.deploymentGroups or [])
        groups2 = set(instance2.model.deploymentGroups or [])

        # If both have no deployment groups, they can relate
        if not groups1 and not groups2:
            return True

        # If one has groups and the other doesn't, they cannot relate
        if (groups1 and not groups2) or (not groups1 and groups2):
            return False

        # Check if they share at least one common group
        return bool(groups1.intersection(groups2))

    def _imply_container_instance_relationships(self, workspace: Workspace) -> None:

        """
        Process implied instance relationships. For example, if we have a
        relationship between two containers, and the container instances of
        those containers exists, then we need to create a new relationship
        between those container instances.

        These implied relationships are used in `DeploymentView`.

        Relationships are only created between instances that share at least
        one common deployment group. If no deployment groups are specified,
        instances are considered to be in the same default group.
        """

        from buildzr.dsl.expression import Expression

        container_instances = [
            cast('ContainerInstance', e) for e in Expression(include_elements=[
                lambda w, e: e.type == ContainerInstance,
        ]).elements(workspace)]

        container_instance_map: Dict[str, List['ContainerInstance']] = {}
        for container_instance in container_instances:
            container_id = container_instance.model.containerId
            if container_id not in container_instance_map:
                container_instance_map[container_id] = []
            container_instance_map[container_id].append(container_instance)

        containers = [
            cast('ContainerInstance', e) for e in Expression(include_elements=[
                lambda w, e: e.type == Container,
        ]).elements(workspace)]

        for container in containers:

            other_containers_ids = {
                c.model.id for c in containers
                if c.model.id != container.model.id
            }

            if not container.model.relationships:
                continue

            for relationship in container.model.relationships:

                if not relationship.destinationId in other_containers_ids:
                    continue

                if container.model.id not in container_instance_map:
                    continue

                if relationship.destinationId not in container_instance_map:
                    continue

                this_container_instances = container_instance_map[container.model.id]
                other_container_instances = container_instance_map[relationship.destinationId]

                for this_container_instance in this_container_instances:
                    for other_container_instance in other_container_instances:

                        # Only create relationship if instances share a deployment group
                        if not self._instances_share_deployment_group(
                            this_container_instance,
                            other_container_instance
                        ):
                            continue

                        already_exists = this_container_instance.model.relationships is not None and any(
                            r.sourceId == this_container_instance.model.id and
                            r.destinationId == other_container_instance.model.id and
                            r.description == relationship.description and
                            r.technology == relationship.technology
                            for r in this_container_instance.model.relationships
                        )

                        if not already_exists:
                            # Note: tags aren't carried over.
                            r = this_container_instance.uses(
                                other_container_instance,
                                description=relationship.description,
                                technology=relationship.technology,
                            )
                            r.model.linkedRelationshipId = relationship.id

DeploymentNode

Bases: DslDeploymentNodeElement, DslElementRelationOverrides['DeploymentNode', 'DeploymentNode']

Source code in buildzr/dsl/dsl.py
class DeploymentNode(DslDeploymentNodeElement, DslElementRelationOverrides[
    'DeploymentNode',
    'DeploymentNode'
]):

    def __init__(self, name: str, description: str="", technology: str="", tags: Set[str]=set(), instances: str="1") -> None:
        self._m = buildzr.models.DeploymentNode()
        self._m.instances = instances
        self._m.id = GenerateId.for_element()
        self._m.name = name
        self._m.children = []
        self._m.softwareSystemInstances = []
        self._m.containerInstances = []
        self._m.infrastructureNodes = []
        self._m.description = description
        self._m.technology = technology
        self._parent: Optional[Workspace] = None
        self._children: Optional[List[
            Union[
                'SoftwareSystemInstance',
                'ContainerInstance',
                'InfrastructureNode',
                'DeploymentNode']]
            ] = []
        self._tags = OrderedTags(['Element', 'Deployment Node'], tags)
        self._m.tags = ','.join(self._tags)

        self._sources: List[DslElement] = []
        self._destinations: List[DslElement] = []
        self._relationships: Set[DslRelationship] = OrderedSet()

        # If the deployment stack is not empty, then we're inside the context of
        # another deployment node. Otherwise, we're at the root of the
        # workspace.
        stack = _current_deployment_node_stack.get()
        if stack:
            stack[-1].add_deployment_node(self)
        else:
            workspace = _current_workspace.get()
            if workspace:
                self._parent = workspace
                workspace.add_model(self)

        deployment_environment = _current_deployment_environment.get()
        if deployment_environment is not None:
            self._m.environment = deployment_environment.name
            deployment_environment.add_deployment_node(self)

    @property
    def model(self) -> buildzr.models.DeploymentNode:
        return self._m

    @property
    def tags(self) -> Set[str]:
        return self._tags

    @property
    def parent(self) -> Optional[Workspace]:
        return self._parent

    @property
    def children(self) -> Optional[List[Union['SoftwareSystemInstance', 'ContainerInstance', 'InfrastructureNode', 'DeploymentNode']]]:
        return self._children

    @property
    def destinations(self) -> List[DslElement]:
        return self._destinations

    @property
    def sources(self) -> List[DslElement]:
        return self._sources

    @property
    def relationships(self) -> Set[DslRelationship]:
        return self._relationships

    def __enter__(self) -> Self:
        stack = _current_deployment_node_stack.get()
        stack.extend([self])
        self._token = _current_deployment_node_stack.set(stack)
        return self

    def __exit__(
        self,
        exc_type: Optional[Type[BaseException]],
        exc_value: Optional[BaseException],
        traceback: Optional[Any]
    ) -> None:
        stack = _current_deployment_node_stack.get()
        stack.pop()
        _current_deployment_node_stack.reset(self._token)

    def add_infrastructure_node(self, node: 'InfrastructureNode') -> None:
        self._m.infrastructureNodes.append(node.model)
        self._children.append(node)

    def add_element_instance(self, instance: Union['SoftwareSystemInstance', 'ContainerInstance']) -> None:
        if isinstance(instance, SoftwareSystemInstance):
            self._m.softwareSystemInstances.append(instance.model)
        elif isinstance(instance, ContainerInstance):
            self._m.containerInstances.append(instance.model)
        self._children.append(instance)

    def add_deployment_node(self, node: 'DeploymentNode') -> None:
        self._m.children.append(node.model)
        self._children.append(node)

ContainerInstance

Bases: DslElementInstance, DslElementRelationOverrides['ContainerInstance', 'InfrastructureNode']

Source code in buildzr/dsl/dsl.py
class ContainerInstance(DslElementInstance, DslElementRelationOverrides[
    'ContainerInstance',
    'InfrastructureNode',
]):

    def __init__(
        self,
        container: 'Container',
        deployment_groups: Optional[List['DeploymentGroup']]=None,
        tags: Set[str]=set(),
    ) -> None:
        self._m = buildzr.models.ContainerInstance()
        self._m.id = GenerateId.for_element()
        self._m.containerId = container.model.id
        self._parent: Optional[DeploymentNode] = None
        self._element = container
        self._m.deploymentGroups = [g.name for g in deployment_groups] if deployment_groups else ["Default"]
        self._tags = OrderedTags(['Container Instance'], tags)
        self._m.tags = ','.join(self._tags)

        self._sources: List[DslElement] = []
        self._destinations: List[DslElement] = []
        self._relationships: Set[DslRelationship] = OrderedSet()

        stack = _current_deployment_node_stack.get()
        if stack:
            self._parent = stack[-1]
            self._parent.add_element_instance(self)

        deployment_environment = _current_deployment_environment.get()
        if deployment_environment is not None:
            self._m.environment = deployment_environment.name

    @property
    def model(self) -> buildzr.models.ContainerInstance:
        return self._m

    @property
    def tags(self) -> Set[str]:
        return self._tags

    @property
    def parent(self) -> Optional[DeploymentNode]:
        return self._parent

    @property
    def children(self) -> None:
        """
        The `ContainerInstance` element does not have any children, and will always return
        `None`.
        """
        return None

    @property
    def sources(self) -> List[DslElement]:
        return self._sources

    @property
    def destinations(self) -> List[DslElement]:
        return self._destinations

    @property
    def relationships(self) -> Set[DslRelationship]:
        return self._relationships

    @property
    def element(self) -> DslElement:
        return self._element

children property

children: None

The ContainerInstance element does not have any children, and will always return None.

SoftwareSystemInstance

Bases: DslElementInstance, DslElementRelationOverrides['SoftwareSystemInstance', 'InfrastructureNode']

Source code in buildzr/dsl/dsl.py
class SoftwareSystemInstance(DslElementInstance, DslElementRelationOverrides[
    'SoftwareSystemInstance',
    'InfrastructureNode',
]):

    def __init__(
        self,
        software_system: 'SoftwareSystem',
        deployment_groups: Optional[List['DeploymentGroup']]=None,
        tags: Set[str]=set(),
    ) -> None:
        self._m = buildzr.models.SoftwareSystemInstance()
        self._m.id = GenerateId.for_element()
        self._m.softwareSystemId = software_system.model.id
        self._parent: Optional[DeploymentNode] = None
        self._element = software_system
        self._m.deploymentGroups = [g.name for g in deployment_groups] if deployment_groups else ["Default"]
        self._tags = OrderedTags(['Software System Instance'], tags)
        self._m.tags = ','.join(self._tags)

        self._sources: List[DslElement] = []
        self._destinations: List[DslElement] = []
        self._relationships: Set[DslRelationship] = OrderedSet()

        stack = _current_deployment_node_stack.get()
        if stack:
            self._parent = stack[-1]
            self._parent.add_element_instance(self)

        deployment_environment = _current_deployment_environment.get()
        if deployment_environment is not None:
            self._m.environment = deployment_environment.name

    @property
    def model(self) -> buildzr.models.SoftwareSystemInstance:
        return self._m

    @property
    def tags(self) -> Set[str]:
        return self._tags

    @property
    def parent(self) -> Optional[DeploymentNode]:
        return self._parent

    @property
    def children(self) -> None:
        """
        The `SoftwareSystemInstance` element does not have any children, and will always return
        `None`.
        """
        return None

    @property
    def destinations(self) -> List[DslElement]:
        return self._destinations

    @property
    def sources(self) -> List[DslElement]:
        return self._sources

    @property
    def relationships(self) -> Set[DslRelationship]:
        return self._relationships

    @property
    def element(self) -> DslElement:
        return self._element

children property

children: None

The SoftwareSystemInstance element does not have any children, and will always return None.

InfrastructureNode

Bases: DslInfrastructureNodeElement, DslElementRelationOverrides['InfrastructureNode', Union['DeploymentNode', 'InfrastructureNode', 'SoftwareSystemInstance', 'ContainerInstance']]

Source code in buildzr/dsl/dsl.py
class InfrastructureNode(DslInfrastructureNodeElement, DslElementRelationOverrides[
    'InfrastructureNode',
    Union[
        'DeploymentNode',
        'InfrastructureNode',
        'SoftwareSystemInstance',
        'ContainerInstance',
    ]
]):

    def __init__(self, name: str, description: str="", technology: str="", tags: Set[str]=set(), properties: Dict[str, Any]=dict()) -> None:
        self._m = buildzr.models.InfrastructureNode()
        self._m.id = GenerateId.for_element()
        self._m.name = name
        self._m.description = description
        self._m.technology = technology
        self._m.properties = properties
        self._parent: Optional[DeploymentNode] = None
        self._tags = OrderedTags(['Element', 'Infrastructure Node'], tags)
        self._m.tags = ','.join(self._tags)

        self._sources: List[DslElement] = []
        self._destinations: List[DslElement] = []
        self._relationships: Set[DslRelationship] = OrderedSet()

        stack = _current_deployment_node_stack.get()
        if stack:
            stack[-1].add_infrastructure_node(self)

        deployment_environment = _current_deployment_environment.get()
        if deployment_environment is not None:
            self._m.environment = deployment_environment.name

    @property
    def model(self) -> buildzr.models.InfrastructureNode:
        return self._m

    @property
    def tags(self) -> Set[str]:
        return self._tags

    @property
    def parent(self) -> Optional[DeploymentNode]:
        return self._parent

    @property
    def children(self) -> None:
        """
        The `InfrastructureNode` element does not have any children, and will always return
        `None`.
        """
        return None

    @property
    def sources(self) -> List[DslElement]:
        return self._sources

    @property
    def destinations(self) -> List[DslElement]:
        return self._destinations

    @property
    def relationships(self) -> Set[DslRelationship]:
        return self._relationships

children property

children: None

The InfrastructureNode element does not have any children, and will always return None.

DeploymentGroup

Source code in buildzr/dsl/dsl.py
class DeploymentGroup:

    def __init__(self, name: str) -> None:
        self._name = name

    @property
    def name(self) -> str:
        return self._name

Views

SystemLandscapeView

Bases: DslViewElement

Source code in buildzr/dsl/dsl.py
class SystemLandscapeView(DslViewElement):

    from buildzr.dsl.expression import Expression, WorkspaceExpression, ElementExpression, RelationshipExpression

    @property
    def model(self) -> buildzr.models.SystemLandscapeView:
        return self._m

    def __init__(
        self,
        key: str,
        description: str,
        auto_layout: _AutoLayout='tb',
        title: Optional[str]=None,
        include_elements: List[Union[DslElement, Callable[[WorkspaceExpression, ElementExpression], bool]]]=[],
        exclude_elements: List[Union[DslElement, Callable[[WorkspaceExpression, ElementExpression], bool]]]=[],
        include_relationships: List[Union[DslElement, Callable[[WorkspaceExpression, RelationshipExpression], bool]]]=[],
        exclude_relationships: List[Union[DslElement, Callable[[WorkspaceExpression, RelationshipExpression], bool]]]=[],
        properties: Optional[Dict[str, str]]=None,
    ) -> None:
        self._m = buildzr.models.SystemLandscapeView()

        self._m.key = key
        self._m.description = description

        self._m.automaticLayout = _auto_layout_to_model(auto_layout)
        self._m.title = title
        self._m.properties = properties

        self._include_elements = include_elements
        self._exclude_elements = exclude_elements
        self._include_relationships = include_relationships
        self._exclude_relationships = exclude_relationships

        workspace = _current_workspace.get()
        if workspace is not None:
            workspace.apply_view(self)

    def _on_added(self, workspace: Workspace) -> None:

        from buildzr.dsl.expression import Expression, WorkspaceExpression, ElementExpression, RelationshipExpression
        from buildzr.models import ElementView, RelationshipView

        expression = Expression(
            include_elements=self._include_elements,
            exclude_elements=self._exclude_elements,
            include_relationships=self._include_relationships,
            exclude_relationships=self._exclude_relationships,
        )

        include_view_elements_filter: List[Union[DslElement, Callable[[WorkspaceExpression, ElementExpression], bool]]] = [
            lambda w, e: e.type == Person,
            lambda w, e: e.type == SoftwareSystem,
            lambda w, e: e.type == Element,
        ]

        exclude_view_elements_filter: List[Union[DslElement, Callable[[WorkspaceExpression, ElementExpression], bool]]] = [
            lambda w, e: e.type == Container,
            lambda w, e: e.type == Component,
        ]

        include_view_relationships_filter: List[Union[DslElement, Callable[[WorkspaceExpression, RelationshipExpression], bool]]] = [
            lambda w, r: r.source.type == Person,
            lambda w, r: r.source.type == SoftwareSystem,
            lambda w, r: r.source.type == Element,
            lambda w, r: r.destination.type == Person,
            lambda w, r: r.destination.type == SoftwareSystem,
            lambda w, r: r.destination.type == Element,
        ]

        expression = Expression(
            include_elements=self._include_elements + include_view_elements_filter,
            exclude_elements=self._exclude_elements + exclude_view_elements_filter,
            include_relationships=self._include_relationships + include_view_relationships_filter,
            exclude_relationships=self._exclude_relationships,
        )

        element_ids = map(
            lambda x: str(x.model.id),
            expression.elements(workspace)
        )

        relationship_ids = map(
            lambda x: str(x.model.id),
            expression.relationships(workspace)
        )

        self._m.elements = []
        for element_id in element_ids:
            # Add x, y coordinates (required by Structurizr for rendering)
            self._m.elements.append(ElementView(id=element_id, x=0, y=0))

        self._m.relationships = []
        for relationship_id in relationship_ids:
            self._m.relationships.append(RelationshipView(id=relationship_id))

SystemContextView

Bases: DslViewElement

If no filter is applied, this view includes all elements that have a direct relationship with the selected SoftwareSystem.

Source code in buildzr/dsl/dsl.py
class SystemContextView(DslViewElement):

    """
    If no filter is applied, this view includes all elements that have a direct
    relationship with the selected `SoftwareSystem`.
    """

    from buildzr.dsl.expression import Expression, WorkspaceExpression, ElementExpression, RelationshipExpression

    @property
    def model(self) -> buildzr.models.SystemContextView:
        return self._m

    def __init__(
        self,
        software_system_selector: Union[SoftwareSystem, Callable[[WorkspaceExpression], SoftwareSystem]],
        key: str,
        description: str,
        auto_layout: _AutoLayout='tb',
        title: Optional[str]=None,
        include_elements: List[Union[DslElement, Callable[[WorkspaceExpression, ElementExpression], bool]]]=[],
        exclude_elements: List[Union[DslElement, Callable[[WorkspaceExpression, ElementExpression], bool]]]=[],
        include_relationships: List[Union[DslElement, Callable[[WorkspaceExpression, RelationshipExpression], bool]]]=[],
        exclude_relationships: List[Union[DslElement, Callable[[WorkspaceExpression, RelationshipExpression], bool]]]=[],
        properties: Optional[Dict[str, str]]=None,
    ) -> None:
        self._m = buildzr.models.SystemContextView()

        self._m.key = key
        self._m.description = description

        self._m.automaticLayout = _auto_layout_to_model(auto_layout)
        # Add applied field to automaticLayout (required by Structurizr)
        if self._m.automaticLayout:
            self._m.automaticLayout.applied = False

        self._m.title = title
        self._m.properties = properties
        # Add enterprise boundary visibility and order (required by Structurizr)
        self._m.enterpriseBoundaryVisible = True
        self._m.order = 1

        self._selector = software_system_selector
        self._include_elements = include_elements
        self._exclude_elements = exclude_elements
        self._include_relationships = include_relationships
        self._exclude_relationships = exclude_relationships

        workspace = _current_workspace.get()
        if workspace is not None:
            workspace.apply_view(self)

    def _on_added(self, workspace: Workspace) -> None:

        from buildzr.dsl.expression import Expression, WorkspaceExpression, ElementExpression, RelationshipExpression
        from buildzr.models import ElementView, RelationshipView

        if isinstance(self._selector, SoftwareSystem):
            software_system = self._selector
        else:
            software_system = self._selector(WorkspaceExpression(workspace))
        self._m.softwareSystemId = software_system.model.id
        view_elements_filter: List[Union[DslElement, Callable[[WorkspaceExpression, ElementExpression], bool]]] = [
            lambda w, e: e == software_system,
            lambda w, e: software_system.model.id in e.sources.ids,
            lambda w, e: software_system.model.id in e.destinations.ids,
        ]

        view_relationships_filter: List[Union[DslElement, Callable[[WorkspaceExpression, RelationshipExpression], bool]]] = [
            lambda w, r: software_system == r.source,
            lambda w, r: software_system == r.destination,
        ]

        expression = Expression(
            include_elements=self._include_elements + view_elements_filter,
            exclude_elements=self._exclude_elements,
            include_relationships=self._include_relationships + view_relationships_filter,
            exclude_relationships=self._exclude_relationships,
        )

        element_ids = map(
            lambda x: str(x.model.id),
            expression.elements(workspace)
        )

        relationship_ids = map(
            lambda x: str(x.model.id),
            expression.relationships(workspace)
        )

        self._m.elements = []
        for element_id in element_ids:
            # Add x, y coordinates (required by Structurizr for rendering)
            self._m.elements.append(ElementView(id=element_id, x=0, y=0))

        self._m.relationships = []
        for relationship_id in relationship_ids:
            self._m.relationships.append(RelationshipView(id=relationship_id))

ContainerView

Bases: DslViewElement

Source code in buildzr/dsl/dsl.py
class ContainerView(DslViewElement):

    from buildzr.dsl.expression import Expression, WorkspaceExpression, ElementExpression, RelationshipExpression

    @property
    def model(self) -> buildzr.models.ContainerView:
        return self._m

    def __init__(
        self,
        software_system_selector: Union[SoftwareSystem, Callable[[WorkspaceExpression], SoftwareSystem]],
        key: str,
        description: str,
        auto_layout: _AutoLayout='tb',
        title: Optional[str]=None,
        include_elements: List[Union[DslElement, Callable[[WorkspaceExpression, ElementExpression], bool]]]=[],
        exclude_elements: List[Union[DslElement, Callable[[WorkspaceExpression, ElementExpression], bool]]]=[],
        include_relationships: List[Union[DslElement, Callable[[WorkspaceExpression, RelationshipExpression], bool]]]=[],
        exclude_relationships: List[Union[DslElement, Callable[[WorkspaceExpression, RelationshipExpression], bool]]]=[],
        properties: Optional[Dict[str, str]]=None,
    ) -> None:
        self._m = buildzr.models.ContainerView()

        self._m.key = key
        self._m.description = description

        self._m.automaticLayout = _auto_layout_to_model(auto_layout)
        self._m.title = title
        self._m.properties = properties

        self._selector = software_system_selector
        self._include_elements = include_elements
        self._exclude_elements = exclude_elements
        self._include_relationships = include_relationships
        self._exclude_relationships = exclude_relationships

        workspace = _current_workspace.get()
        if workspace is not None:
            workspace.apply_view(self)

    def _on_added(self, workspace: Workspace) -> None:

        from buildzr.dsl.expression import Expression, WorkspaceExpression, ElementExpression, RelationshipExpression
        from buildzr.models import ElementView, RelationshipView

        if isinstance(self._selector, SoftwareSystem):
            software_system = self._selector
        else:
            software_system = self._selector(WorkspaceExpression(workspace))
        self._m.softwareSystemId = software_system.model.id

        container_ids = { container.model.id for container in software_system.children}

        view_elements_filter: List[Union[DslElement, Callable[[WorkspaceExpression, ElementExpression], bool]]] = [
            lambda w, e: e.parent == software_system,
            lambda w, e: any(container_ids.intersection({ id for id in e.sources.ids })),
            lambda w, e: any(container_ids.intersection({ id for id in e.destinations.ids })),
        ]

        view_relationships_filter: List[Union[DslElement, Callable[[WorkspaceExpression, RelationshipExpression], bool]]] = [
            lambda w, r: software_system == r.source.parent,
            lambda w, r: software_system == r.destination.parent,
        ]

        expression = Expression(
            include_elements=self._include_elements + view_elements_filter,
            exclude_elements=self._exclude_elements,
            include_relationships=self._include_relationships + view_relationships_filter,
            exclude_relationships=self._exclude_relationships,
        )

        element_ids = map(
            lambda x: str(x.model.id),
            expression.elements(workspace)
        )

        relationship_ids = map(
            lambda x: str(x.model.id),
            expression.relationships(workspace)
        )

        self._m.elements = []
        for element_id in element_ids:
            # Add x, y coordinates (required by Structurizr for rendering)
            self._m.elements.append(ElementView(id=element_id, x=0, y=0))

        self._m.relationships = []
        for relationship_id in relationship_ids:
            self._m.relationships.append(RelationshipView(id=relationship_id))

ComponentView

Bases: DslViewElement

Source code in buildzr/dsl/dsl.py
class ComponentView(DslViewElement):

    from buildzr.dsl.expression import Expression, WorkspaceExpression, ElementExpression, RelationshipExpression

    @property
    def model(self) -> buildzr.models.ComponentView:
        return self._m

    def __init__(
        self,
        container_selector: Union[Container, Callable[[WorkspaceExpression], Container]],
        key: str,
        description: str,
        auto_layout: _AutoLayout='tb',
        title: Optional[str]=None,
        include_elements: List[Union[DslElement, Callable[[WorkspaceExpression, ElementExpression], bool]]]=[],
        exclude_elements: List[Union[DslElement, Callable[[WorkspaceExpression, ElementExpression], bool]]]=[],
        include_relationships: List[Union[DslElement, Callable[[WorkspaceExpression, RelationshipExpression], bool]]]=[],
        exclude_relationships: List[Union[DslElement, Callable[[WorkspaceExpression, RelationshipExpression], bool]]]=[],
        properties: Optional[Dict[str, str]]=None,
    ) -> None:
        self._m = buildzr.models.ComponentView()

        self._m.key = key
        self._m.description = description

        self._m.automaticLayout = _auto_layout_to_model(auto_layout)
        self._m.title = title
        self._m.properties = properties

        self._selector = container_selector
        self._include_elements = include_elements
        self._exclude_elements = exclude_elements
        self._include_relationships = include_relationships
        self._exclude_relationships = exclude_relationships

        workspace = _current_workspace.get()
        if workspace is not None:
            workspace.apply_view(self)

    def _on_added(self, workspace: Workspace) -> None:

        from buildzr.dsl.expression import Expression, WorkspaceExpression, ElementExpression, RelationshipExpression
        from buildzr.models import ElementView, RelationshipView

        container: Container
        if isinstance(self._selector, Container):
            container = self._selector
        else:
            container = self._selector(WorkspaceExpression(workspace))
        self._m.containerId = container.model.id

        component_ids = { component.model.id for component in container.children or [] }

        view_elements_filter: List[Union[DslElement, Callable[[WorkspaceExpression, ElementExpression], bool]]] = [
            lambda w, e: e.parent == container,
            lambda w, e: any(component_ids.intersection({ id for id in e.sources.ids })),
            lambda w, e: any(component_ids.intersection({ id for id in e.destinations.ids })),
        ]

        view_relationships_filter: List[Union[DslElement, Callable[[WorkspaceExpression, RelationshipExpression], bool]]] = [
            lambda w, r: container == r.source.parent,
            lambda w, r: container == r.destination.parent,
        ]

        expression = Expression(
            include_elements=self._include_elements + view_elements_filter,
            exclude_elements=self._exclude_elements,
            include_relationships=self._include_relationships + view_relationships_filter,
            exclude_relationships=self._exclude_relationships,
        )

        element_ids = map(
            lambda x: str(x.model.id),
            expression.elements(workspace)
        )

        relationship_ids = map(
            lambda x: str(x.model.id),
            expression.relationships(workspace)
        )

        self._m.elements = []
        for element_id in element_ids:
            # Add x, y coordinates (required by Structurizr for rendering)
            self._m.elements.append(ElementView(id=element_id, x=0, y=0))

        self._m.relationships = []
        for relationship_id in relationship_ids:
            self._m.relationships.append(RelationshipView(id=relationship_id))

DeploymentView

Bases: DslViewElement

Source code in buildzr/dsl/dsl.py
class DeploymentView(DslViewElement):

    from buildzr.dsl.expression import Expression, WorkspaceExpression, ElementExpression, RelationshipExpression

    @property
    def model(self) -> buildzr.models.DeploymentView:
        return self._m

    def __init__(
        self,
        environment: DeploymentEnvironment,
        key: str,
        software_system_selector: Optional[Union[SoftwareSystem, Callable[[WorkspaceExpression], SoftwareSystem]]]=None,
        description: str="",
        auto_layout: _AutoLayout='tb',
        title: Optional[str]=None,
        include_elements: List[Union[DslElement, Callable[[WorkspaceExpression, ElementExpression], bool]]]=[],
        exclude_elements: List[Union[DslElement, Callable[[WorkspaceExpression, ElementExpression], bool]]]=[],
        include_relationships: List[Union[DslElement, Callable[[WorkspaceExpression, RelationshipExpression], bool]]]=[],
        exclude_relationships: List[Union[DslElement, Callable[[WorkspaceExpression, RelationshipExpression], bool]]]=[],
        properties: Optional[Dict[str, str]]=None,
    ) -> None:
        self._m = buildzr.models.DeploymentView()

        self._selector = software_system_selector
        self._environment = environment

        self._m.key = key
        self._m.description = description
        self._m.environment = environment.name

        self._m.automaticLayout = _auto_layout_to_model(auto_layout)
        self._m.title = title
        self._m.properties = properties

        self._include_elements = include_elements
        self._exclude_elements = exclude_elements
        self._include_relationships = include_relationships
        self._exclude_relationships = exclude_relationships

        workspace = _current_workspace.get()
        if workspace is not None:
            workspace.apply_view(self)

    def _on_added(self, workspace: Workspace) -> None:

        from buildzr.dsl.expression import Expression, WorkspaceExpression, ElementExpression, RelationshipExpression
        from buildzr.dsl.explorer import Explorer
        from buildzr.models import ElementView, RelationshipView

        software_system: Optional[SoftwareSystem] = None
        if self._selector is not None:
            if isinstance(self._selector, SoftwareSystem):
                software_system = self._selector
                self._m.softwareSystemId = software_system.model.id
            else:
                software_system = self._selector(WorkspaceExpression(workspace))
                self._m.softwareSystemId = software_system.model.id

        view_elements_filter: List[Union[DslElement, Callable[[WorkspaceExpression, ElementExpression], bool]]] = []
        view_elements_filter_excludes: List[Union[DslElement, Callable[[WorkspaceExpression, ElementExpression], bool]]] = []
        view_relationships_filter_env: List[Union[DslElement, Callable[[WorkspaceExpression, RelationshipExpression], bool]]] = []
        view_relationships_filter_implied_instance_relationships: List[Union[DslElement, Callable[[WorkspaceExpression, RelationshipExpression], bool]]] = []

        def is_software_system_contains_container(
            software_system_id: str,
            container_id: str,
        ) -> bool:
            for software_system in workspace.model.model.softwareSystems:
                if software_system.id == software_system_id:
                    for container in software_system.containers:
                        if container.id == container_id:
                            return True
            return False

        def recursive_includes(
            deployment_node_ancestor_ids: List[str],
            deployment_node: buildzr.models.DeploymentNode,
            upstream_software_system_ids: Set[str],
            environment: str,
            include_ids: Set[str],
            selected_software_system: Optional[buildzr.models.SoftwareSystem] = None,
        ) -> None:

            """
            Recursively includes the relevant deployment nodes, software system
            instances, container instances, and infrastructure nodes based on
            the provided environment and DeploymentView parameters.

            @param deployment_node_ancestor_ids: List of ancestor deployment
            node IDs. Useful for tracing back the upstream deployment nodes that
            should be included in the view. For example, we may have deployment nodes
            `a` -> `b` -> `c`, and we want to include all of them if `c` is included,
            even if `b` has no software system instances, container instances,
            or infrastructure nodes.

            @param upstream_software_system_ids: Set of software system IDs that
            whose instance exists in the upstream deployment nodes.
            """

            instance_ids: Set[str] = set()
            for child in deployment_node.children:
                if child.environment == environment:
                    recursive_includes(
                        deployment_node_ancestor_ids + [deployment_node.id],
                        child,
                        upstream_software_system_ids.union({
                            software_system_instance.softwareSystemId
                            for software_system_instance in deployment_node.softwareSystemInstances
                        }),
                        environment,
                        include_ids,
                        selected_software_system
                    )

            if selected_software_system is None:
                software_instance_ids = {
                    instance.id for instance in deployment_node.softwareSystemInstances
                    if instance.environment == environment
                }

                sibling_software_system_ids = {
                    instance.softwareSystemId for instance in deployment_node.softwareSystemInstances
                    if instance.environment == environment
                }

                container_instance_ids = {
                    instance.id for instance in deployment_node.containerInstances
                    if instance.environment == environment and \
                       not any({
                            is_software_system_contains_container(
                                software_system_id,
                                instance.containerId
                            ) for software_system_id in upstream_software_system_ids.union(sibling_software_system_ids)
                       })
                }

                instance_ids.update(software_instance_ids)
                instance_ids.update(container_instance_ids)

            else:
                container_instance_ids = {
                    instance.id for instance in deployment_node.containerInstances
                    if instance.environment == environment and \
                        is_software_system_contains_container(
                            selected_software_system.id,
                            instance.containerId
                        )
                }

                instance_ids.update(container_instance_ids)

            software_instance_relation_ids: Set[str] = set()
            for software_system_instance in deployment_node.softwareSystemInstances:
                if software_system_instance.relationships and software_system_instance.environment == environment:
                    for relationship in software_system_instance.relationships:
                        software_instance_relation_ids.add(relationship.id)

            container_instance_relation_ids: Set[str] = set()
            if selected_software_system is not None:
                # Note: These relations are created in the `__exit__` of each
                # `DeploymentEnvironment` -- the relationships are being implied
                # from the respective `SoftwareSystem`s and `Container`s.
                for container_instance in deployment_node.containerInstances:
                    if container_instance.relationships and container_instance.environment == environment:
                        for relationship in container_instance.relationships:
                            container_instance_relation_ids.add(relationship.id)

            infrastructure_node_relation_ids: Set[str] = set()
            for infrastructure_node in deployment_node.infrastructureNodes:
                if infrastructure_node.relationships and infrastructure_node.environment == environment:
                    for relationship in infrastructure_node.relationships:
                        infrastructure_node_relation_ids.add(relationship.id)

            infrastructure_node_ids = {
                infrastructure_node.id for infrastructure_node in deployment_node.infrastructureNodes
                if infrastructure_node.environment == environment
            }

            instance_ids.update(software_instance_relation_ids)
            instance_ids.update(container_instance_relation_ids)
            instance_ids.update(infrastructure_node_relation_ids)
            instance_ids.update(infrastructure_node_ids)

            # Only include this deployment node
            # if there's anything to include at all.
            if len(instance_ids) > 0:
                for deployment_node_ancestor_id in deployment_node_ancestor_ids:
                    include_ids.add(deployment_node_ancestor_id)
                include_ids.add(deployment_node.id)
                include_ids.update(instance_ids)

        include_ids: Set[str] = set()
        upstream_software_system_ids: Set[str] = set()

        for root_deployment_node in workspace.model.model.deploymentNodes:
            if root_deployment_node.environment == self._environment.name:
                recursive_includes(
                    [],
                    root_deployment_node,
                    upstream_software_system_ids,
                    self._environment.name,
                    include_ids,
                    software_system.model if software_system else None
                )

        view_elements_filter = [
            lambda w, e: (
                e.id in include_ids
            ),
        ]

        view_relationships_filter_env = [
            lambda w, r: r.source.environment == self._environment.name,
            lambda w, r: r.destination.environment == self._environment.name,
        ]

        view_relationships_filter_implied_instance_relationships = [
            lambda w, r: r.id in include_ids,
        ]

        expression = Expression(
            include_elements=self._include_elements + view_elements_filter,
            exclude_elements=self._exclude_elements,
            include_relationships=self._include_relationships +\
                view_relationships_filter_env +\
                view_relationships_filter_implied_instance_relationships,
            exclude_relationships=self._exclude_relationships,
        )

        element_ids = [str(element.model.id) for element in expression.elements(workspace)]
        relationship_ids = [str(relationship.model.id) for relationship in expression.relationships(workspace)]

        self._m.elements = []
        for element_id in element_ids:
            # Add x, y coordinates (required by Structurizr for rendering)
            self._m.elements.append(ElementView(id=element_id, x=0, y=0))

        self._m.relationships = []
        for relationship_id in relationship_ids:
            self._m.relationships.append(RelationshipView(id=relationship_id))

DynamicView

Bases: DslViewElement

Source code in buildzr/dsl/dsl.py
class DynamicView(DslViewElement):

    from buildzr.dsl.expression import WorkspaceExpression

    @property
    def model(self) -> buildzr.models.DynamicView:
        return self._m

    def __init__(
        self,
        key: str,
        description: str = "",
        scope: Optional[Union[SoftwareSystem, Container, Callable[[WorkspaceExpression], Union[SoftwareSystem, Container]]]] = None,
        steps: List[DslRelationship] = [],
        auto_layout: _AutoLayout = 'tb',
        title: Optional[str] = None,
        properties: Optional[Dict[str, str]] = None,
    ) -> None:

        self._m = buildzr.models.DynamicView()
        self._m.key = key
        self._m.description = description
        self._m.automaticLayout = _auto_layout_to_model(auto_layout)
        self._m.title = title
        self._m.properties = properties

        self._scope = scope
        self._relationships = steps

        workspace = _current_workspace.get()
        if workspace is not None:
            workspace.apply_view(self)

    def _find_original_relationship(
        self,
        workspace: Workspace,
        source: DslElement,
        destination: DslElement,
        exclude_id: str,
        technology: Optional[str] = None,
        allowed_ids: Optional[Set[str]] = None,
        description: Optional[str] = None,
    ) -> Optional[DslRelationship]:
        """Find an existing relationship between source and destination, excluding a specific ID.

        A technology acts as a selector: when one is given, only relationships
        with that exact technology match. Without a technology (None or ""),
        relationships of any technology match, and the ones whose description
        equals `description` are preferred, as in Structurizr. If the matches
        then have more than one technology, the step is ambiguous and a
        ValueError names the candidates.

        When several relationships match, the one with the lowest numeric ID is
        returned so the result does not depend on set iteration order.

        Args:
            workspace: The workspace containing relationships.
            source: The source element of the relationship.
            destination: The destination element of the relationship.
            exclude_id: Relationship ID to exclude from matching.
            technology: If specified, only match relationships with this exact technology.
                       This follows Structurizr behavior where technology acts as a selector.
            allowed_ids: If specified, only match relationships with one of these IDs.
                         Used to skip inline relationships created while evaluating
                         the steps of a DynamicView.
            description: The description of the step. Without a technology,
                         relationships with this exact description are preferred.

        Raises:
            ValueError: If no technology is given and the matching relationships
                have different technologies.
        """
        from buildzr.dsl.explorer import Explorer

        explorer = Explorer(workspace)
        candidates = [
            rel for rel in explorer.walk_relationships()
            if rel.source.model.id == source.model.id
            and rel.destination.model.id == destination.model.id
            and rel.model.id != exclude_id
            and (allowed_ids is None or rel.model.id in allowed_ids)
            # If technology is specified, it must match exactly
            and (not technology or rel.model.technology == technology)
        ]
        if not candidates:
            return None

        if not technology:
            same_description = [
                rel for rel in candidates
                if (rel.model.description or "") == (description or "")
            ]
            if same_description:
                candidates = same_description
            technologies = {rel.model.technology or "" for rel in candidates}
            if len(technologies) > 1:
                source_name = getattr(source.model, 'name', str(source.model.id))
                dest_name = getattr(destination.model, 'name', str(destination.model.id))
                listed = ", ".join(
                    f"'{rel.model.description or ''}' ({rel.model.technology or 'no technology'})"
                    for rel in sorted(candidates, key=lambda r: int(r.model.id))
                )
                raise ValueError(
                    f"The DynamicView step from '{source_name}' to '{dest_name}' has no "
                    f"technology and matches {len(candidates)} relationships with different "
                    f"technologies: {listed}. Add the technology of the relationship you "
                    f"mean to the step, for example: "
                    f"source >> (\"{description or ''}\", \"<technology>\") >> destination."
                )

        return min(candidates, key=lambda r: int(r.model.id))

    def _remove_relationship_from_model(
        self,
        source: DslElement,
        rel_id: str,
    ) -> None:
        """Remove a relationship from both the source element's model and DSL relationships."""
        # Remove from model relationships
        if hasattr(source.model, 'relationships') and source.model.relationships:
            source.model.relationships = [
                r for r in source.model.relationships if r.id != rel_id
            ]
        # Also remove from DSL element's relationships set (used by Explorer)
        to_remove: Optional[DslRelationship] = None
        for r in source.relationships:
            if r.model.id == rel_id:
                to_remove = r
                break
        if to_remove is not None:
            source.relationships.discard(to_remove)

    def _on_added(self, workspace: Workspace) -> None:
        from buildzr.dsl.expression import WorkspaceExpression
        from buildzr.models import ElementView, RelationshipView
        from buildzr.dsl.explorer import Explorer

        # Resolve scope selector and set elementId
        if self._scope is not None:
            if isinstance(self._scope, (SoftwareSystem, Container)):
                self._m.elementId = self._scope.model.id
            elif callable(self._scope):
                resolved = self._scope(WorkspaceExpression(workspace))
                self._m.elementId = resolved.model.id

        # Collect relationship IDs passed to this DynamicView
        dv_rel_ids = {rel.model.id for rel in self._relationships}

        # Collect all relationship IDs in the workspace
        all_rel_ids = {rel.model.id for rel in Explorer(workspace).walk_relationships()}

        # Determine which relationships are "pre-existing" (created before this DynamicView)
        # vs "inline" (created during DynamicView argument evaluation)
        other_rel_ids = all_rel_ids - dv_rel_ids

        if not other_rel_ids:
            # All relationships in workspace are the ones passed to DynamicView.
            # This means they were ALL passed by reference (valid).
            pre_existing_rel_ids = dv_rel_ids.copy()
        else:
            # Some relationships exist outside of DynamicView. These are truly pre-existing.
            # For relationships passed to DynamicView, check if they existed before
            # by comparing IDs. Relationships with lower IDs were created earlier.
            max_other_id = max(int(rid) for rid in other_rel_ids)
            pre_existing_rel_ids = other_rel_ids.copy()
            for rel in self._relationships:
                if int(rel.model.id) <= max_other_id:
                    # This relationship was created before or around the same time
                    # as other relationships, so it's pre-existing (passed by reference)
                    pre_existing_rel_ids.add(rel.model.id)

        # Process relationships and collect elements
        element_ids: Set[str] = set()
        self._m.relationships = []

        for idx, rel in enumerate(self._relationships, start=1):
            source = rel.source
            destination = rel.destination
            rel_description: Optional[str] = None
            rel_id = rel.model.id
            rel_technology = rel.model.technology  # Technology from inline relationship (if any)

            # Check if there's an original relationship (created before this one)
            # If the inline relationship specifies a technology, use it as a selector
            # (following Structurizr behavior where technology acts as a selector, not an override)
            # The original must have been created before this step's relationship,
            # so a later step's inline duplicate is never picked.
            earlier_rel_ids = {
                rid for rid in pre_existing_rel_ids if int(rid) < int(rel.model.id)
            }
            original_rel = self._find_original_relationship(
                workspace, source, destination, rel.model.id,
                technology=rel_technology, allowed_ids=earlier_rel_ids,
                description=rel.model.description,
            )

            if original_rel is not None:
                # This is a view-specific relationship - use original's ID
                # and remove the duplicate from the model
                rel_description = rel.model.description
                self._remove_relationship_from_model(source, rel.model.id)
                rel_id = original_rel.model.id
            elif rel.model.id not in pre_existing_rel_ids:
                # This relationship was created during DynamicView argument evaluation
                # (inline syntax) and there's no pre-existing relationship between
                # source and destination (with matching technology if specified). This is invalid.
                source_name = getattr(source.model, 'name', str(source.model.id))
                dest_name = getattr(destination.model, 'name', str(destination.model.id))
                if rel_technology:
                    raise ValueError(
                        f"No existing relationship found between '{source_name}' and "
                        f"'{dest_name}' with technology '{rel_technology}'. "
                        f"When technology is specified in a DynamicView relationship, it acts as a "
                        f"selector to match a model relationship with that exact technology."
                    )
                raise ValueError(
                    f"No existing relationship found between '{source_name}' and "
                    f"'{dest_name}'. DynamicView relationships must reference "
                    f"pre-existing relationships in the model. Either define the relationship "
                    f"before creating the DynamicView, or pass the relationship variable directly."
                )
            # else: This is the original relationship passed by reference, use it directly

            # Collect element IDs
            element_ids.add(str(source.model.id))
            element_ids.add(str(destination.model.id))

            # Add relationship view with order and optional description override
            self._m.relationships.append(
                RelationshipView(
                    id=rel_id,
                    order=str(idx),
                    description=rel_description,
                )
            )

        # Populate elements
        self._m.elements = [ElementView(id=eid) for eid in element_ids]

CustomView

Bases: DslViewElement

A custom view for displaying custom elements (Element type).

CustomView is specifically designed for custom elements that sit outside the C4 model. By default, it only includes Element types and relationships between them. This matches the Structurizr DSL behavior where custom views are intended for custom elements.

Note: Structurizr CLI/Lite only supports CustomElement types in CustomView. Including other element types (Person, SoftwareSystem, etc.) will cause export errors.

Source code in buildzr/dsl/dsl.py
class CustomView(DslViewElement):
    """
    A custom view for displaying custom elements (Element type).

    CustomView is specifically designed for custom elements that sit outside
    the C4 model. By default, it only includes Element types and relationships
    between them. This matches the Structurizr DSL behavior where custom views
    are intended for custom elements.

    Note: Structurizr CLI/Lite only supports CustomElement types in CustomView.
    Including other element types (Person, SoftwareSystem, etc.) will cause
    export errors.
    """

    from buildzr.dsl.expression import Expression, WorkspaceExpression, ElementExpression, RelationshipExpression

    @property
    def model(self) -> buildzr.models.CustomView:
        return self._m

    def __init__(
        self,
        key: str,
        description: str = "",
        auto_layout: _AutoLayout = 'tb',
        title: Optional[str] = None,
        include_elements: List[Union[DslElement, Callable[[WorkspaceExpression, ElementExpression], bool]]] = [],
        exclude_elements: List[Union[DslElement, Callable[[WorkspaceExpression, ElementExpression], bool]]] = [],
        include_relationships: List[Union[DslElement, Callable[[WorkspaceExpression, RelationshipExpression], bool]]] = [],
        exclude_relationships: List[Union[DslElement, Callable[[WorkspaceExpression, RelationshipExpression], bool]]] = [],
        properties: Optional[Dict[str, str]] = None,
    ) -> None:
        self._m = buildzr.models.CustomView()

        self._m.key = key
        self._m.description = description

        self._m.automaticLayout = _auto_layout_to_model(auto_layout)
        self._m.title = title
        self._m.properties = properties

        # Validate that include_elements only contains Element types
        # CustomView can ONLY display custom elements (Element type)
        for elem in include_elements:
            if isinstance(elem, DslElement) and not isinstance(elem, Element):
                raise ValueError(
                    f"CustomView can only include Element types, not {type(elem).__name__}. "
                    f"Use SystemLandscapeView or other views for C4 elements."
                )

        self._include_elements = include_elements
        self._exclude_elements = exclude_elements
        self._include_relationships = include_relationships
        self._exclude_relationships = exclude_relationships

        workspace = _current_workspace.get()
        if workspace is not None:
            workspace.apply_view(self)

    def _on_added(self, workspace: Workspace) -> None:

        from buildzr.dsl.expression import Expression, WorkspaceExpression, ElementExpression, RelationshipExpression
        from buildzr.models import ElementView, RelationshipView

        # CustomView only includes Element (custom element) types by default
        # This matches Structurizr CLI/DSL behavior
        include_view_elements_filter: List[Union[DslElement, Callable[[WorkspaceExpression, ElementExpression], bool]]] = [
            lambda w, e: e.type == Element,
        ]

        # Include relationships where both source and destination are Element types
        include_view_relationships_filter: List[Union[DslElement, Callable[[WorkspaceExpression, RelationshipExpression], bool]]] = [
            lambda w, r: r.source.type == Element and r.destination.type == Element,
        ]

        expression = Expression(
            include_elements=self._include_elements + include_view_elements_filter,
            exclude_elements=self._exclude_elements,
            include_relationships=self._include_relationships + include_view_relationships_filter,
            exclude_relationships=self._exclude_relationships,
        )

        element_ids = map(
            lambda x: str(x.model.id),
            expression.elements(workspace)
        )

        relationship_ids = map(
            lambda x: str(x.model.id),
            expression.relationships(workspace)
        )

        self._m.elements = []
        for element_id in element_ids:
            # Add x, y coordinates (required by Structurizr for rendering)
            self._m.elements.append(ElementView(id=element_id, x=0, y=0))

        self._m.relationships = []
        for relationship_id in relationship_ids:
            self._m.relationships.append(RelationshipView(id=relationship_id))

Styling

StyleElements

Source code in buildzr/dsl/dsl.py
class StyleElements:

    from buildzr.dsl.expression import WorkspaceExpression, ElementExpression

    Shapes = Union[
        Literal['Box'],
        Literal['RoundedBox'],
        Literal['Circle'],
        Literal['Ellipse'],
        Literal['Hexagon'],
        Literal['Cylinder'],
        Literal['Pipe'],
        Literal['Person'],
        Literal['Robot'],
        Literal['Folder'],
        Literal['WebBrowser'],
        Literal['MobileDevicePortrait'],
        Literal['MobileDeviceLandscape'],
        Literal['Component'],
    ]

    @property
    def model(self) -> List[buildzr.models.ElementStyle]:
        return self._m

    @property
    def parent(self) -> Optional[Workspace]:
        return self._parent

    # TODO: Validate arguments with pydantic.
    def __init__(
            self,
            on: List[Union[
                DslElement,
                Group,
                Callable[[WorkspaceExpression, ElementExpression], bool],
                Type[Union['Person', 'SoftwareSystem', 'Container', 'Component']],
                str
            ]],
            tag: Optional[str]=None,
            shape: Optional[Shapes]=None,
            icon: Optional[str]=None,
            width: Optional[int]=None,
            height: Optional[int]=None,
            background: Optional[Union['str', Tuple[int, int, int], Color]]=None,
            color: Optional[Union['str', Tuple[int, int, int], Color]]=None,
            stroke: Optional[Union[str, Tuple[int, int, int], Color]]=None,
            stroke_width: Optional[int]=None,
            font_size: Optional[int]=None,
            border: Optional[Literal['solid', 'dashed', 'dotted']]=None,
            opacity: Optional[int]=None,
            metadata: Optional[bool]=None,
            description: Optional[bool]=None,
    ) -> None:

        # How the tag is populated depends on each element type in the
        # `elemenets`.
        # - If the element is a `DslElement`, then we create a unique tag
        #   specifically to help the stylizer identify that specific element.
        #   For example, if the element has an id `3`, then we should create a
        #   tag, say, `style-element-3`.
        # - If the element is a `Group`, then we simply make create the tag
        #   based on the group name and its nested path. For example,
        #   `Group:Company 1/Department 1`.
        # - If the element is a `Callable[[Workspace, Element], bool]`, we just
        #   run the function to filter out all the elements that matches the
        #   description, and create a unique tag for all of the filtered
        #   elements.
        # - If the element is a `Type[Union['Person', 'SoftwareSystem', 'Container', 'Component']]`,
        #   we create a tag based on the class name. This is based on the fact
        #   that the default tag for each element is the element's type.
        # - If the element is a `str`, we just use the string as the tag.
        #   This is useful for when you want to apply a style to all elements
        #   with a specific tag, just like in the original Structurizr DSL.
        #
        # Note that a new `buildzr.models.ElementStyle` is created for each
        # item, not for each of `StyleElements` instance. This makes the styling
        # makes more concise and flexible.

        from buildzr.dsl.expression import ElementExpression

        if background:
            assert Color.is_valid_color(background), "Invalid background color: {}".format(background)
        if color:
            assert Color.is_valid_color(color), "Invalid color: {}".format(color)
        if stroke:
            assert Color.is_valid_color(stroke), "Invalid stroke color: {}".format(stroke)

        self._m: List[buildzr.models.ElementStyle] = []
        self._parent: Optional[Workspace] = None

        workspace = _current_workspace.get()
        if workspace is not None:
            self._parent = workspace

        self._elements = on

        border_enum: Dict[str, buildzr.models.Border] = {
            'solid': buildzr.models.Border.Solid,
            'dashed': buildzr.models.Border.Dashed,
            'dotted': buildzr.models.Border.Dotted,
        }

        shape_enum: Dict[str, buildzr.models.Shape] = {
            'Box': buildzr.models.Shape.Box,
            'RoundedBox': buildzr.models.Shape.RoundedBox,
            'Circle': buildzr.models.Shape.Circle,
            'Ellipse': buildzr.models.Shape.Ellipse,
            'Hexagon': buildzr.models.Shape.Hexagon,
            'Cylinder': buildzr.models.Shape.Cylinder,
            'Pipe': buildzr.models.Shape.Pipe,
            'Person': buildzr.models.Shape.Person,
            'Robot': buildzr.models.Shape.Robot,
            'Folder': buildzr.models.Shape.Folder,
            'WebBrowser': buildzr.models.Shape.WebBrowser,
            'MobileDevicePortrait': buildzr.models.Shape.MobileDevicePortrait,
            'MobileDeviceLandscape': buildzr.models.Shape.MobileDeviceLandscape,
            'Component': buildzr.models.Shape.Component,
        }

        # A single unique element to be applied to all elements
        # affected by this style.
        # If a tag is provided (e.g., from a ThemeElement), use it for meaningful
        # legend display. Otherwise, generate a unique internal tag.
        element_tag = tag if tag else _style_tag('buildzr-styleelements', on, dict(
            shape=shape, icon=icon, width=width, height=height,
            background=background, color=color, stroke=stroke,
            stroke_width=stroke_width, font_size=font_size, border=border,
            opacity=opacity, metadata=metadata, description=description))

        # Track which tags we've already created styles for (to avoid duplicates)
        created_tags: set[str] = set()

        for element in self._elements:
            # Determine the tag for this element
            if isinstance(element, DslElement) and not isinstance(element.model, buildzr.models.Workspace):
                tag = element_tag
                element.add_tags(element_tag)
            elif isinstance(element, Group):
                tag = f"Group:{element.full_name()}"
            elif isinstance(element, type):
                tag = f"{element.__name__}"
            elif isinstance(element, str):
                tag = element
            elif callable(element):
                from buildzr.dsl.expression import ElementExpression, Expression
                if self._parent:
                    tag = element_tag
                    matched_elems = Expression(include_elements=[element]).elements(self._parent)
                    for e in matched_elems:
                        e.add_tags(element_tag)
                else:
                    raise ValueError("Cannot use callable to select elements to style without a Workspace.")
            else:
                continue

            # Only create one style per unique tag
            if tag in created_tags:
                continue
            created_tags.add(tag)

            element_style = buildzr.models.ElementStyle()
            element_style.tag = tag
            element_style.shape = shape_enum[shape] if shape else None
            element_style.icon = icon
            element_style.width = width
            element_style.height = height
            element_style.background = Color(background).to_hex() if background else None
            element_style.color = Color(color).to_hex() if color else None
            element_style.stroke = Color(stroke).to_hex() if stroke else None
            element_style.strokeWidth = stroke_width
            element_style.fontSize = font_size
            element_style.border = border_enum[border] if border else None
            element_style.opacity = opacity
            element_style.metadata = metadata
            element_style.description = description
            self._m.append(element_style)

        workspace = _current_workspace.get()
        if workspace is not None:
            workspace.apply_style(self)

StyleRelationships

Source code in buildzr/dsl/dsl.py
class StyleRelationships:

    from buildzr.dsl.expression import WorkspaceExpression, RelationshipExpression

    @property
    def model(self) -> List[buildzr.models.RelationshipStyle]:
        return self._m

    @property
    def parent(self) -> Optional[Workspace]:
        return self._parent

    def __init__(
        self,
        on: Optional[List[Union[
            DslRelationship,
            Group,
            Callable[[WorkspaceExpression, RelationshipExpression], bool],
            str
        ]]]=None,
        thickness: Optional[int]=None,
        color: Optional[Union[str, Tuple[int, int, int], Color]]=None,
        routing: Optional[Literal['Direct', 'Orthogonal', 'Curved']]=None,
        font_size: Optional[int]=None,
        width: Optional[int]=None,
        dashed: Optional[bool]=None,
        position: Optional[int]=None,
        opacity: Optional[int]=None,
    ) -> None:

        if color is not None:
            assert Color.is_valid_color(color), "Invalid color: {}".format(color)

        routing_enum: Dict[str, buildzr.models.Routing1] = {
            'Direct': buildzr.models.Routing1.Direct,
            'Orthogonal': buildzr.models.Routing1.Orthogonal,
            'Curved': buildzr.models.Routing1.Curved,
        }

        self._m: List[buildzr.models.RelationshipStyle] = []
        self._parent: Optional[Workspace] = None

        workspace = _current_workspace.get()
        if workspace is not None:
            self._parent = workspace

        # A single unique tag to be applied to all relationships
        # affected by this style.
        relation_tag = _style_tag('buildzr-stylerelationships', on, dict(
            thickness=thickness, color=color, routing=routing,
            font_size=font_size, width=width, dashed=dashed,
            position=position, opacity=opacity))

        if on is None:
            self._m.append(buildzr.models.RelationshipStyle(
                thickness=thickness,
                color=Color(color).to_hex() if color else None,
                routing=routing_enum[routing] if routing else None,
                fontSize=font_size,
                width=width,
                dashed=dashed,
                position=position,
                opacity=opacity,
                tag="Relationship",
            ))
        else:
            for relationship in on:

                relationship_style = buildzr.models.RelationshipStyle()
                relationship_style.thickness = thickness
                relationship_style.color = Color(color).to_hex() if color else None
                relationship_style.routing = routing_enum[routing] if routing else None
                relationship_style.fontSize = font_size
                relationship_style.width = width
                relationship_style.dashed = dashed
                relationship_style.position = position
                relationship_style.opacity = opacity

                if isinstance(relationship, DslRelationship):
                    relationship.add_tags(relation_tag)
                    relationship_style.tag = relation_tag
                elif isinstance(relationship, Group):
                    from buildzr.dsl.expression import Expression
                    if self._parent:
                        rels = Expression(include_relationships=[
                            lambda w, r: r.source.group == relationship.full_name() and \
                                         r.destination.group == relationship.full_name()
                        ]).relationships(self._parent)
                        for r in rels:
                            r.add_tags(relation_tag)
                        relationship_style.tag = relation_tag
                    else:
                        raise ValueError("Cannot use callable to select elements to style without a Workspace.")
                elif isinstance(relationship, str):
                    relationship_style.tag = relationship
                elif callable(relationship):
                    from buildzr.dsl.expression import Expression
                    if self._parent:
                        relationship_style.tag = relation_tag
                        matched_rels = Expression(include_relationships=[relationship]).relationships(self._parent)
                        for matched_rel in matched_rels:
                            matched_rel.add_tags(relation_tag)
                    else:
                        raise ValueError("Cannot use callable to select elements to style without a Workspace.")
                self._m.append(relationship_style)

        workspace = _current_workspace.get()
        if workspace is not None:
            workspace.apply_style(self)

Utilities

desc

desc(value: str, tech: Optional[str] = None) -> _RelationshipDescription[DslElement]
Source code in buildzr/dsl/relations.py
def desc(value: str, tech: Optional[str]=None) -> '_RelationshipDescription[DslElement]':
    if tech is None:
        return _RelationshipDescription(value)
    else:
        return _RelationshipDescription(value, tech)

Explorer

Source code in buildzr/dsl/explorer.py
class Explorer:

    def __init__(
        self,
        workspace_or_element: Union[
            Workspace,
            Person,
            SoftwareSystem,
            Container,
            Component,
            DeploymentNode,
            InfrastructureNode,
            SoftwareSystemInstance,
            ContainerInstance,
            Element,
        ]
    ):
        self._workspace_or_element = workspace_or_element

    def walk_elements(self) -> Generator[Union[
        Person,
        SoftwareSystem,
        Container,
        Component,
        DeploymentNode,
        InfrastructureNode,
        SoftwareSystemInstance,
        ContainerInstance,
        Element,
    ], None, None]:
        if self._workspace_or_element.children:
            for child in self._workspace_or_element.children:
                explorer = Explorer(child).walk_elements()
                yield child
                yield from explorer

    def walk_relationships(self) -> Generator[DslRelationship, None, None]:

        if self._workspace_or_element.children:

            for child in self._workspace_or_element.children:

                if child.relationships:
                    for relationship in child.relationships:
                        yield cast(_Relationship, relationship) # TODO: Temporary fix. Use a better approach - Generics?

                explorer = Explorer(child).walk_relationships()
                yield from explorer

Expression

A class used to filter the elements and the relationships in the workspace. To be used when defining views.

In the Structurizr DSL, these are called “Expressions”. See the Structurizr docs here: https://docs.structurizr.com/dsl/expressions

Source code in buildzr/dsl/expression.py
class Expression:

    """
    A class used to filter the elements and the relationships in the workspace.
    To be used when defining views.

    In the Structurizr DSL, these are called "Expressions". See the Structurizr docs here:
    https://docs.structurizr.com/dsl/expressions
    """

    def __init__(
        self,
        include_elements: Iterable[Union[DslElement, Callable[[WorkspaceExpression, ElementExpression], bool]]]=[lambda w, e: True],
        exclude_elements: Iterable[Union[DslElement, Callable[[WorkspaceExpression, ElementExpression], bool]]]=[],
        include_relationships: Iterable[Union[DslElement, Callable[[WorkspaceExpression, RelationshipExpression], bool]]]=[lambda w, e: True],
        exclude_relationships: Iterable[Union[DslElement, Callable[[WorkspaceExpression, RelationshipExpression], bool]]]=[],
    ) -> 'None':
        self._include_elements = include_elements
        self._exclude_elements = exclude_elements
        self._include_relationships = include_relationships
        self._exclude_relationships = exclude_relationships

    def elements(
        self,
        workspace: Workspace,
    ) -> List[DslElement]:

        filtered_elements: List[DslElement] = []

        workspace_elements = buildzr.dsl.Explorer(workspace).walk_elements()
        for element in workspace_elements:
            includes: List[bool] = []
            excludes: List[bool] = []
            for f in self._include_elements:
                if isinstance(f, DslElement):
                    includes.append(f == element)
                else:
                    includes.append(f(WorkspaceExpression(workspace), ElementExpression(element)))
            for f in self._exclude_elements:
                if isinstance(f, DslElement):
                    excludes.append(f == element)
                else:
                    excludes.append(f(WorkspaceExpression(workspace), ElementExpression(element)))
            if any(includes) and not any(excludes):
                filtered_elements.append(element)

        return filtered_elements

    def relationships(
        self,
        workspace: Workspace
    ) -> List[DslRelationship]:

        """
        Returns the relationships that are included as defined in
        `include_relationships` and excludes those that are defined in
        `exclude_relationships`. Any relationships that directly works on
        elements that are excluded as defined in `exclude_elements` will also be
        excluded.
        """

        filtered_relationships: List[DslRelationship] = []

        def _is_relationship_of_excluded_elements(
            workspace: WorkspaceExpression,
            relationship: RelationshipExpression,
            exclude_element_predicates: Iterable[Union[DslElement, Callable[[WorkspaceExpression, ElementExpression], bool]]],
        ) -> bool:
            for f in exclude_element_predicates:
                if isinstance(f, DslElement):
                    if f == relationship.source or f == relationship.destination:
                        return True
                else:
                    if f(workspace, relationship.source) or f(workspace, relationship.destination):
                        return True
            return False

        workspace_relationships = buildzr.dsl.Explorer(workspace).walk_relationships()

        for relationship in workspace_relationships:

            includes: List[bool] = []
            excludes: List[bool] = []

            for f in self._include_relationships:
                if isinstance(f, DslElement):
                    includes.append(f == relationship)
                else:
                    includes.append(f(WorkspaceExpression(workspace), RelationshipExpression(relationship)))

            for f in self._exclude_relationships:
                if isinstance(f, DslElement):
                    excludes.append(f == relationship)
                else:
                    excludes.append(f(WorkspaceExpression(workspace), RelationshipExpression(relationship)))

            # Also exclude relationships whose source or destination elements
            # are excluded.
            excludes.append(
                _is_relationship_of_excluded_elements(
                    WorkspaceExpression(workspace),
                    RelationshipExpression(relationship),
                    self._exclude_elements,
                )
            )

            if any(includes) and not any(excludes):
                filtered_relationships.append(relationship)

        return filtered_relationships

relationships

relationships(workspace: Workspace) -> List[DslRelationship]

Returns the relationships that are included as defined in include_relationships and excludes those that are defined in exclude_relationships. Any relationships that directly works on elements that are excluded as defined in exclude_elements will also be excluded.

Source code in buildzr/dsl/expression.py
def relationships(
    self,
    workspace: Workspace
) -> List[DslRelationship]:

    """
    Returns the relationships that are included as defined in
    `include_relationships` and excludes those that are defined in
    `exclude_relationships`. Any relationships that directly works on
    elements that are excluded as defined in `exclude_elements` will also be
    excluded.
    """

    filtered_relationships: List[DslRelationship] = []

    def _is_relationship_of_excluded_elements(
        workspace: WorkspaceExpression,
        relationship: RelationshipExpression,
        exclude_element_predicates: Iterable[Union[DslElement, Callable[[WorkspaceExpression, ElementExpression], bool]]],
    ) -> bool:
        for f in exclude_element_predicates:
            if isinstance(f, DslElement):
                if f == relationship.source or f == relationship.destination:
                    return True
            else:
                if f(workspace, relationship.source) or f(workspace, relationship.destination):
                    return True
        return False

    workspace_relationships = buildzr.dsl.Explorer(workspace).walk_relationships()

    for relationship in workspace_relationships:

        includes: List[bool] = []
        excludes: List[bool] = []

        for f in self._include_relationships:
            if isinstance(f, DslElement):
                includes.append(f == relationship)
            else:
                includes.append(f(WorkspaceExpression(workspace), RelationshipExpression(relationship)))

        for f in self._exclude_relationships:
            if isinstance(f, DslElement):
                excludes.append(f == relationship)
            else:
                excludes.append(f(WorkspaceExpression(workspace), RelationshipExpression(relationship)))

        # Also exclude relationships whose source or destination elements
        # are excluded.
        excludes.append(
            _is_relationship_of_excluded_elements(
                WorkspaceExpression(workspace),
                RelationshipExpression(relationship),
                self._exclude_elements,
            )
        )

        if any(includes) and not any(excludes):
            filtered_relationships.append(relationship)

    return filtered_relationships

Color

Source code in buildzr/dsl/color.py
class Color:
    r: int
    g: int
    b: int

    _ENGLISH_COLORS = {
        'black': '#000000',
        'white': '#ffffff',
        'red': '#ff0000',
        'green': '#00ff00',
        'blue': '#0000ff',
        'yellow': '#ffff00',
        'cyan': '#00ffff',
        'magenta': '#ff00ff',
        'gray': '#808080',
        'grey': '#808080',
        'orange': '#ffa500',
        'purple': '#800080',
        'pink': '#ffc0cb',
        'brown': '#a52a2a',
        'lime': '#00ff00',
        'navy': '#000080',
        'teal': '#008080',
        'olive': '#808000',
        'maroon': '#800000',
        'silver': '#c0c0c0',
        'gold': '#ffd700',
    }

    def __init__(
        self,
        value: Union[
            'Color',
            str,
            Tuple[int, int, int],
            Literal[
                'black', 'white', 'red', 'green', 'blue', 'yellow', 'cyan', 'magenta', 'gray', 'grey', 'orange', 'purple', 'pink', 'brown', 'lime', 'navy', 'teal', 'olive', 'maroon', 'silver', 'gold'
            ]
        ]
    ):
        if isinstance(value, Color):
            self.r, self.g, self.b = value.r, value.g, value.b
        elif isinstance(value, tuple):
            if len(value) == 3:
                self.r, self.g, self.b = value
            else:
                raise ValueError("Tuple must be (r, g, b)")
        elif isinstance(value, str):
            self.r, self.g, self.b = self._parse_color(value)
        else:
            raise TypeError("Invalid type for Color constructor")

    @classmethod
    def is_valid_color(cls, value: Union[str, Tuple[int, int, int], 'Color']) -> bool:
        try:
            if isinstance(value, tuple):
                if len(value) == 3 and all(isinstance(x, int) and 0 <= x <= 255 for x in value):
                    return True
                return False
            elif isinstance(value, str):
                v = value.strip().lower()
                if v in cls._ENGLISH_COLORS:
                    return True
                if v.startswith('#'):
                    try:
                        cls._parse_hex(v)
                        return True
                    except Exception:
                        return False
                if v.startswith('rgb'):
                    try:
                        cls._parse_rgb(v)
                        return True
                    except Exception:
                        return False
                return False
            return False
        except Exception:
            return False

    @classmethod
    def _parse_color(cls, value: str) -> Tuple[int, int, int]:
        value = value.strip().lower()
        if value in cls._ENGLISH_COLORS:
            value = cls._ENGLISH_COLORS[value]
        if value.startswith('#'):
            return cls._parse_hex(value)
        if value.startswith('rgb'):
            return cls._parse_rgb(value)
        raise ValueError(f"Unknown color format: {value}")

    @classmethod
    def _parse_hex(cls, value: str) -> Tuple[int, int, int]:
        value = value.lstrip('#')
        if len(value) == 3:
            value = ''.join([c*2 for c in value])
        if len(value) == 6:
            r, g, b = value[0:2], value[2:4], value[4:6]
            return int(r, 16), int(g, 16), int(b, 16)
        raise ValueError(f"Invalid hex color: #{value}")

    @classmethod
    def _parse_rgb(cls, value: str) -> Tuple[int, int, int]:
        # Match rgb(r, g, b)
        match = re.match(r"rgb\(([^)]+)\)", value)
        if not match:
            raise ValueError(f"Invalid rgb color: {value}")
        parts = [x.strip() for x in match.group(1).split(',')]
        if len(parts) == 3:
            r, g, b = map(int, parts)
            return r, g, b
        raise ValueError(f"Invalid rgb color: {value}")

    def to_hex(self) -> str:
        return f"#{self.r:02x}{self.g:02x}{self.b:02x}"

    def __str__(self) -> str:
        return f"rgb({self.r}, {self.g}, {self.b})"