├── .adr-dir ├── .editorconfig ├── .gitattributes ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.rst ├── ISSUE_TEMPLATE │ ├── 01-bug-report.md │ ├── 02-question.md │ └── 03-feature-request.md ├── PULL_REQUEST_TEMPLATE.md ├── SUPPORT.rst └── workflows │ ├── cron.yml │ └── main.yml ├── .gitignore ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── docs ├── .pages ├── development │ ├── .pages │ └── adr │ │ ├── .pages │ │ ├── 0001-record-architecture-decisions.md │ │ ├── 0002-version-control-our-code.md │ │ ├── 0003-python-3-6-only.md │ │ ├── 0004-python-package-versioning.md │ │ ├── 0005-code-quality-assurance.md │ │ ├── 0006-code-testing.md │ │ ├── 0007-unit-tests.md │ │ ├── 0008-package-structure.md │ │ └── 0009-use-pydantic-for-json-de-serialization.md ├── index.md ├── reference │ ├── .pages │ ├── structurizr.StructurizrClient.md │ └── structurizr.StructurizrClientSettings.md └── requirements.txt ├── examples ├── big_bank.py ├── financial_risk_system.py ├── getting_started.py └── upload_workspace.py ├── mkdocs.yml ├── pyproject.toml ├── setup.cfg ├── setup.py ├── src └── structurizr │ ├── __init__.py │ ├── _version.py │ ├── abstract_base.py │ ├── api │ ├── __init__.py │ ├── api_response.py │ ├── structurizr_client.py │ ├── structurizr_client_exception.py │ └── structurizr_client_settings.py │ ├── base_model.py │ ├── helpers.py │ ├── mixin │ ├── __init__.py │ ├── childless_mixin.py │ ├── model_ref_mixin.py │ └── viewset_ref_mixin.py │ ├── model │ ├── __init__.py │ ├── code_element.py │ ├── code_element_role.py │ ├── component.py │ ├── container.py │ ├── container_instance.py │ ├── deployment_element.py │ ├── deployment_node.py │ ├── element.py │ ├── enterprise.py │ ├── groupable_element.py │ ├── http_health_check.py │ ├── implied_relationship_strategies.py │ ├── infrastructure_node.py │ ├── interaction_style.py │ ├── location.py │ ├── model.py │ ├── model_item.py │ ├── person.py │ ├── perspective.py │ ├── relationship.py │ ├── sequential_integer_id_generator.py │ ├── software_system.py │ ├── software_system_instance.py │ ├── static_structure_element.py │ ├── static_structure_element_instance.py │ └── tags.py │ ├── view │ ├── __init__.py │ ├── abstract_view.py │ ├── animation.py │ ├── automatic_layout.py │ ├── border.py │ ├── branding.py │ ├── color.py │ ├── component_view.py │ ├── configuration.py │ ├── container_view.py │ ├── deployment_view.py │ ├── dynamic_view.py │ ├── element_style.py │ ├── element_view.py │ ├── filtered_view.py │ ├── font.py │ ├── interaction_order.py │ ├── orientation.py │ ├── paper_size.py │ ├── rank_direction.py │ ├── relationship_style.py │ ├── relationship_view.py │ ├── routing.py │ ├── sequence_counter.py │ ├── sequence_number.py │ ├── shape.py │ ├── static_view.py │ ├── styles.py │ ├── system_context_view.py │ ├── system_landscape_view.py │ ├── terminology.py │ ├── vertex.py │ ├── view.py │ ├── view_set.py │ └── view_sort_order.py │ └── workspace.py ├── tests ├── e2e │ └── test_client.py ├── integration │ ├── data │ │ ├── workspace_definition │ │ │ ├── BigBank.json │ │ │ ├── FinancialRiskSystem.json │ │ │ ├── GettingStarted.json │ │ │ ├── Grouping.json │ │ │ └── Trivial.json │ │ └── workspace_validation │ │ │ ├── ChildDeploymentNodeNamesAreNotUnique.json │ │ │ ├── ComponentNamesAreNotUnique.json │ │ │ ├── ContainerAssociatedWithComponentViewIsMissingFromTheModel.json │ │ │ ├── ContainerNamesAreNotUnique.json │ │ │ ├── ElementAssociatedWithDecisionIsMissingFromTheModel.json │ │ │ ├── ElementAssociatedWithDocumentationSectionIsMissingFromTheModel.json │ │ │ ├── ElementAssociatedWithDynamicViewIsMissingFromTheModel.json │ │ │ ├── ElementIdsAreNotUnique.json │ │ │ ├── ElementReferencedByViewIsMissingFromTheModel.json │ │ │ ├── PeopleAndSoftwareSystemNamesAreNotUnique.json │ │ │ ├── RelationshipDescriptionsAreNotUnique.json │ │ │ ├── RelationshipIdsAreNotUnique.json │ │ │ ├── RelationshipReferencedByViewIsMissingFromTheModel.json │ │ │ ├── SoftwareSystemAssociatedWithContainerViewIsMissingFromTheModel.json │ │ │ ├── SoftwareSystemAssociatedWithDeploymentViewIsMissingFromTheModel.json │ │ │ ├── SoftwareSystemAssociatedWithSystemContextViewIsMissingFromTheModel.json │ │ │ ├── TopLevelDeploymentNodeNamesAreNotUnique.json │ │ │ ├── TopLevelDeploymentNodeNamesAreNotUniqueButTheyExistInDifferentEnvironments.json │ │ │ ├── ViewAssociatedWithFilteredViewIsMissingFromTheWorkspace.json │ │ │ └── ViewKeysAreNotUnique.json │ ├── test_api.py │ ├── test_grouping.py │ ├── test_implied_relationship_strategies.py │ ├── test_model_deployment_node_deserialization.py │ ├── test_model_element_relationships.py │ ├── test_model_elements.py │ ├── test_relationship_replication.py │ └── test_workspace_io.py └── unit │ ├── api │ ├── test_api_response.py │ ├── test_structurizr_client.py │ └── test_structurizr_client_settings.py │ ├── model │ ├── test_code_element.py │ ├── test_code_element_role.py │ ├── test_container.py │ ├── test_container_instance.py │ ├── test_deployment_node.py │ ├── test_element.py │ ├── test_enterprise.py │ ├── test_groupable_element.py │ ├── test_infrastructure_node.py │ ├── test_interaction_style.py │ ├── test_location.py │ ├── test_model.py │ ├── test_model_item.py │ ├── test_person.py │ ├── test_perspective.py │ ├── test_relationship.py │ ├── test_software_system.py │ └── test_software_system_instance.py │ ├── test_abstract_base.py │ ├── test_base_model.py │ ├── test_helpers.py │ ├── test_workspace.py │ └── view │ ├── test_color.py │ ├── test_container_view.py │ ├── test_deployment_view.py │ ├── test_dynamic_view.py │ ├── test_filtered_view.py │ ├── test_interaction_order.py │ ├── test_paper_size.py │ ├── test_relationship_view.py │ ├── test_sequence_number.py │ ├── test_static_view.py │ ├── test_view.py │ └── test_view_set.py └── tox.ini /.adr-dir: -------------------------------------------------------------------------------- 1 | docs/development/adr 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor Configuration (http://editorconfig.org) 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | max_line_length = 88 12 | 13 | [*.{json,yml}] 14 | indent_size = 2 15 | 16 | [*.{md,rst}] 17 | trim_trailing_whitespace = false 18 | 19 | [Makefile] 20 | indent_style = tab 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | src/structurizr/_version.py export-subst 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/01-bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug to help improve this project 4 | --- 5 | 6 | 14 | 15 | #### Problem description 16 | 17 | Please explain: 18 | * **what** you tried to achieve, 19 | * **how** you went about it (referring to the code sample), and 20 | * **why** the current behaviour is a problem and what output 21 | you expected instead. 22 | 23 | #### Code Sample 24 | 25 | Create a [minimal, complete, verifiable example](https://stackoverflow.com/help/mcve). 26 | 27 | 28 | ```python 29 | ``` 30 | 31 | 32 | ``` 33 | ``` 34 | 35 | ### Context 36 | 37 | 43 | 44 |
45 | 46 | ``` 47 | ``` 48 | 49 |
50 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/02-question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question 4 | --- 5 | 6 | ### Checklist 7 | 8 | 10 | 11 | - [ ] I searched the [documentation](https://structurizr-python.readthedocs.io). 12 | - [ ] I looked through [similar issues on GitHub](https://github.com/Midnighter/structurizr-python/issues). 13 | - [ ] I looked up "How to do ... in structurizr-python" on a search engine. 14 | 15 | ### Question 16 | 17 | 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/03-feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | ### Checklist 7 | 8 | 9 | 10 | - [ ] There are [no similar issues or pull requests](https://github.com/Midnighter/structurizr-python/issues) for this yet. 11 | 12 | ### Is your feature related to a problem? Please describe it. 13 | 14 | 18 | 19 | ## Describe the solution you would like. 20 | 21 | 26 | 27 | ## Describe alternatives you considered 28 | 29 | 33 | 34 | ## Additional context 35 | 36 | 37 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * [ ] fix #(issue number) 2 | * [ ] description of feature/fix 3 | * [ ] tests added/passed 4 | * [ ] add an entry to the [next release](../CHANGELOG.rst) 5 | -------------------------------------------------------------------------------- /.github/SUPPORT.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Support 3 | ======= 4 | 5 | * structurizr-python `gitter chat `_ 6 | 7 | -------------------------------------------------------------------------------- /.github/workflows/cron.yml: -------------------------------------------------------------------------------- 1 | name: Cron Test 2 | 3 | on: 4 | schedule: 5 | # Run every Tuesday at 11:00. 6 | - cron: '0 11 * * 2' 7 | 8 | jobs: 9 | test: 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: [ubuntu-latest, macos-latest, windows-latest] 15 | python-version: [3.6, 3.7, 3.8, 3.9] 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip setuptools wheel 26 | python -m pip install tox tox-gh-actions 27 | - name: Check requirements with pip 28 | run: python -m pip check . 29 | - name: Test isort 30 | run: tox -e isort 31 | if: matrix.python-version == '3.6' 32 | - name: Test black 33 | run: tox -e black 34 | if: matrix.python-version == '3.6' 35 | - name: Test flake8 36 | run: tox -e flake8 37 | if: matrix.python-version == '3.6' 38 | - name: Test docs 39 | run: tox -e docs 40 | if: matrix.python-version == '3.6' 41 | - name: Test safety 42 | run: tox -e safety 43 | - name: Test suite 44 | run: tox -- --cov-report=xml 45 | env: 46 | SECRET_WORKSPACE_ID: ${{ secrets.STRUCTURIZR_WORKSPACE_ID }} 47 | SECRET_API_KEY: ${{ secrets.STRUCTURIZR_API_KEY }} 48 | SECRET_API_SECRET: ${{ secrets.STRUCTURIZR_API_SECRET }} 49 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI-CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - stable 7 | - devel 8 | tags: 9 | - '[0-9]+.[0-9]+.[0-9]+' 10 | - '[0-9]+.[0-9]+.[0-9]+rc[0-9]+' 11 | pull_request: 12 | branches: 13 | - stable 14 | - devel 15 | 16 | jobs: 17 | test: 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | os: [ubuntu-latest, macos-latest, windows-latest] 23 | python-version: [3.6, 3.7, 3.8, 3.9] 24 | 25 | steps: 26 | - uses: actions/checkout@v2 27 | - name: Set up Python ${{ matrix.python-version }} 28 | uses: actions/setup-python@v2 29 | with: 30 | python-version: ${{ matrix.python-version }} 31 | - name: Install dependencies 32 | run: | 33 | python -m pip install --upgrade pip setuptools wheel 34 | python -m pip install tox tox-gh-actions twine pep517 35 | - name: Check requirements with pip 36 | run: python -m pip check . 37 | - name: Test isort 38 | run: tox -e isort 39 | if: matrix.python-version == '3.6' 40 | - name: Test black 41 | run: tox -e black 42 | if: matrix.python-version == '3.6' 43 | - name: Test flake8 44 | run: tox -e flake8 45 | if: matrix.python-version == '3.6' 46 | - name: Test docs 47 | run: tox -e docs 48 | if: matrix.python-version == '3.6' 49 | - name: Test safety 50 | run: tox -e safety 51 | - name: Test suite 52 | run: tox -- --cov-report=xml 53 | env: 54 | SECRET_WORKSPACE_ID: ${{ secrets.STRUCTURIZR_WORKSPACE_ID }} 55 | SECRET_API_KEY: ${{ secrets.STRUCTURIZR_API_KEY }} 56 | SECRET_API_SECRET: ${{ secrets.STRUCTURIZR_API_SECRET }} 57 | - name: Report coverage 58 | shell: bash 59 | run: bash <(curl -s https://codecov.io/bash) 60 | - name: Check package PEP517 compliance 61 | run: python -m pep517.check . 62 | - name: Build package 63 | run: python -m pep517.build --source --binary . 64 | - name: Check the package compliance 65 | run: twine check dist/* 66 | 67 | release: 68 | needs: test 69 | if: startsWith(github.ref, 'refs/tags') 70 | runs-on: ${{ matrix.os }} 71 | strategy: 72 | matrix: 73 | os: [ubuntu-latest] 74 | python-version: [3.8] 75 | 76 | steps: 77 | - uses: actions/checkout@v2 78 | - name: Set up Python ${{ matrix.python-version }} 79 | uses: actions/setup-python@v2 80 | with: 81 | python-version: ${{ matrix.python-version }} 82 | - name: Get tag 83 | id: tag 84 | run: echo "::set-output name=version::${GITHUB_REF#refs/tags/}" 85 | - name: Install dependencies 86 | run: | 87 | python -m pip install --upgrade pip setuptools wheel 88 | python -m pip install twine pep517 89 | - name: Build package 90 | run: python -m pep517.build --source --binary . 91 | - name: Publish to PyPI 92 | env: 93 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 94 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 95 | run: 96 | twine upload --skip-existing --non-interactive dist/* 97 | - name: Create GitHub release 98 | uses: actions/create-release@v1 99 | env: 100 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 101 | with: 102 | tag_name: ${{ github.ref }} 103 | release_name: ${{ github.ref }} 104 | body: > 105 | Please see 106 | https://github.com/${{ github.repository }}/blob/${{ steps.tag.outputs.version }}/CHANGELOG.rst 107 | for the full release notes. 108 | draft: false 109 | prerelease: false 110 | 111 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | History 3 | ======= 4 | 5 | Next Release 6 | ------------ 7 | 8 | 9 | 0.6.0 (2021-06-10) 10 | ------------------ 11 | * Feat: Add ``DynamicView`` (#77) 12 | * Feat: Add ``FilteredView`` (#81) 13 | * Breaking change: View.find_element_view and find_relationship_view parameter changes. 14 | 15 | 0.5.0 (2021-05-03) 16 | ------------------ 17 | * Feat: Add support for grouping elements (#72) 18 | * Feat: Locking workspace in ``with`` block through ``lock()`` method (#62) 19 | * Fix: On free plans, ignore errors when locking/unlocking workspaces (thanks @maximveksler) 20 | 21 | 0.4.0 (2021-02-05) 22 | ------------------ 23 | * Fix: Don't duplicate relationships if ``add_nearest_neighbours()`` called twice (#63) 24 | * Fix: Support blank diagrams descriptions from the Structurizr UI (#40) 25 | * Fix: External boundaries visible flag in ContainerView now preserved in JSON (#67) 26 | 27 | 0.3.0 (2020-11-29) 28 | ------------------ 29 | * Fix: Better error on bad ``ModelItem`` constructor argument (#50) 30 | * Fix: Suppress archiving of downloaded workspaces by setting archive location to ``None`` (#54) 31 | * Feat: Add ``DeploymentView`` (#55) 32 | 33 | 0.2.1 (2020-11-27) 34 | ------------------ 35 | * Docs: There is now documentation live at https://structurizr-python.readthedocs.io/ 36 | 37 | 0.2.0 (2020-11-20) 38 | ------------------ 39 | * Fix: Add implied relationships to entities (#42) 40 | * Fix: Add ``dump()``, ``dumps()`` and ``loads()`` methods to ``Workspace`` (#48) 41 | * Fix: Add support for DeploymentNodes, ContainerInstances, SoftwareSystemInstances and InfrastructureNodes (#37) 42 | * Fix: Remove need for Model in hydration methods (#52) 43 | 44 | 0.1.1 (2020-10-19) 45 | ------------------ 46 | * Fix: Adjust to change in httpx library API (#38) 47 | 48 | 0.1.0 (2020-10-15) 49 | ------------------ 50 | * Fix: Resolve overlap between add methods (#13) 51 | * Fix: Add IDs to the big bank example (#16) 52 | * Fix: Preserve tag order as in other SDKs (#22) 53 | * Fix: Consistency of element relationships (#31) 54 | * Add Python 3.9 to the test suite 55 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | include src/structurizr/_version.py 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: qa 2 | 3 | ################################################################################ 4 | # COMMANDS # 5 | ################################################################################ 6 | 7 | ## Apply code quality assurance tools. 8 | qa: 9 | isort src/structurizr/ tests/ examples/ setup.py 10 | black src/structurizr/ tests/ examples/ setup.py 11 | 12 | ## Prepare a release by generating the automatic code documentation. 13 | release: 14 | sphinx-apidoc -f -o docs/source/autogen src/structurizr 15 | 16 | ################################################################################ 17 | # Self Documenting Commands # 18 | ################################################################################ 19 | 20 | .DEFAULT_GOAL := show-help 21 | 22 | # Inspired by 23 | # 24 | # sed script explained: 25 | # /^##/: 26 | # * save line in hold space 27 | # * purge line 28 | # * Loop: 29 | # * append newline + line to hold space 30 | # * go to next line 31 | # * if line starts with doc comment, strip comment character off and loop 32 | # * remove target prerequisites 33 | # * append hold space (+ newline) to line 34 | # * replace newline plus comments by `---` 35 | # * print line 36 | # Separate expressions are necessary because labels cannot be delimited by 37 | # semicolon; see 38 | .PHONY: show-help 39 | show-help: 40 | @echo "$$(tput bold)Available rules:$$(tput sgr0)" 41 | @echo 42 | @sed -n -e "/^## / { \ 43 | h; \ 44 | s/.*//; \ 45 | :doc" \ 46 | -e "H; \ 47 | n; \ 48 | s/^## //; \ 49 | t doc" \ 50 | -e "s/:.*//; \ 51 | G; \ 52 | s/\\n## /---/; \ 53 | s/\\n/ /g; \ 54 | p; \ 55 | }" ${MAKEFILE_LIST} \ 56 | | LC_ALL='C' sort --ignore-case \ 57 | | awk -F '---' \ 58 | -v ncol=$$(tput cols) \ 59 | -v indent=19 \ 60 | -v col_on="$$(tput setaf 6)" \ 61 | -v col_off="$$(tput sgr0)" \ 62 | '{ \ 63 | printf "%s%*s%s ", col_on, -indent, $$1, col_off; \ 64 | n = split($$2, words, " "); \ 65 | line_length = ncol - indent; \ 66 | for (i = 1; i <= n; i++) { \ 67 | line_length -= length(words[i]) + 1; \ 68 | if (line_length <= 0) { \ 69 | line_length = ncol - indent - length(words[i]) - 1; \ 70 | printf "\n%*s ", -indent, " "; \ 71 | } \ 72 | printf "%s ", words[i]; \ 73 | } \ 74 | printf "\n"; \ 75 | }' \ 76 | | more $(shell test $(shell uname) = Darwin && \ 77 | echo '--no-init --raw-control-chars') 78 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============================= 2 | Structurizr for Python 3 | ============================= 4 | 5 | .. image:: https://img.shields.io/pypi/v/structurizr-python.svg 6 | :target: https://pypi.org/project/structurizr-python/ 7 | :alt: Current PyPI Version 8 | 9 | .. image:: https://img.shields.io/pypi/pyversions/structurizr-python.svg 10 | :target: https://pypi.org/project/structurizr-python/ 11 | :alt: Supported Python Versions 12 | 13 | .. image:: https://img.shields.io/pypi/l/structurizr-python.svg 14 | :target: https://www.apache.org/licenses/LICENSE-2.0 15 | :alt: Apache Software License Version 2.0 16 | 17 | .. image:: https://img.shields.io/badge/Contributor%20Covenant-v1.4%20adopted-ff69b4.svg 18 | :target: .github/CODE_OF_CONDUCT.md 19 | :alt: Code of Conduct 20 | 21 | .. image:: https://github.com/Midnighter/structurizr-python/workflows/CI-CD/badge.svg 22 | :target: https://github.com/Midnighter/structurizr-python/workflows/CI-CD 23 | :alt: GitHub Actions 24 | 25 | .. image:: https://codecov.io/gh/Midnighter/structurizr-python/branch/devel/graph/badge.svg 26 | :target: https://codecov.io/gh/Midnighter/structurizr-python 27 | :alt: Codecov 28 | 29 | .. image:: https://img.shields.io/badge/code%20style-black-000000.svg 30 | :target: https://github.com/ambv/black 31 | :alt: Black 32 | 33 | .. image:: https://readthedocs.org/projects/structurizr-python/badge/?version=latest 34 | :target: https://structurizr-python.readthedocs.io/en/latest/?badge=latest 35 | :alt: Documentation Status 36 | 37 | .. summary-start 38 | 39 | A Python client package for the Structurizr cloud service and on-premises installation. 40 | 41 | Archive Mode 42 | ============ 43 | 44 | **Take Note:** I realized that I rely more and more on the `Structurizr DSL 45 | `_ and less on this Python client. Since this 46 | is a hobby project and my time is limited, I decided that I will stop supporting 47 | this package for now and put it into archive mode. If you depend on this 48 | project, you can always install the last version from PyPI; and if you want to 49 | pick up maintenance & development, please contact me by email or on the 50 | `Structurizr Slack #python `_ 51 | channel. I will be happy to introduce you to the codebase and assist in any way 52 | I can. 53 | 54 | Warning 55 | ======= 56 | 57 | The structurizr-python package is in active development and should be considered Alpha 58 | software. Reports of problems are appreciated but please do not expect fully working 59 | software at this point. If you want to get involved, you are most welcome as this is 60 | a spare time project. Please write me an e-mail or on the 61 | `Structurizr Slack team `_ so that we can coordinate. 62 | 63 | Install 64 | ======= 65 | 66 | It's as simple as: 67 | 68 | .. code-block:: console 69 | 70 | pip install structurizr-python 71 | 72 | Copyright 73 | ========= 74 | 75 | * Copyright © 2020, Moritz E. Beber. 76 | * Free software distributed under the `Apache Software License 2.0 77 | `_. 78 | 79 | .. summary-end 80 | -------------------------------------------------------------------------------- /docs/.pages: -------------------------------------------------------------------------------- 1 | nav: 2 | - Overview: index.md 3 | - reference 4 | - ... 5 | -------------------------------------------------------------------------------- /docs/development/.pages: -------------------------------------------------------------------------------- 1 | title: Development Documentation 2 | 3 | nav: 4 | - ... 5 | - Contributing Guidelines: https://github.com/Midnighter/structurizr-python/blob/devel/.github/CONTRIBUTING.rst 6 | - Issue Tracker: https://github.com/Midnighter/structurizr-python/issues/ 7 | - Development Plan: https://github.com/Midnighter/structurizr-python/projects/1/ 8 | - adr 9 | 10 | -------------------------------------------------------------------------------- /docs/development/adr/.pages: -------------------------------------------------------------------------------- 1 | title: Architecture Decision Records 2 | 3 | nav: 4 | - ... 5 | -------------------------------------------------------------------------------- /docs/development/adr/0001-record-architecture-decisions.md: -------------------------------------------------------------------------------- 1 | # 1. Record architecture decisions 2 | 3 | Date: 2019-02-15 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | We need to record the architectural decisions made on this project. 12 | 13 | ## Decision 14 | 15 | We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). 16 | 17 | ## Consequences 18 | 19 | See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools). 20 | -------------------------------------------------------------------------------- /docs/development/adr/0002-version-control-our-code.md: -------------------------------------------------------------------------------- 1 | # 2. Version control our code 2 | 3 | Date: 2019-02-15 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | We need to version control our code in order to avoid disasters and maintain 12 | sanity. We also want to collaborate online with a wider community. 13 | 14 | ## Decision 15 | 16 | We use git for version control and GitHub for collaboration. 17 | 18 | ## Consequences 19 | 20 | Standard practice. 21 | 22 | -------------------------------------------------------------------------------- /docs/development/adr/0003-python-3-6-only.md: -------------------------------------------------------------------------------- 1 | # 3. Python 3.6+ only 2 | 3 | Date: 2019-02-15 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | Python 2 support will be discontinued in 2020. Python 3.6 is the first version 12 | to natively support f-strings which are sweet. 13 | 14 | ## Decision 15 | 16 | We make an early decision to only support Python 3.6 and above. 17 | 18 | ## Consequences 19 | 20 | We have a single code base targetting only one major version. We can use 21 | f-strings such as `f"Hello {name}!"`. 22 | -------------------------------------------------------------------------------- /docs/development/adr/0004-python-package-versioning.md: -------------------------------------------------------------------------------- 1 | # 4. Python package versioning 2 | 3 | Date: 2019-02-15 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | We need a simple way to manage our package version. 12 | 13 | ## Decision 14 | 15 | We use versioneer to do this for us. 16 | 17 | ## Consequences 18 | 19 | We can create new release versions simply by creating a corresponding git tag. 20 | Currently, if you want to do a local `pip install .`, this only works for 21 | pip<19. 22 | -------------------------------------------------------------------------------- /docs/development/adr/0005-code-quality-assurance.md: -------------------------------------------------------------------------------- 1 | # 5. Code quality assurance 2 | 3 | Date: 2019-02-15 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | Writing code that adheres to style guides and other best practices can be 12 | annoying. We want to standardize on some best-in-class tools. 13 | 14 | ## Decision 15 | 16 | We will use isort, black, and flake8. 17 | 18 | ## Consequences 19 | 20 | The tool isort creates well formatted imports. Black is a pedantic tool that 21 | re-formats your code in a particular style. This removes burden from the 22 | individual programmer once they relinquish control. We use flake8 to later check 23 | all style guidelines. 24 | -------------------------------------------------------------------------------- /docs/development/adr/0006-code-testing.md: -------------------------------------------------------------------------------- 1 | # 6. Code Testing 2 | 3 | Date: 2019-02-15 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | Setting up different testing environments and configurations can be a painful 12 | and error prone process. 13 | 14 | ## Decision 15 | 16 | We use tox to define, configure, and run different test scenarios. 17 | 18 | ## Consequences 19 | 20 | Using tox means every developer will have reproducible test scenarios even 21 | though it causes a slight burden in proper configuration. 22 | -------------------------------------------------------------------------------- /docs/development/adr/0007-unit-tests.md: -------------------------------------------------------------------------------- 1 | # 7. Unit tests 2 | 3 | Date: 2019-02-15 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | We need to make a decision on the testing framework for our project. 12 | 13 | ## Decision 14 | 15 | We will make use of pytest. It is a de facto standard in the Python community 16 | and has unrivaled power. 17 | 18 | ## Consequences 19 | 20 | There is a learning curve to pytest if you have never used it before. 21 | -------------------------------------------------------------------------------- /docs/development/adr/0008-package-structure.md: -------------------------------------------------------------------------------- 1 | # 8. Package structure 2 | 3 | Date: 2019-02-15 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | We try to structure our package in logical sub-units but we want to maintain a 12 | consistent public interface. 13 | 14 | ## Decision 15 | 16 | We allow for arbitrarily nested sub-packages but export important classes and 17 | functions to the top level thus exposing a public interface. Our unit tests 18 | should reflect this package structure. 19 | 20 | ## Consequences 21 | 22 | Creating many modules and sub-packages can increase complexity of dependencies 23 | internally but will improve separation and use of clearly defined intefaces. 24 | -------------------------------------------------------------------------------- /docs/development/adr/0009-use-pydantic-for-json-de-serialization.md: -------------------------------------------------------------------------------- 1 | # 9. Use pydantic for JSON (de-)serialization 2 | 3 | Date: 2020-06-09 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | In order to interact with a remote workspace, for example, at structurizr.com. 12 | The remote or local workspace has to be (de-)serialized from or to JSON. 13 | 14 | ## Decision 15 | 16 | In order to perform these operations we choose 17 | [pydantic](https://pydantic-docs.helpmanual.io/) which has a nice API, active 18 | community, good data validation, helpful documentation, and good performance. 19 | 20 | ## Consequences 21 | 22 | We separate the models representing Structurizr entities and their business 23 | logic from how those models are (de-)serialized. That means that for each model 24 | we have a corresponding IO pydantic model describing the JSON data model. 25 | 26 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Structurizr for Python 2 | 3 | A Python client package for the [Structurizr](https://structurizr.com/) cloud 4 | service and on-premises installation. 5 | 6 | ## Installation 7 | 8 | For the moment, the main installation method is from PyPI, for example, using 9 | `pip`. 10 | 11 | ``` 12 | pip install structurizr-python 13 | ``` 14 | 15 | ## Getting Started 16 | 17 | There are two starting points. Either you have no Structurizr workspace or you 18 | have an existing workspace that you want to modify locally. 19 | 20 | 1. If you don't have a workspace yet, you can sign up for one at 21 | [Structurizr](https://structurizr.com/help/getting-started). Once you have a 22 | workspace, take note of its ID, API key, and secret. In order to get started 23 | with creating a new workspace, take a look at [the 24 | examples](https://github.com/Midnighter/structurizr-python/tree/devel/examples). 25 | In particular the 26 | [getting-started](https://github.com/Midnighter/structurizr-python/blob/devel/examples/getting_started.py) 27 | script will be suitable. 28 | 29 | The `#!python main()` function in each example script creates a more or less 30 | involved workspace for you. When you have created a workspace, it is time 31 | to upload it so that you can create diagrams for it. You will need to 32 | create a 33 | [`StructurizrClient`][structurizr.api.structurizr_client.StructurizrClient] 34 | instance and its 35 | [settings][structurizr.api.structurizr_client_settings.StructurizrClientSettings]. 36 | The settings can be provided as arguments, be read from environment 37 | variables, or be provided in a `.env` file. 38 | 39 | ```python 40 | from structurizr import StructurizrClient, StructurizrClientSettings 41 | 42 | workspace = main() 43 | settings = StructurizrClientSettings( 44 | workspace_id=1234, 45 | api_key='your api key', 46 | api_secret='your api secret', 47 | ) 48 | client = StructurizrClient(settings=settings) 49 | client.put_workspace(workspace) 50 | ``` 51 | 52 | The example should now be available in your online workspace. 53 | 54 | 2. In case you already have a comprehensive workspace online, the Python client 55 | can help with creating local copies and modifying it. 56 | 57 | ```python 58 | from structurizr import StructurizrClient, StructurizrClientSettings 59 | 60 | settings = StructurizrClientSettings( 61 | workspace_id=1234, 62 | api_key='your api key', 63 | api_secret='your api secret', 64 | ) 65 | client = StructurizrClient(settings=settings) 66 | workspace = client.get_workspace() 67 | ``` 68 | 69 | You can then modify the workspace as you please and upload your new version 70 | as shown above. 71 | 72 | ## Copyright 73 | 74 | * Copyright © 2020, Moritz E. Beber. 75 | * Free software distributed under the [Apache Software License 76 | 2.0](https://www.apache.org/licenses/LICENSE-2.0). 77 | -------------------------------------------------------------------------------- /docs/reference/.pages: -------------------------------------------------------------------------------- 1 | title: API Reference 2 | 3 | nav: 4 | - ... 5 | -------------------------------------------------------------------------------- /docs/reference/structurizr.StructurizrClient.md: -------------------------------------------------------------------------------- 1 | ::: structurizr.StructurizrClient 2 | selection: 3 | members: 4 | - get_workspace 5 | - put_workspace 6 | - lock_workspace 7 | - unlock_workspace 8 | -------------------------------------------------------------------------------- /docs/reference/structurizr.StructurizrClientSettings.md: -------------------------------------------------------------------------------- 1 | ::: structurizr.StructurizrClientSettings 2 | selection: 3 | filters: 4 | - '!Config' 5 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | livereload ~=2.6 # Needed by mkdocstrings 2 | mkdocs ~=1.1 3 | mkdocs-material ~=6.1 4 | mkdocs-awesome-pages-plugin ~=2.4 5 | mkdocstrings ~=0.14 6 | versioneer-518 7 | -------------------------------------------------------------------------------- /examples/getting_started.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """ 17 | Provide a 'getting started' example. 18 | 19 | Illustrate how to create a software architecture diagram using code. 20 | """ 21 | 22 | 23 | import logging 24 | 25 | from structurizr import Workspace 26 | from structurizr.model import Tags 27 | from structurizr.view import ElementStyle, Shape 28 | from structurizr.view.paper_size import PaperSize 29 | 30 | 31 | def main() -> Workspace: 32 | """Create the 'getting started' example.""" 33 | workspace = Workspace( 34 | name="Getting Started", 35 | description="This is a model of my software system.", 36 | ) 37 | 38 | model = workspace.model 39 | 40 | user = model.add_person(name="User", description="A user of my software system.") 41 | software_system = model.add_software_system( 42 | name="Software System", description="My software system." 43 | ) 44 | user.uses(software_system, "Uses") 45 | 46 | context_view = workspace.views.create_system_context_view( 47 | software_system=software_system, 48 | key="SystemContext", 49 | description="An example of a System Context diagram.", 50 | ) 51 | context_view.add_all_elements() 52 | context_view.paper_size = PaperSize.A5_Landscape 53 | 54 | styles = workspace.views.configuration.styles 55 | styles.add( 56 | ElementStyle(tag=Tags.SOFTWARE_SYSTEM, background="#1168bd", color="#ffffff") 57 | ) 58 | styles.add( 59 | ElementStyle( 60 | tag=Tags.PERSON, 61 | background="#08427b", 62 | color="#ffffff", 63 | shape=Shape.Person, 64 | ) 65 | ) 66 | return workspace 67 | 68 | 69 | if __name__ == "__main__": 70 | logging.basicConfig(level="INFO") 71 | main() 72 | -------------------------------------------------------------------------------- /examples/upload_workspace.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2020, Moritz E. Beber. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | 18 | """Provide a command line tool for uploading workspace examples.""" 19 | 20 | 21 | import logging 22 | import sys 23 | from importlib import import_module 24 | 25 | from structurizr import StructurizrClient, StructurizrClientSettings 26 | 27 | 28 | if __name__ == "__main__": 29 | logging.basicConfig(level="INFO") 30 | example = import_module(sys.argv[1]) 31 | workspace = example.main() 32 | settings = StructurizrClientSettings() 33 | workspace.id = settings.workspace_id 34 | client = StructurizrClient(settings=settings) 35 | with client.lock(): 36 | client.put_workspace(workspace) 37 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Structurizr-Python 2 | site_description: A Python client package for the Structurizr cloud service and on-premises installation. 3 | site_author: Moritz E. Beber 4 | site_url: https://structurizr-python.readthedocs.io/ 5 | 6 | markdown_extensions: 7 | - pymdownx.highlight 8 | - pymdownx.superfences 9 | - pymdownx.inlinehilite 10 | 11 | theme: 12 | name: material 13 | palette: 14 | scheme: preference 15 | primary: blue 16 | accent: cyan 17 | language: en 18 | features: 19 | - navigation.instant 20 | icon: 21 | repo: fontawesome/brands/github 22 | 23 | plugins: 24 | - search: 25 | prebuild_index: true 26 | - awesome-pages 27 | - mkdocstrings: 28 | default_handler: python 29 | handlers: 30 | python: 31 | rendering: 32 | show_source: true 33 | show_root_heading: false 34 | show_root_toc_entry: true 35 | show_root_full_path: false 36 | watch: 37 | - src/structurizr 38 | 39 | extra: 40 | social: 41 | - icon: fontawesome/brands/slack 42 | link: https://join.slack.com/t/structurizr/shared_invite/enQtMzkyMjY1NzMwNTkzLTcyOGI1MTZmNDQwMDQ5YmZlMThiYmU1ZTM2ZWZiMzYwMjVhNmM0OWIwNjFlZTM1YmY3YzU0ZDY2MTA1YTk5Mjg 43 | name: Join the Structurizr Slack 44 | - icon: fontawesome/brands/github 45 | link: https://github.com/Midnighter 46 | name: Moritz on GitHub 47 | - icon: fontawesome/brands/twitter 48 | link: https://twitter.com/me_beber 49 | name: Moritz on Twitter 50 | - icon: fontawesome/brands/linkedin 51 | link: https://www.linkedin.com/in/moritz-beber-b597a55a/ 52 | name: Moritz on LinkedIn 53 | 54 | copyright: Copyright © 2020, Moritz E. Beber. 55 | 56 | repo_url: https://github.com/Midnighter/structurizr-python 57 | repo_name: Midnighter/structurizr-python 58 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = 'setuptools.build_meta' 3 | requires = [ 4 | 'setuptools>=40.6.0', 5 | 'versioneer-518', 6 | 'wheel' 7 | ] 8 | 9 | [tool.black] 10 | line-length = 88 11 | python-version = ['py36'] 12 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = structurizr-python 3 | url = https://github.com/Midnighter/structurizr-python 4 | download_url = https://pypi.org/project/structurizr-python/ 5 | project_urls = 6 | Source Code = https://github.com/Midnighter/structurizr-python 7 | Documentation = https://structurizr-python.readthedocs.io 8 | Bug Tracker = https://github.com/Midnighter/structurizr-python/issues 9 | author = Moritz E. Beber 10 | author_email = midnighter@posteo.net 11 | # Please consult https://pypi.org/classifiers/ for a full list. 12 | classifiers = 13 | Development Status :: 3 - Alpha 14 | Environment :: Web Environment 15 | Intended Audience :: Developers 16 | Intended Audience :: Information Technology 17 | License :: OSI Approved :: Apache Software License 18 | Natural Language :: English 19 | Operating System :: OS Independent 20 | Programming Language :: Python :: 3.6 21 | Programming Language :: Python :: 3.7 22 | Programming Language :: Python :: 3.8 23 | Programming Language :: Python :: 3.9 24 | Programming Language :: Python :: 3 :: Only 25 | Topic :: Software Development :: Documentation 26 | license = Apache-2.0 27 | description = A Python client package for the Structurizr cloud service and on-premises installation. 28 | long_description = file: README.rst 29 | long_description_content_type = text/x-rst 30 | keywords = 31 | Structurizr 32 | Structurizr-API 33 | C4 model 34 | software architecture 35 | diagrams-as-code 36 | diagrams 37 | 38 | [options] 39 | zip_safe = True 40 | install_requires = 41 | depinfo 42 | httpx ~= 0.16 43 | importlib_metadata; python_version <'3.8' 44 | ordered-set 45 | pydantic >= 1.8.2 46 | python-dotenv 47 | python_requires = >=3.6 48 | tests_require = 49 | tox 50 | packages = find: 51 | package_dir = 52 | = src 53 | 54 | [options.packages.find] 55 | where = src 56 | 57 | [options.extras_require] 58 | development = 59 | black 60 | isort 61 | pep517 62 | tox 63 | 64 | # See the docstring in versioneer.py for instructions. Note that you must 65 | # re-run 'versioneer.py setup' after changing this section, and commit the 66 | # resulting files. 67 | 68 | [versioneer] 69 | VCS = git 70 | style = pep440 71 | versionfile_source = src/structurizr/_version.py 72 | versionfile_build = structurizr/_version.py 73 | tag_prefix = 74 | 75 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | 4 | # Copyright (c) 2020, Moritz E. Beber. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | 19 | """Set up the Structurizr for Python package.""" 20 | 21 | 22 | import versioneer 23 | from setuptools import setup 24 | 25 | 26 | # All other arguments are defined in `setup.cfg`. 27 | setup(version=versioneer.get_version(), cmdclass=versioneer.get_cmdclass()) 28 | -------------------------------------------------------------------------------- /src/structurizr/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Create top level imports.""" 17 | 18 | 19 | __author__ = "Moritz E. Beber" 20 | __email__ = "midnighter@posteo.net" 21 | 22 | 23 | from .helpers import show_versions 24 | from .workspace import Workspace, WorkspaceIO 25 | from .api import ( 26 | StructurizrClient, 27 | StructurizrClientException, 28 | StructurizrClientSettings, 29 | ) 30 | -------------------------------------------------------------------------------- /src/structurizr/_version.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Midnighter/structurizr-python/31f1dcadb3ff113d8a77ce132657237ea01c307b/src/structurizr/_version.py -------------------------------------------------------------------------------- /src/structurizr/abstract_base.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a common abstract base class.""" 17 | 18 | 19 | from abc import ABC 20 | 21 | 22 | __all__ = ("AbstractBase",) 23 | 24 | 25 | class AbstractBase(ABC): # noqa: B024 26 | """Define common business logic through an abstract base class.""" 27 | 28 | def __init__(self, **kwargs): 29 | """ 30 | Initialize an abstract base class. 31 | 32 | The AbstractBase class is designed to be the singular root of the entire class 33 | hierarchy, similar to `object`, and acts as a guard against unknown keyword 34 | arguments. Any keyword arguments not consumed in the hierarchy above cause a 35 | `TypeError`. 36 | 37 | """ 38 | if kwargs: 39 | phrase = ( 40 | "unexpected keyword arguments" 41 | if len(kwargs) > 1 42 | else "an unexpected keyword argument" 43 | ) 44 | message = "\n ".join(f"{key}={value}" for key, value in kwargs.items()) 45 | raise TypeError( 46 | f"{type(self).__name__}.__init__() got {phrase}:\n {message}" 47 | ) 48 | super().__init__() 49 | 50 | def __hash__(self) -> int: 51 | """Return an integer that represents a unique hash value for this instance.""" 52 | return id(self) 53 | 54 | def __repr__(self) -> str: 55 | """Return a string representation of this instance.""" 56 | return f"{type(self).__name__}({id(self)})" 57 | -------------------------------------------------------------------------------- /src/structurizr/api/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide API classes and functions.""" 17 | 18 | 19 | from .api_response import APIResponse 20 | from .structurizr_client_exception import StructurizrClientException 21 | from .structurizr_client_settings import StructurizrClientSettings 22 | from .structurizr_client import StructurizrClient 23 | -------------------------------------------------------------------------------- /src/structurizr/api/api_response.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide the Structurizr client.""" 17 | 18 | 19 | from typing import Optional 20 | 21 | from pydantic import Field 22 | 23 | from ..base_model import BaseModel 24 | 25 | 26 | __all__ = ("APIResponse",) 27 | 28 | 29 | class APIResponse(BaseModel): 30 | """ 31 | Define a Structurizr API response. 32 | 33 | An API response indicating success or failure. 34 | 35 | Attributes: 36 | success (bool): `True` if the API call was successful, `False` otherwise. 37 | message (str): A human readable response message. 38 | revision (int, optional): The internal revision number. 39 | 40 | """ 41 | 42 | success: bool = Field( 43 | ..., description="`True` if the API call was successful, `False` otherwise." 44 | ) 45 | message: str = Field(..., description="A human readable response message.") 46 | revision: Optional[int] = Field(None, description="The internal revision number.") 47 | -------------------------------------------------------------------------------- /src/structurizr/api/structurizr_client_exception.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a Structurizr client error.""" 17 | 18 | 19 | __all__ = ("StructurizrClientException",) 20 | 21 | 22 | class StructurizrClientException(ConnectionError): 23 | """ 24 | Define a Structurizr client error. 25 | 26 | Thrown by the `StructurizrClient` when something goes wrong. 27 | 28 | """ 29 | 30 | pass 31 | -------------------------------------------------------------------------------- /src/structurizr/base_model.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a customized base model.""" 17 | 18 | 19 | from pydantic import BaseModel as BaseModel_ 20 | 21 | 22 | __all__ = ("BaseModel",) 23 | 24 | 25 | class BaseModel(BaseModel_): 26 | """Define a customized base model.""" 27 | 28 | class Config: 29 | """Define default configuration options for all models.""" 30 | 31 | anystr_strip_whitespace = True 32 | allow_population_by_field_name = True 33 | orm_mode = True 34 | 35 | def dict( 36 | self, 37 | *, 38 | by_alias: bool = True, 39 | exclude_defaults: bool = False, 40 | exclude_none: bool = True, 41 | **kwargs 42 | ) -> dict: 43 | """ 44 | Serialize the model using custom settings. 45 | 46 | Args: 47 | by_alias (bool, optional): Whether to create serialized field names by 48 | their alias (default `True`). 49 | exclude_defaults (bool, optional): Whether to exclude fields that are equal 50 | to their default value from being serialized (default `False`). 51 | exclude_none (bool, optional): Whether to exclude keys with `None` values 52 | entirely (default `True`). 53 | **kwargs: Further keyword arguments are passed to the pydantic super method 54 | `.dict`. 55 | 56 | Returns: 57 | dict: The serialized model as a (nested) dictionary. 58 | 59 | See Also: 60 | pydantic.BaseModel.dict 61 | 62 | """ 63 | return super().dict( 64 | by_alias=by_alias, 65 | exclude_defaults=exclude_defaults, 66 | exclude_none=exclude_none, 67 | **kwargs 68 | ) 69 | 70 | def json( 71 | self, *, by_alias: bool = True, exclude_defaults: bool = True, **kwargs 72 | ) -> str: 73 | """ 74 | Serialize the model using custom settings. 75 | 76 | Args: 77 | by_alias (bool, optional): Whether to create serialized field names by 78 | their alias (default `True`). 79 | exclude_defaults (bool, optional): Whether to exclude fields that are equal 80 | to their default value from being serialized (default `True`). 81 | **kwargs: 82 | 83 | Returns: 84 | str: The serialized model as a JSON string. 85 | 86 | See Also: 87 | pydantic.BaseModel.json 88 | 89 | """ 90 | return super().json( 91 | by_alias=by_alias, exclude_defaults=exclude_defaults, **kwargs 92 | ) 93 | -------------------------------------------------------------------------------- /src/structurizr/helpers.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Define general helper functions.""" 17 | 18 | 19 | from depinfo import print_dependencies 20 | 21 | 22 | def show_versions() -> None: 23 | """Print dependency information.""" 24 | print_dependencies("structurizr-python") 25 | -------------------------------------------------------------------------------- /src/structurizr/mixin/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide mixins that modify class behaviour.""" 17 | 18 | 19 | from .model_ref_mixin import ModelRefMixin 20 | from .viewset_ref_mixin import ViewSetRefMixin 21 | -------------------------------------------------------------------------------- /src/structurizr/mixin/childless_mixin.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a mixin that indicates an element type does not have children.""" 17 | 18 | 19 | from typing import Iterable 20 | 21 | from ..model.element import Element 22 | 23 | 24 | class ChildlessMixin: 25 | """Define a mixin for childless element types.""" 26 | 27 | @property 28 | def child_elements(self) -> Iterable[Element]: 29 | """Return child elements (from `Element.children`).""" 30 | return [] 31 | -------------------------------------------------------------------------------- /src/structurizr/mixin/model_ref_mixin.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a mixin that includes a model reference.""" 17 | 18 | 19 | from typing import TYPE_CHECKING 20 | from weakref import ref 21 | 22 | 23 | if TYPE_CHECKING: # pragma: no cover 24 | from ..model import Model 25 | 26 | 27 | __all__ = ("ModelRefMixin",) 28 | 29 | 30 | class ModelRefMixin: 31 | """Define a model reference mixin.""" 32 | 33 | def __init__(self, **kwargs) -> None: 34 | """Initialize the mixin.""" 35 | super().__init__(**kwargs) 36 | self._model = lambda: None 37 | 38 | @property 39 | def model(self) -> "Model": 40 | """Return the referenced model.""" 41 | return self.get_model() 42 | 43 | @property 44 | def has_model(self) -> bool: 45 | """Return whether a model has been set.""" 46 | return self._model() is not None 47 | 48 | def get_model(self) -> "Model": 49 | """ 50 | Retrieve the model instance from the reference. 51 | 52 | Returns: 53 | Model: The model, if any. 54 | 55 | Raises: 56 | RuntimeError: In case there exists no referenced model. 57 | 58 | """ 59 | model = self._model() 60 | if model is None: 61 | raise RuntimeError( 62 | f"You must add this {type(self).__name__} element to a model instance " 63 | f"first." 64 | ) 65 | return model 66 | 67 | def set_model(self, model: "Model") -> None: 68 | """ 69 | Create a weak reference to the model instance. 70 | 71 | Warnings: 72 | This is an internal method and should not be directly called by users. 73 | 74 | Args: 75 | model (Model): 76 | 77 | """ 78 | self._model = ref(model) 79 | -------------------------------------------------------------------------------- /src/structurizr/mixin/viewset_ref_mixin.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a mixin that includes a view set reference.""" 17 | 18 | 19 | from typing import TYPE_CHECKING 20 | from weakref import ref 21 | 22 | 23 | if TYPE_CHECKING: 24 | from ..view import ViewSet 25 | 26 | 27 | __all__ = ("ViewSetRefMixin",) 28 | 29 | 30 | class ViewSetRefMixin: 31 | """Define a view set reference mixin.""" 32 | 33 | def __init__(self, **kwargs) -> None: 34 | """Initialize the mixin.""" 35 | super().__init__(**kwargs) 36 | self._viewset = lambda: None 37 | 38 | def get_viewset(self) -> "ViewSet": 39 | """ 40 | Retrieve the view set instance from the reference. 41 | 42 | Returns: 43 | ViewSet: The view set, if any. 44 | 45 | Raises: 46 | RuntimeError: In case there exists no referenced view set. 47 | 48 | """ 49 | view_set = self._viewset() 50 | if view_set is None: 51 | raise RuntimeError( 52 | f"You must add this {type(self).__name__} view to a view set instance " 53 | f"first." 54 | ) 55 | return view_set 56 | 57 | def set_viewset(self, viewset: "ViewSet") -> None: 58 | """ 59 | Create a weak reference to a view set instance. 60 | 61 | Warnings: 62 | This is an internal method and should not be directly called by users. 63 | 64 | Args: 65 | viewset (ViewSet): 66 | 67 | """ 68 | self._viewset = ref(viewset) 69 | -------------------------------------------------------------------------------- /src/structurizr/model/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide models for defining a Structurizr software architecture.""" 17 | 18 | 19 | from .enterprise import Enterprise, EnterpriseIO 20 | from .perspective import Perspective, PerspectiveIO 21 | from .location import Location 22 | from .interaction_style import InteractionStyle 23 | from .element import Element, ElementIO 24 | from .person import Person, PersonIO 25 | from .software_system import SoftwareSystem, SoftwareSystemIO 26 | from .relationship import Relationship, RelationshipIO 27 | from .model import Model, ModelIO 28 | from .tags import Tags 29 | from .container import Container, ContainerIO 30 | from .component import Component, ComponentIO 31 | 32 | 33 | _symbols = locals() 34 | 35 | RelationshipIO.update_forward_refs(**_symbols) 36 | -------------------------------------------------------------------------------- /src/structurizr/model/code_element_role.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a representation of a code element's role.""" 17 | 18 | 19 | from enum import Enum, unique 20 | 21 | 22 | __all__ = ("CodeElementRole",) 23 | 24 | 25 | @unique 26 | class CodeElementRole(Enum): 27 | """Represent a code element's role.""" 28 | 29 | Primary = "Primary" 30 | Supporting = "Supporting" 31 | -------------------------------------------------------------------------------- /src/structurizr/model/container_instance.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a container instance model.""" 17 | 18 | 19 | from typing import TYPE_CHECKING 20 | 21 | from pydantic import Field 22 | 23 | from .container import Container 24 | from .static_structure_element_instance import ( 25 | StaticStructureElementInstance, 26 | StaticStructureElementInstanceIO, 27 | ) 28 | from .tags import Tags 29 | 30 | 31 | if TYPE_CHECKING: # pragma: no cover 32 | from .deployment_node import DeploymentNode 33 | 34 | 35 | __all__ = ("ContainerInstance", "ContainerInstanceIO") 36 | 37 | 38 | class ContainerInstanceIO(StaticStructureElementInstanceIO): 39 | """Represents a container instance which can be added to a deployment node.""" 40 | 41 | container_id: str = Field(alias="containerId") 42 | 43 | 44 | class ContainerInstance(StaticStructureElementInstance): 45 | """Represents a container instance which can be added to a deployment node.""" 46 | 47 | def __init__(self, *, container: Container, **kwargs) -> None: 48 | """Initialize a container instance.""" 49 | super().__init__(element=container, **kwargs) 50 | self.tags.add(Tags.CONTAINER_INSTANCE) 51 | 52 | @property 53 | def container(self) -> Container: 54 | """Return the container for this instance.""" 55 | return self.element 56 | 57 | @property 58 | def container_id(self) -> str: 59 | """Return the ID of the container for this instance.""" 60 | return self.container.id 61 | 62 | @classmethod 63 | def hydrate( 64 | cls, 65 | container_instance_io: ContainerInstanceIO, 66 | container: Container, 67 | parent: "DeploymentNode", 68 | ) -> "ContainerInstance": 69 | """Hydrate a new ContainerInstance instance from its IO. 70 | 71 | This will also automatically register with the model. 72 | """ 73 | instance = cls( 74 | **cls.hydrate_arguments(container_instance_io), 75 | container=container, 76 | parent=parent, 77 | ) 78 | return instance 79 | -------------------------------------------------------------------------------- /src/structurizr/model/deployment_element.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a superclass for deployment nodes and container instances.""" 17 | 18 | 19 | from abc import ABC 20 | from typing import Optional 21 | 22 | from .element import Element, ElementIO 23 | 24 | 25 | __all__ = ("DeploymentElement", "DeploymentElementIO") 26 | 27 | 28 | DEFAULT_DEPLOYMENT_ENVIRONMENT = "Default" 29 | 30 | 31 | class DeploymentElementIO(ElementIO, ABC): 32 | """ 33 | Define a superclass for all deployment elements. 34 | 35 | Attributes: 36 | environment (str): 37 | 38 | """ 39 | 40 | environment: Optional[str] = DEFAULT_DEPLOYMENT_ENVIRONMENT 41 | 42 | 43 | class DeploymentElement(Element, ABC): 44 | """ 45 | Define a superclass for all deployment elements. 46 | 47 | Attributes: 48 | environment (str): 49 | 50 | """ 51 | 52 | def __init__( 53 | self, *, environment: str = DEFAULT_DEPLOYMENT_ENVIRONMENT, **kwargs 54 | ) -> None: 55 | """Initialize a deployment element.""" 56 | super().__init__(**kwargs) 57 | self.environment = environment 58 | 59 | @classmethod 60 | def hydrate_arguments(cls, deployment_element_io: DeploymentElementIO) -> dict: 61 | """Build constructor arguments from IO.""" 62 | return { 63 | **super().hydrate_arguments(deployment_element_io), 64 | "environment": deployment_element_io.environment, 65 | } 66 | -------------------------------------------------------------------------------- /src/structurizr/model/enterprise.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide an enterprise model.""" 17 | 18 | 19 | from pydantic import Field 20 | 21 | from ..abstract_base import AbstractBase 22 | from ..base_model import BaseModel 23 | 24 | 25 | __all__ = ("Enterprise", "EnterpriseIO") 26 | 27 | 28 | class EnterpriseIO(BaseModel): 29 | """ 30 | Represent an enterprise. 31 | 32 | Attributes: 33 | name (str): The name of the enterprise. 34 | 35 | """ 36 | 37 | name: str = Field(..., description="The name of the enterprise.") 38 | 39 | 40 | class Enterprise(AbstractBase): 41 | """ 42 | Represent an enterprise. 43 | 44 | Attributes: 45 | name (str): The name of the enterprise. 46 | 47 | """ 48 | 49 | def __init__(self, *, name: str, **kwargs) -> None: 50 | """Initialize an enterprise by name.""" 51 | super().__init__(**kwargs) 52 | self.name = name 53 | 54 | @classmethod 55 | def hydrate(cls, enterprise_io: EnterpriseIO) -> "Enterprise": 56 | """Hydrate a new Enterprise instance from its IO.""" 57 | return cls(name=enterprise_io.name) 58 | -------------------------------------------------------------------------------- /src/structurizr/model/groupable_element.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # https://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | 14 | """Provide a superclass for all elements that can be included in a group.""" 15 | 16 | 17 | from abc import ABC 18 | from typing import Optional 19 | 20 | from .element import Element, ElementIO 21 | 22 | 23 | __all__ = ("GroupableElementIO", "GroupableElement") 24 | 25 | 26 | class GroupableElementIO(ElementIO, ABC): 27 | """ 28 | Define a superclass for all elements that can be included in a group. 29 | 30 | Attributes: 31 | group (str): The name of thegroup in which this element should be included, or 32 | None if no group. 33 | """ 34 | 35 | group: Optional[str] 36 | 37 | 38 | class GroupableElement(Element, ABC): 39 | """ 40 | Define a superclass for all elements that can be included in a group. 41 | 42 | Attributes: 43 | group (str): The name of thegroup in which this element should be included, or 44 | None if no group. 45 | """ 46 | 47 | def __init__(self, *, group: Optional[str] = None, **kwargs): 48 | """Initialise a GroupableElement.""" 49 | super().__init__(**kwargs) 50 | group = group.strip() or None if group else None 51 | self.group = group 52 | 53 | @classmethod 54 | def hydrate_arguments(cls, io: GroupableElementIO) -> dict: 55 | """Hydrate an ElementIO into the constructor arguments for Element.""" 56 | return { 57 | **super().hydrate_arguments(io), 58 | "group": io.group, 59 | } 60 | -------------------------------------------------------------------------------- /src/structurizr/model/http_health_check.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide an HTTP health check model.""" 17 | 18 | 19 | from typing import Dict, Iterable 20 | 21 | from pydantic import Field, HttpUrl 22 | 23 | from ..abstract_base import AbstractBase 24 | from ..base_model import BaseModel 25 | 26 | 27 | __all__ = ("HTTPHealthCheck", "HTTPHealthCheckIO") 28 | 29 | 30 | DEFAULT_HEALTH_CHECK_INTERVAL_IN_SECONDS = 30 31 | DEFAULT_HEALTH_CHECK_TIMEOUT_IN_MILLISECONDS = 5000 32 | 33 | 34 | class HTTPHealthCheckIO(BaseModel): 35 | """ 36 | Describe an HTTP-based health check. 37 | 38 | Attributes: 39 | name: The name of the health check. 40 | url: The health check URL/endpoint. 41 | interval: The polling interval, in seconds. 42 | timeout: The timeout after which a health check is deemed as failed, 43 | in milliseconds. 44 | headers: A set of name-value pairs corresponding to HTTP headers that 45 | should be sent with the request. 46 | 47 | """ 48 | 49 | name: str = "" 50 | url: HttpUrl = "" 51 | interval: int = DEFAULT_HEALTH_CHECK_INTERVAL_IN_SECONDS 52 | timeout: int = DEFAULT_HEALTH_CHECK_TIMEOUT_IN_MILLISECONDS 53 | headers: Dict[str, str] = Field({}) 54 | 55 | 56 | class HTTPHealthCheck(AbstractBase): 57 | """ 58 | Describe an HTTP-based health check. 59 | 60 | Attributes: 61 | name: The name of the health check. 62 | url: The health check URL/endpoint. 63 | interval: The polling interval, in seconds. 64 | timeout: The timeout after which a health check is deemed as failed, 65 | in milliseconds. 66 | headers: A set of name-value pairs corresponding to HTTP headers that 67 | should be sent with the request. 68 | 69 | """ 70 | 71 | def __init__( 72 | self, 73 | *, 74 | name: str = "", 75 | url: str = "", 76 | interval: int = DEFAULT_HEALTH_CHECK_INTERVAL_IN_SECONDS, 77 | timeout: int = DEFAULT_HEALTH_CHECK_TIMEOUT_IN_MILLISECONDS, 78 | headers: Iterable = (), 79 | **kwargs, 80 | ): 81 | """Initialize an HTTP health check.""" 82 | super().__init__(**kwargs) 83 | self.name = name 84 | self.url = url 85 | self.interval = interval 86 | self.timeout = timeout 87 | self.headers = dict(headers) 88 | 89 | @classmethod 90 | def hydrate( 91 | cls, 92 | io: HTTPHealthCheckIO, 93 | ) -> "HTTPHealthCheck": 94 | """Hydrate a new HTTPHealthCheck instance from its IO.""" 95 | return cls( 96 | name=io.name, 97 | url=io.url, 98 | interval=io.interval, 99 | timeout=io.timeout, 100 | headers=io.headers, 101 | ) 102 | -------------------------------------------------------------------------------- /src/structurizr/model/infrastructure_node.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # https://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | 14 | """Provide an infrastructure node model.""" 15 | 16 | from typing import TYPE_CHECKING, Optional 17 | 18 | from ..mixin.childless_mixin import ChildlessMixin 19 | from .deployment_element import DeploymentElement, DeploymentElementIO 20 | from .tags import Tags 21 | 22 | 23 | if TYPE_CHECKING: # pragma: no cover 24 | from .deployment_node import DeploymentNode 25 | 26 | 27 | __all__ = ("InfrastructureNode", "InfrastructureNodeIO") 28 | 29 | 30 | class InfrastructureNodeIO(DeploymentElementIO): 31 | """ 32 | Represent an infrastructure node. 33 | 34 | An infrastructure node is something like: 35 | * Load balancer 36 | * Firewall 37 | * DNS service 38 | * etc 39 | """ 40 | 41 | technology: Optional[str] = "" 42 | 43 | 44 | class InfrastructureNode(ChildlessMixin, DeploymentElement): 45 | """ 46 | Represent an infrastructure node. 47 | 48 | An infrastructure node is something like: 49 | * Load balancer 50 | * Firewall 51 | * DNS service 52 | * etc 53 | """ 54 | 55 | def __init__( 56 | self, 57 | *, 58 | technology: str = "", 59 | parent: "DeploymentNode" = None, 60 | **kwargs, 61 | ): 62 | """Initialize an infrastructure node model.""" 63 | super().__init__(**kwargs) 64 | self.technology = technology 65 | self.tags.add(Tags.INFRASTRUCTURE_NODE) 66 | self.parent = parent 67 | 68 | @classmethod 69 | def hydrate( 70 | cls, 71 | node_io: InfrastructureNodeIO, 72 | parent: "DeploymentNode", 73 | ) -> "InfrastructureNode": 74 | """Hydrate a new InfrastructureNode instance from its IO.""" 75 | node = cls( 76 | **cls.hydrate_arguments(node_io), 77 | technology=node_io.technology, 78 | parent=parent, 79 | ) 80 | return node 81 | -------------------------------------------------------------------------------- /src/structurizr/model/interaction_style.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a representation of a relationship's interaction style.""" 17 | 18 | 19 | from enum import Enum, unique 20 | 21 | 22 | __all__ = ("InteractionStyle",) 23 | 24 | 25 | @unique 26 | class InteractionStyle(Enum): 27 | """ 28 | Represents the kind of interaction between two elements. 29 | 30 | Use styles on relationships to make the difference between synchronous and 31 | asynchronous communication visible. Use styles and pass either SYNCHRONOUS or 32 | ASYNCHRONOUS tags to define different styles for synchronous and asynchronous 33 | communication. 34 | 35 | See Also: 36 | Relationship 37 | views.Styles 38 | Tags 39 | 40 | """ 41 | 42 | Synchronous = "Synchronous" 43 | Asynchronous = "Asynchronous" 44 | -------------------------------------------------------------------------------- /src/structurizr/model/location.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a representation of an element's location.""" 17 | 18 | 19 | from enum import Enum, unique 20 | 21 | 22 | __all__ = ("Location",) 23 | 24 | 25 | @unique 26 | class Location(Enum): 27 | """ 28 | Represents the location of an element with regard to a specific viewpoint. 29 | 30 | For example, "our customers are external to our enterprise". 31 | 32 | """ 33 | 34 | External = "External" 35 | Internal = "Internal" 36 | Unspecified = "Unspecified" 37 | -------------------------------------------------------------------------------- /src/structurizr/model/model_item.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide the base class for elements and relationships.""" 17 | 18 | 19 | from abc import ABC 20 | from typing import Dict, Iterable, List, Union 21 | 22 | from ordered_set import OrderedSet 23 | from pydantic import Field, validator 24 | 25 | from ..abstract_base import AbstractBase 26 | from ..base_model import BaseModel 27 | from .perspective import Perspective, PerspectiveIO 28 | 29 | 30 | __all__ = ("ModelItemIO", "ModelItem") 31 | 32 | 33 | class ModelItemIO(BaseModel, ABC): 34 | """ 35 | Define a base class for elements and relationships. 36 | 37 | Attributes: 38 | id (str): 39 | tags (set of str): 40 | properties (dict): 41 | perspectives (set of Perspective): 42 | 43 | """ 44 | 45 | id: str = Field(default="") 46 | tags: List[str] = Field(default=()) 47 | properties: Dict[str, str] = Field(default={}) 48 | perspectives: List[PerspectiveIO] = Field(default=()) 49 | 50 | @validator("tags", pre=True) 51 | def split_tags(cls, tags: Union[str, Iterable[str]]) -> List[str]: 52 | """Convert comma-separated tag list into list if needed.""" 53 | if isinstance(tags, str): 54 | return tags.split(",") 55 | return list(tags) 56 | 57 | def dict(self, **kwargs) -> dict: 58 | """Map this IO into a dictionary suitable for serialisation.""" 59 | obj = super().dict(**kwargs) 60 | if "tags" in obj: 61 | obj["tags"] = ",".join(obj["tags"]) 62 | return obj 63 | 64 | 65 | class ModelItem(AbstractBase, ABC): 66 | """ 67 | Define a base class for elements and relationships. 68 | 69 | Attributes: 70 | id (str): 71 | tags (set of str): 72 | properties (dict): 73 | perspectives (set of Perspective): 74 | 75 | """ 76 | 77 | def __init__( 78 | self, 79 | *, 80 | id: str = "", 81 | tags: Iterable[str] = (), 82 | properties: Dict[str, str] = (), 83 | perspectives: Iterable[Perspective] = (), 84 | **kwargs, 85 | ): 86 | """Initialise a ModelItem instance.""" 87 | super().__init__(**kwargs) 88 | self.id = id 89 | self.tags = OrderedSet(tags) 90 | self.properties = dict(properties) 91 | self.perspectives = set(perspectives) 92 | 93 | def __repr__(self) -> str: 94 | """Return repr(self).""" 95 | return f"{type(self).__name__}(id={self.id})" 96 | 97 | @classmethod 98 | def hydrate_arguments(cls, model_item_io: ModelItemIO) -> dict: 99 | """Hydrate an ModelItemIO into the constructor arguments for ModelItem.""" 100 | return { 101 | "id": model_item_io.id, 102 | "tags": model_item_io.tags, 103 | "properties": model_item_io.properties, # TODO: implement 104 | "perspectives": map(Perspective.hydrate, model_item_io.perspectives), 105 | } 106 | -------------------------------------------------------------------------------- /src/structurizr/model/person.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a person model.""" 17 | 18 | 19 | from typing import Optional 20 | 21 | from pydantic import Field 22 | 23 | from ..mixin.childless_mixin import ChildlessMixin 24 | from .location import Location 25 | from .relationship import Relationship 26 | from .static_structure_element import StaticStructureElement, StaticStructureElementIO 27 | from .tags import Tags 28 | 29 | 30 | __all__ = ("PersonIO", "Person") 31 | 32 | 33 | class PersonIO(StaticStructureElementIO): 34 | """ 35 | Represent a person in the C4 model. 36 | 37 | Attributes: 38 | location (Location): The location of this person. 39 | 40 | """ 41 | 42 | location: Location = Field( 43 | default=Location.Unspecified, description="The location of this person." 44 | ) 45 | 46 | 47 | class Person(ChildlessMixin, StaticStructureElement): 48 | """ 49 | Represent a person in the C4 model. 50 | 51 | Attributes: 52 | location (Location): The location of this person. 53 | 54 | """ 55 | 56 | def __init__(self, *, location: Location = Location.Unspecified, **kwargs) -> None: 57 | """Initialise a Person.""" 58 | super().__init__(**kwargs) 59 | self.location = location 60 | 61 | self.tags.add(Tags.PERSON) 62 | 63 | @classmethod 64 | def hydrate(cls, person_io: PersonIO) -> "Person": 65 | """Create a new person and hydrate from its IO.""" 66 | person = cls( 67 | **cls.hydrate_arguments(person_io), 68 | location=person_io.location, 69 | ) 70 | return person 71 | 72 | def interacts_with( 73 | self, destination: "Person", description: str, **kwargs 74 | ) -> Optional[Relationship]: 75 | """Create a relationship with the given other Person.""" 76 | return self.uses(destination=destination, description=description, **kwargs) 77 | -------------------------------------------------------------------------------- /src/structurizr/model/perspective.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide an architectural perspective model.""" 17 | 18 | 19 | from pydantic import Field 20 | 21 | from ..abstract_base import AbstractBase 22 | from ..base_model import BaseModel 23 | 24 | 25 | __all__ = ("Perspective", "PerspectiveIO") 26 | 27 | 28 | class PerspectiveIO(BaseModel): 29 | """ 30 | Represent an architectural perspective. 31 | 32 | Architectural perspectives can be applied to elements and relationships. 33 | 34 | Notes: 35 | See https://www.viewpoints-and-perspectives.info/home/perspectives/ for more 36 | details of this concept. 37 | 38 | Attributes: 39 | name (str): The name of the perspective, e.g., 'Security'. 40 | description (str): A longer description of the architectural perspective. 41 | 42 | """ 43 | 44 | name: str = Field(..., description="The name of the perspective, e.g., 'Security'.") 45 | description: str = Field( 46 | ..., description="A longer description of the architectural perspective." 47 | ) 48 | 49 | 50 | class Perspective(AbstractBase): 51 | """ 52 | Represent an architectural perspective. 53 | 54 | Architectural perspectives can be applied to elements and relationships. 55 | 56 | Notes: 57 | See https://www.viewpoints-and-perspectives.info/home/perspectives/ for more 58 | details of this concept. 59 | 60 | Attributes: 61 | name (str): The name of the perspective, e.g., 'Security'. 62 | description (str): A longer description of the architectural perspective. 63 | 64 | """ 65 | 66 | def __init__(self, *, name: str, description: str, **kwargs) -> None: 67 | """Initialize an architectural perspective.""" 68 | super().__init__(**kwargs) 69 | self.name = name 70 | self.description = description 71 | 72 | @classmethod 73 | def hydrate(cls, perspective_io: PerspectiveIO) -> "Perspective": 74 | """Hydrate a new Perspective instance from its IO.""" 75 | return cls(name=perspective_io.name, description=perspective_io.description) 76 | -------------------------------------------------------------------------------- /src/structurizr/model/sequential_integer_id_generator.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a sequential integer ID generator.""" 17 | 18 | 19 | __all__ = ("SequentialIntegerIDGenerator",) 20 | 21 | 22 | class SequentialIntegerIDGenerator: 23 | """Define a sequential integer ID generator.""" 24 | 25 | def __init__(self, **kwargs) -> None: 26 | """Initialize a new generator.""" 27 | super().__init__(**kwargs) 28 | self._counter = 0 29 | 30 | def generate_id(self, **kwargs) -> str: 31 | """ 32 | Generate a new sequential integer ID. 33 | 34 | Returns: 35 | str: The generated ID as a string. 36 | 37 | """ 38 | self._counter += 1 39 | return str(self._counter) 40 | 41 | def found(self, id: str) -> None: 42 | """ 43 | Update the generator with an existing ID. 44 | 45 | The ID is used to update the internal counter, if it can be converted to an 46 | integer. 47 | 48 | Args: 49 | id (str): The externally created ID. 50 | 51 | """ 52 | try: 53 | id_as_int = int(id) 54 | except ValueError: 55 | pass 56 | else: 57 | if id_as_int > self._counter: 58 | self._counter = id_as_int 59 | -------------------------------------------------------------------------------- /src/structurizr/model/software_system_instance.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a softwrae system instance model.""" 17 | 18 | 19 | from typing import TYPE_CHECKING 20 | 21 | from pydantic import Field 22 | 23 | from .software_system import SoftwareSystem 24 | from .static_structure_element_instance import ( 25 | StaticStructureElementInstance, 26 | StaticStructureElementInstanceIO, 27 | ) 28 | from .tags import Tags 29 | 30 | 31 | if TYPE_CHECKING: # pragma: no cover 32 | from .deployment_node import DeploymentNode 33 | 34 | 35 | __all__ = ("SoftwareSystemInstance", "SoftwareSystemInstanceIO") 36 | 37 | 38 | class SoftwareSystemInstanceIO(StaticStructureElementInstanceIO): 39 | """Represents a software system instance which can be added to a deployment node.""" 40 | 41 | software_system_id: str = Field(alias="softwareSystemId") 42 | 43 | 44 | class SoftwareSystemInstance(StaticStructureElementInstance): 45 | """Represents a software system instance which can be added to a deployment node.""" 46 | 47 | def __init__(self, *, software_system: SoftwareSystem, **kwargs) -> None: 48 | """Initialize a software system instance.""" 49 | super().__init__(element=software_system, **kwargs) 50 | self.tags.add(Tags.SOFTWARE_SYSTEM_INSTANCE) 51 | 52 | @property 53 | def software_system(self) -> SoftwareSystem: 54 | """Return the software system for this instance.""" 55 | return self.element 56 | 57 | @property 58 | def software_system_id(self) -> str: 59 | """Return the ID of the software system for this instance.""" 60 | return self.software_system.id 61 | 62 | @classmethod 63 | def hydrate( 64 | cls, 65 | system_instance_io: SoftwareSystemInstanceIO, 66 | system: SoftwareSystem, 67 | parent: "DeploymentNode", 68 | ) -> "SoftwareSystemInstance": 69 | """Hydrate a new SoftwareSystemInstance instance from its IO.""" 70 | instance = cls( 71 | **cls.hydrate_arguments(system_instance_io), 72 | software_system=system, 73 | parent=parent, 74 | ) 75 | return instance 76 | -------------------------------------------------------------------------------- /src/structurizr/model/static_structure_element.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a superclass for all static structure model elements.""" 17 | 18 | 19 | from abc import ABC 20 | from typing import TYPE_CHECKING, Optional 21 | 22 | from .element import Element 23 | from .groupable_element import GroupableElement, GroupableElementIO 24 | 25 | 26 | if TYPE_CHECKING: # pragma: no cover 27 | from .relationship import Relationship 28 | 29 | 30 | __all__ = ("StaticStructureElementIO", "StaticStructureElement") 31 | 32 | 33 | class StaticStructureElementIO(GroupableElementIO, ABC): 34 | """ 35 | Define a superclass for all static structure model elements. 36 | 37 | This is the superclass for model elements that describe the static structure 38 | of a software system, namely Person, SoftwareSystem, Container and Component. 39 | 40 | """ 41 | 42 | pass 43 | 44 | 45 | class StaticStructureElement(GroupableElement, ABC): 46 | """ 47 | Define a superclass for all static structure model elements. 48 | 49 | This is the superclass for model elements that describe the static structure 50 | of a software system, namely Person, SoftwareSystem, Container and Component. 51 | 52 | """ 53 | 54 | def uses( 55 | self, 56 | destination: Element, 57 | description: str = "Uses", 58 | technology: str = "", 59 | **kwargs, 60 | ) -> Optional["Relationship"]: 61 | """Add a unidirectional "uses" style relationship to another element.""" 62 | return self.get_model().add_relationship( 63 | source=self, 64 | destination=destination, 65 | description=description, 66 | technology=technology, 67 | **kwargs, 68 | ) 69 | 70 | def delivers( 71 | self, 72 | destination: Element, 73 | description: str, 74 | technology: str = "", 75 | **kwargs, 76 | ) -> Optional["Relationship"]: 77 | """Add a unidirectional relationship to another element.""" 78 | return self.uses( 79 | destination=destination, 80 | description=description, 81 | technology=technology, 82 | **kwargs, 83 | ) 84 | -------------------------------------------------------------------------------- /src/structurizr/model/tags.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide required tags.""" 17 | 18 | 19 | from ..abstract_base import AbstractBase 20 | 21 | 22 | __all__ = ("Tags",) 23 | 24 | 25 | class Tags(AbstractBase): 26 | """Define required tags.""" 27 | 28 | ELEMENT = "Element" 29 | RELATIONSHIP = "Relationship" 30 | 31 | PERSON = "Person" 32 | SOFTWARE_SYSTEM = "Software System" 33 | CONTAINER = "Container" 34 | COMPONENT = "Component" 35 | 36 | DEPLOYMENT_NODE = "Deployment Node" 37 | CONTAINER_INSTANCE = "Container Instance" 38 | SOFTWARE_SYSTEM_INSTANCE = "Software System Instance" 39 | INFRASTRUCTURE_NODE = "Infrastructure Node" 40 | 41 | SYNCHRONOUS = "Synchronous" 42 | ASYNCHRONOUS = "Asynchronous" 43 | -------------------------------------------------------------------------------- /src/structurizr/view/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide different views onto a Structurizr software architecture.""" 17 | 18 | from .border import Border 19 | from .shape import Shape 20 | from .orientation import Orientation 21 | from .paper_size import PaperSize 22 | from .rank_direction import RankDirection 23 | from .automatic_layout import AutomaticLayout, AutomaticLayoutIO 24 | from .animation import Animation, AnimationIO 25 | from .system_context_view import SystemContextView, SystemContextViewIO 26 | from .view_set import ViewSet, ViewSetIO 27 | from .element_style import ElementStyle, ElementStyleIO 28 | from .relationship_style import RelationshipStyle, RelationshipStyleIO 29 | from .configuration import Configuration, ConfigurationIO 30 | from .element_style import ElementStyle, ElementStyleIO 31 | from .vertex import Vertex, VertexIO 32 | -------------------------------------------------------------------------------- /src/structurizr/view/abstract_view.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # https://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | 14 | """Provide a superclass for all views.""" 15 | 16 | 17 | from abc import ABC 18 | from typing import Dict 19 | 20 | from ..abstract_base import AbstractBase 21 | from ..base_model import BaseModel 22 | from ..mixin import ViewSetRefMixin 23 | 24 | 25 | __all__ = ("AbstractView", "AbstractViewIO") 26 | 27 | 28 | class AbstractViewIO(BaseModel, ABC): 29 | """ 30 | Define an abstract base class for all views. 31 | 32 | Views include static views, dynamic views, deployment views and filtered views. 33 | """ 34 | 35 | key: str 36 | description: str = "" 37 | title: str = "" 38 | 39 | 40 | class AbstractView(ViewSetRefMixin, AbstractBase, ABC): 41 | """ 42 | Define an abstract base class for all views. 43 | 44 | Views include static views, dynamic views, deployment views and filtered views. 45 | 46 | """ 47 | 48 | def __init__( 49 | self, 50 | *, 51 | key: str = None, 52 | description: str, 53 | title: str = "", 54 | **kwargs, 55 | ): 56 | """Initialize a view with a 'private' view set.""" 57 | super().__init__(**kwargs) 58 | self.key = key 59 | self.description = description 60 | self.title = title 61 | 62 | def __repr__(self) -> str: 63 | """Return repr(self).""" 64 | return f"{type(self).__name__}(key={self.key})" 65 | 66 | @classmethod 67 | def hydrate_arguments(cls, view_io: AbstractViewIO) -> Dict: 68 | """Hydrate an AbstractViewIO into the constructor args for AbstractView.""" 69 | return { 70 | "key": view_io.key, 71 | "description": view_io.description, 72 | "title": view_io.title, 73 | } 74 | -------------------------------------------------------------------------------- /src/structurizr/view/animation.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a wrapper for a collection of animation steps.""" 17 | 18 | 19 | from typing import Iterable, List 20 | 21 | from ..abstract_base import AbstractBase 22 | from ..base_model import BaseModel 23 | 24 | 25 | __all__ = ("Animation", "AnimationIO") 26 | 27 | 28 | class AnimationIO(BaseModel): 29 | """ 30 | Define a wrapper for a collection of animation steps. 31 | 32 | Attributes: 33 | order: 34 | elements: 35 | relationships: 36 | 37 | """ 38 | 39 | order: int 40 | elements: List[str] = [] 41 | relationships: List[str] = [] 42 | 43 | 44 | class Animation(AbstractBase): 45 | """ 46 | Define a wrapper for a collection of animation steps. 47 | 48 | Attributes: 49 | order: the order in which this animation step appears 50 | elements: the IDs of the elements to show in this step 51 | relationships: ths IDs of the relationships to show in this step 52 | 53 | """ 54 | 55 | def __init__( 56 | self, 57 | *, 58 | order: int, 59 | elements: Iterable[str] = (), 60 | relationships: Iterable[str] = (), 61 | **kwargs 62 | ): 63 | """Initialize an animation.""" 64 | super().__init__(**kwargs) 65 | self.order = order 66 | self.elements = set(elements) 67 | self.relationships = set(relationships) 68 | 69 | @classmethod 70 | def hydrate(cls, animation_io: AnimationIO) -> "Animation": 71 | """Hydrate a new Animation instance from its IO.""" 72 | return cls( 73 | order=animation_io.order, 74 | elements=animation_io.elements, 75 | relationships=animation_io.relationships, 76 | ) 77 | -------------------------------------------------------------------------------- /src/structurizr/view/automatic_layout.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide an automatic layout configuration.""" 17 | 18 | 19 | from pydantic import Field 20 | 21 | from ..abstract_base import AbstractBase 22 | from ..base_model import BaseModel 23 | from .rank_direction import RankDirection 24 | 25 | 26 | __all__ = ("AutomaticLayout", "AutomaticLayoutIO") 27 | 28 | 29 | class AutomaticLayoutIO(BaseModel): 30 | """Define a wrapper for automatic layout configuration.""" 31 | 32 | rank_direction: RankDirection = Field(..., alias="rankDirection") 33 | rank_separation: int = Field(..., alias="rankSeparation") 34 | node_separation: int = Field(..., alias="nodeSeparation") 35 | edge_separation: int = Field(..., alias="edgeSeparation") 36 | vertices: bool 37 | 38 | 39 | class AutomaticLayout(AbstractBase): 40 | """Define a wrapper for automatic layout configuration.""" 41 | 42 | def __init__( 43 | self, 44 | *, 45 | rank_direction: RankDirection, 46 | rank_separation: int, 47 | node_separation: int, 48 | edge_separation: int, 49 | vertices: bool, 50 | **kwargs 51 | ) -> None: 52 | """Initialize an automatic layout.""" 53 | super().__init__(**kwargs) 54 | self.rank_direction = rank_direction 55 | self.rank_separation = rank_separation 56 | self.node_separation = node_separation 57 | self.edge_separation = edge_separation 58 | self.vertices = vertices 59 | 60 | @classmethod 61 | def hydrate(cls, automatic_layout_io: AutomaticLayoutIO) -> "AutomaticLayout": 62 | """Hydrate a new AutomaticLayout instance from its IO.""" 63 | return cls( 64 | rank_direction=automatic_layout_io.rank_direction, 65 | rank_separation=automatic_layout_io.rank_separation, 66 | node_separation=automatic_layout_io.node_separation, 67 | edge_separation=automatic_layout_io.edge_separation, 68 | vertices=automatic_layout_io.vertices, 69 | ) 70 | -------------------------------------------------------------------------------- /src/structurizr/view/border.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide different border style choices.""" 17 | 18 | 19 | from enum import Enum, unique 20 | 21 | 22 | __all__ = ("Border",) 23 | 24 | 25 | @unique 26 | class Border(Enum): 27 | """Represent a border style.""" 28 | 29 | Solid = "Solid" 30 | Dashed = "Dashed" 31 | -------------------------------------------------------------------------------- /src/structurizr/view/branding.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide an implementation of a corporate branding.""" 17 | 18 | 19 | from typing import Optional 20 | 21 | from ..abstract_base import AbstractBase 22 | from ..base_model import BaseModel 23 | from .font import Font, FontIO 24 | 25 | 26 | __all__ = ("Branding", "BrandingIO") 27 | 28 | 29 | class BrandingIO(BaseModel): 30 | """Represent an instance of a corporate branding.""" 31 | 32 | logo: Optional[str] 33 | font: Optional[FontIO] 34 | 35 | 36 | class Branding(AbstractBase): 37 | """Represent a corporate branding.""" 38 | 39 | def __init__( 40 | self, *, logo: Optional[str] = None, font: Optional[Font] = None, **kwargs 41 | ) -> None: 42 | """Initialize a corporate branding.""" 43 | super().__init__(**kwargs) 44 | self.logo = logo 45 | self.font = font 46 | -------------------------------------------------------------------------------- /src/structurizr/view/color.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber, Ilai Fallach. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a color object to validate and serialize colors.""" 17 | 18 | 19 | import pydantic.color 20 | 21 | 22 | class Color(pydantic.color.Color): 23 | """Represent a natural color.""" 24 | 25 | def as_hex(self) -> str: 26 | """Return a six character hex representation of the color.""" 27 | values = [pydantic.color.float_to_255(c) for c in self._rgba[:3]] 28 | if self._rgba.alpha is not None: 29 | values.append(pydantic.color.float_to_255(self._rgba.alpha)) 30 | 31 | return f"#{''.join(f'{v:02x}' for v in values)}" 32 | 33 | def __str__(self) -> str: 34 | """Return a hex string representation of the color.""" 35 | return self.as_hex() 36 | -------------------------------------------------------------------------------- /src/structurizr/view/configuration.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a configuration for rendering a workspace.""" 17 | 18 | 19 | from typing import Optional 20 | 21 | from pydantic import Field 22 | 23 | from ..abstract_base import AbstractBase 24 | from ..base_model import BaseModel 25 | from .branding import Branding, BrandingIO 26 | from .styles import Styles, StylesIO 27 | from .terminology import Terminology, TerminologyIO 28 | from .view_sort_order import ViewSortOrder 29 | 30 | 31 | __all__ = ("Configuration", "ConfigurationIO") 32 | 33 | 34 | class ConfigurationIO(BaseModel): 35 | """Represent a configuration instance.""" 36 | 37 | branding: Optional[BrandingIO] 38 | styles: Optional[StylesIO] 39 | theme: Optional[str] 40 | terminology: Optional[TerminologyIO] 41 | default_view: Optional[str] = Field(None, alias="defaultView") 42 | last_saved_view: Optional[str] = Field(None, alias="lastSavedView") 43 | view_sort_order: ViewSortOrder = Field( 44 | default=ViewSortOrder.Default, 45 | alias="viewSortOrder", 46 | ) 47 | 48 | 49 | class Configuration(AbstractBase): 50 | """Configure how information in a workspace is rendered.""" 51 | 52 | def __init__( 53 | self, 54 | *, 55 | branding: Optional[Branding] = None, 56 | styles: Optional[Styles] = None, 57 | theme: Optional[str] = None, 58 | terminology: Optional[Terminology] = None, 59 | default_view: Optional[str] = None, 60 | last_saved_view: Optional[str] = None, 61 | view_sort_order: ViewSortOrder = ViewSortOrder.Default, 62 | **kwargs 63 | ) -> None: 64 | """Initialize an element view.""" 65 | super().__init__(**kwargs) 66 | self.branding = Branding() if branding is None else branding 67 | self.styles = Styles() if styles is None else styles 68 | self.theme = theme 69 | self.terminology = Terminology() if terminology is None else terminology 70 | self.default_view = default_view 71 | self.last_saved_view = last_saved_view 72 | self.view_sort_order = view_sort_order 73 | 74 | @classmethod 75 | def hydrate(cls, configuration_io: ConfigurationIO) -> "Configuration": 76 | """Hydrate new Configuration instance from its IO.""" 77 | return cls( 78 | # TODO: 79 | # branding=Branding.hydrate(configuration_io.branding), 80 | styles=Styles.hydrate(configuration_io.styles), 81 | theme=configuration_io.theme, 82 | # terminology=Terminology.hydrate(configuration_io.terminology), 83 | default_view=configuration_io.default_view, 84 | last_saved_view=configuration_io.last_saved_view, 85 | view_sort_order=configuration_io.view_sort_order, 86 | ) 87 | -------------------------------------------------------------------------------- /src/structurizr/view/element_view.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a container for an element instance in a view.""" 17 | 18 | 19 | from typing import Optional 20 | 21 | from ..abstract_base import AbstractBase 22 | from ..base_model import BaseModel 23 | from ..model import Element 24 | 25 | 26 | __all__ = ("ElementView", "ElementViewIO") 27 | 28 | 29 | class ElementViewIO(BaseModel): 30 | """Represent an instance of an element in a view.""" 31 | 32 | id: Optional[str] 33 | x: Optional[int] 34 | y: Optional[int] 35 | 36 | 37 | class ElementView(AbstractBase): 38 | """Represent an instance of an element in a view.""" 39 | 40 | def __init__( 41 | self, 42 | *, 43 | element: Optional[Element] = None, 44 | x: Optional[int] = None, 45 | y: Optional[int] = None, 46 | id: str = "", 47 | **kwargs, 48 | ) -> None: 49 | """Initialize an element view.""" 50 | super().__init__(**kwargs) 51 | self.element = element 52 | self.id = element.id if element else id 53 | self.x = x 54 | self.y = y 55 | 56 | def __repr__(self) -> str: 57 | """Return repr(self).""" 58 | return f"{type(self).__name__}(id={self.id})" 59 | 60 | @classmethod 61 | def hydrate(cls, element_view_io: ElementViewIO) -> "ElementView": 62 | """Hydrate a new ElementView instance from its IO.""" 63 | return cls(id=element_view_io.id, x=element_view_io.x, y=element_view_io.y) 64 | 65 | def copy_layout_information_from(self, source: "ElementView") -> None: 66 | """Copy the layout information from another view.""" 67 | self.x = source.x 68 | self.y = source.y 69 | -------------------------------------------------------------------------------- /src/structurizr/view/font.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a font description.""" 17 | 18 | 19 | from typing import Optional 20 | 21 | from pydantic import HttpUrl 22 | 23 | from ..abstract_base import AbstractBase 24 | from ..base_model import BaseModel 25 | 26 | 27 | __all__ = ("Font", "FontIO") 28 | 29 | 30 | class FontIO(BaseModel): 31 | """Represent an instance of a font.""" 32 | 33 | name: str 34 | url: Optional[HttpUrl] = None 35 | 36 | 37 | class Font(AbstractBase): 38 | """Represent a font.""" 39 | 40 | def __init__(self, *, name: str, url: Optional[str] = None, **kwargs) -> None: 41 | """Initialize a font.""" 42 | super().__init__(**kwargs) 43 | self.name = name 44 | self.url = url 45 | -------------------------------------------------------------------------------- /src/structurizr/view/interaction_order.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # https://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | """Provide a type that supports logical sequencing of interactions.""" 14 | 15 | from typing import Any 16 | 17 | 18 | class InteractionOrder(str): 19 | """ 20 | Represent the order of interactions within a diagram. 21 | 22 | Orders are typically expressed as period-separated string, e.g. 1.2a.13, where 23 | numeric ordering is preserved as opposed to lexical - e.g. 1.13 > 1.2. 24 | """ 25 | 26 | def __new__(cls, order: Any) -> "InteractionOrder": 27 | """Initialise a new InteractionOrder instance.""" 28 | order_str = str(order) 29 | self = super(InteractionOrder, cls).__new__(cls, order_str) 30 | self._segments = order_str.split(".") 31 | return self 32 | 33 | def __lt__(self, other: "InteractionOrder") -> bool: 34 | """Return true if the this InteractionOrder is logically less than other.""" 35 | if not isinstance(other, InteractionOrder): 36 | raise TypeError 37 | 38 | for (a, b) in zip(self._segments, other._segments): 39 | if _segment_less_than(a, b): 40 | return True 41 | elif _segment_less_than(b, a): 42 | return False 43 | 44 | return len(self._segments) < len(other._segments) 45 | 46 | def __le__(self, other: "InteractionOrder") -> bool: 47 | """Return true if the this InteractionOrder is logically <= other.""" 48 | return not other < self 49 | 50 | def __gt__(self, other: "InteractionOrder") -> bool: 51 | """Return true if the this InteractionOrder is logically greater than other.""" 52 | return other < self 53 | 54 | def __ge__(self, other: "InteractionOrder") -> bool: 55 | """Return true if the this InteractionOrder is logically >= other.""" 56 | return not self < other 57 | 58 | 59 | def _segment_less_than(a: str, b: str) -> bool: 60 | """Return True if a is logically less that b.""" 61 | max_len = max(len(a), len(b)) 62 | return a.rjust(max_len) < b.rjust(max_len) 63 | -------------------------------------------------------------------------------- /src/structurizr/view/orientation.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide paper orientation choices.""" 17 | 18 | 19 | from enum import Enum, unique 20 | 21 | 22 | __all__ = ("Orientation",) 23 | 24 | 25 | @unique 26 | class Orientation(Enum): 27 | """Represent paper orientation.""" 28 | 29 | Portrait = "Portrait" 30 | Landscape = "Landscape" 31 | -------------------------------------------------------------------------------- /src/structurizr/view/paper_size.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a representation of paper size.""" 17 | 18 | 19 | from enum import Enum 20 | 21 | from .orientation import Orientation 22 | 23 | 24 | __all__ = ("PaperSize",) 25 | 26 | 27 | class PaperSize(Enum): 28 | """Represent paper sizes in pixels at 300dpi.""" 29 | 30 | def __new__( 31 | cls, 32 | value: str, 33 | size: str, 34 | orientation: Orientation, 35 | width: int, 36 | height: int, 37 | **kwargs 38 | ) -> "PaperSize": 39 | """ 40 | Construct a specific paper size but with a string value. 41 | 42 | References: 43 | https://docs.python.org/3/library/enum.html#when-to-use-new-vs-init 44 | 45 | """ 46 | obj = object.__new__(cls) 47 | obj._value_ = value 48 | obj.size = size 49 | obj.orientation = orientation 50 | obj.width = width 51 | obj.height = height 52 | 53 | return obj 54 | 55 | A6_Portrait = ("A6_Portrait", "A6", Orientation.Portrait, 1240, 1748) 56 | A6_Landscape = ("A6_Landscape", "A6", Orientation.Landscape, 1748, 1240) 57 | 58 | A5_Portrait = ("A5_Portrait", "A5", Orientation.Portrait, 1748, 2480) 59 | A5_Landscape = ("A5_Landscape", "A5", Orientation.Landscape, 2480, 1748) 60 | 61 | A4_Portrait = ("A4_Portrait", "A4", Orientation.Portrait, 2480, 3508) 62 | A4_Landscape = ("A4_Landscape", "A4", Orientation.Landscape, 3508, 2480) 63 | 64 | A3_Portrait = ("A3_Portrait", "A3", Orientation.Portrait, 3508, 4961) 65 | A3_Landscape = ("A3_Landscape", "A3", Orientation.Landscape, 4961, 3508) 66 | 67 | A2_Portrait = ("A2_Portrait", "A2", Orientation.Portrait, 4961, 7016) 68 | A2_Landscape = ("A2_Landscape", "A2", Orientation.Landscape, 7016, 4961) 69 | 70 | A1_Portrait = ("A1_Portrait", "A1", Orientation.Portrait, 7016, 9933) 71 | A1_Landscape = ("A1_Landscape", "A1", Orientation.Landscape, 9933, 7016) 72 | 73 | A0_Portrait = ("A0_Portrait", "A0", Orientation.Portrait, 9933, 14043) 74 | A0_Landscape = ("A0_Landscape", "A0", Orientation.Landscape, 14043, 9933) 75 | 76 | Letter_Portrait = ("Letter_Portrait", "Letter", Orientation.Portrait, 2550, 3300) 77 | Letter_Landscape = ("Letter_Landscape", "Letter", Orientation.Landscape, 3300, 2550) 78 | 79 | Legal_Portrait = ("Legal_Portrait", "Legal", Orientation.Portrait, 2550, 4200) 80 | Legal_Landscape = ("Legal_Landscape", "Legal", Orientation.Landscape, 4200, 2550) 81 | 82 | Slide_4_3 = ("Slide_4_3", "Slide 4:3", Orientation.Landscape, 3306, 2480) 83 | Slide_16_9 = ("Slide_16_9", "Slide 16:9", Orientation.Landscape, 3508, 1973) 84 | Slide_16_10 = ("Slide_16_10", "Slide 16:10", Orientation.Landscape, 3508, 2193) 85 | -------------------------------------------------------------------------------- /src/structurizr/view/rank_direction.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a choices for node rank direction.""" 17 | 18 | 19 | from enum import Enum, unique 20 | 21 | 22 | __all__ = ("RankDirection",) 23 | 24 | 25 | @unique 26 | class RankDirection(Enum): 27 | """Represent node rank direction.""" 28 | 29 | TopBottom = "TopBottom" 30 | BottomTop = "BottomTop" 31 | LeftRight = "LeftRight" 32 | RightLeft = "RightLeft" 33 | -------------------------------------------------------------------------------- /src/structurizr/view/relationship_style.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a way to style a relationship.""" 17 | 18 | 19 | from typing import Optional 20 | 21 | from pydantic import Field 22 | 23 | from ..abstract_base import AbstractBase 24 | from ..base_model import BaseModel 25 | from .color import Color 26 | from .routing import Routing 27 | 28 | 29 | __all__ = ("RelationshipStyle", "RelationshipStyleIO") 30 | 31 | 32 | class RelationshipStyleIO(BaseModel): 33 | """Represent a relationship's style.""" 34 | 35 | tag: str 36 | thickness: Optional[int] 37 | width: Optional[int] 38 | color: Optional[Color] 39 | font_size: Optional[int] = Field(default=None, alias="fontSize") 40 | dashed: Optional[bool] 41 | routing: Optional[Routing] 42 | position: Optional[int] 43 | opacity: Optional[int] 44 | 45 | 46 | class RelationshipStyle(AbstractBase): 47 | """Define an relationship's style.""" 48 | 49 | START_OF_LINE = 0 50 | END_OF_LINE = 100 51 | 52 | def __init__( 53 | self, 54 | *, 55 | tag: str, 56 | thickness: Optional[int] = None, 57 | width: Optional[int] = None, 58 | color: Optional[str] = None, 59 | font_size: Optional[int] = None, 60 | dashed: Optional[bool] = None, 61 | routing: Optional[Routing] = None, 62 | position: Optional[int] = None, 63 | opacity: Optional[int] = None, 64 | **kwargs 65 | ) -> None: 66 | """Initialize a relationship style.""" 67 | super().__init__(**kwargs) 68 | self.tag = tag 69 | self.thickness = thickness 70 | self.color = color 71 | self.font_size = font_size 72 | self.width = width 73 | self.dashed = dashed 74 | self.routing = routing 75 | self.position = position 76 | self.opacity = opacity 77 | 78 | @classmethod 79 | def hydrate(cls, relationship_style_io: RelationshipStyleIO) -> "RelationshipStyle": 80 | """Hydrate a new RelationshipStyle instance from its IO.""" 81 | return cls( 82 | tag=relationship_style_io.tag, 83 | thickness=relationship_style_io.thickness, 84 | color=relationship_style_io.color, 85 | font_size=relationship_style_io.font_size, 86 | width=relationship_style_io.width, 87 | dashed=relationship_style_io.dashed, 88 | routing=relationship_style_io.routing, 89 | position=relationship_style_io.position, 90 | opacity=relationship_style_io.opacity, 91 | ) 92 | -------------------------------------------------------------------------------- /src/structurizr/view/routing.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide different relationship link routing choices.""" 17 | 18 | 19 | from enum import Enum, unique 20 | 21 | 22 | __all__ = ("Routing",) 23 | 24 | 25 | @unique 26 | class Routing(Enum): 27 | """Represent a relationship link's routing.""" 28 | 29 | Direct = "Direct" 30 | Orthogonal = "Orthogonal" 31 | -------------------------------------------------------------------------------- /src/structurizr/view/sequence_counter.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # https://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | """Provide an incrementing sequence counter.""" 14 | 15 | 16 | class SequenceCounter: 17 | """Provides an incrementing counter.""" 18 | 19 | def __init__(self, parent: "SequenceCounter" = None): 20 | """Initialise a new SequenceCounter.""" 21 | self.sequence = 0 22 | self.parent = parent 23 | 24 | def increment(self): 25 | """Advance the counter.""" 26 | self.sequence += 1 27 | 28 | def __str__(self): 29 | """Provide a string representation of the counter.""" 30 | return str(self.sequence) 31 | 32 | 33 | class ParallelSequenceCounter(SequenceCounter): 34 | """Provide support for parallel sequences.""" 35 | 36 | def __init__(self, parent: SequenceCounter): 37 | """Initialise a new ParallelSequenceCounter instance.""" 38 | super().__init__(parent) 39 | self.sequence = parent.sequence 40 | 41 | 42 | class SubsequenceCounter(SequenceCounter): 43 | """Provide support for subsequences.""" 44 | 45 | def __init__(self, parent: SequenceCounter): 46 | """Initialise a new SubsequenceCounter instance.""" 47 | super().__init__(parent) 48 | 49 | def __str__(self): 50 | """Provide a string representation of the counter.""" 51 | return f"{self.parent}.{self.sequence}" 52 | -------------------------------------------------------------------------------- /src/structurizr/view/sequence_number.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # https://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | """Provide a sequence number, used in Dynamic views.""" 14 | 15 | 16 | from .interaction_order import InteractionOrder 17 | from .sequence_counter import ( 18 | ParallelSequenceCounter, 19 | SequenceCounter, 20 | SubsequenceCounter, 21 | ) 22 | 23 | 24 | class SequenceNumber: 25 | """A class to provide interaction sequence numbers.""" 26 | 27 | def __init__(self): 28 | """Initialise a new SequenceNumber instance.""" 29 | self.counter = SequenceCounter() 30 | 31 | def get_next(self) -> InteractionOrder: 32 | """Return the next number in the sequence.""" 33 | self.counter.increment() 34 | return InteractionOrder(self.counter) 35 | 36 | def start_subsequence(self): 37 | """Start a subsequence. 38 | 39 | See `DynamicView.subvsequence()` for an explanation. 40 | """ 41 | self.counter = SubsequenceCounter(self.counter) 42 | 43 | def end_subsequence(self): 44 | """End an active subsequence.""" 45 | self.counter = self.counter.parent 46 | 47 | def start_parallel_sequence(self): 48 | """Begin a parallel sequence. 49 | 50 | See `DynamicView.parallel_sequence()` for an explanation. 51 | """ 52 | self.counter = ParallelSequenceCounter(self.counter) 53 | 54 | def end_parallel_sequence(self, continue_numbering: bool): 55 | """End a parallel sequence. 56 | 57 | Args: 58 | continue_numbering: if True then the main sequence will continue from 59 | the next number after this parallel sequence, else 60 | it will reset back to before this parallel sequence 61 | began. 62 | 63 | See `DynamicView.parallel_sequence()` for an explanation. 64 | """ 65 | if continue_numbering: 66 | sequence = self.counter.sequence 67 | self.counter = self.counter.parent 68 | self.counter.sequence = sequence 69 | else: 70 | self.counter = self.counter.parent 71 | -------------------------------------------------------------------------------- /src/structurizr/view/shape.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide different element shapes.""" 17 | 18 | 19 | from enum import Enum, unique 20 | 21 | 22 | __all__ = ("Shape",) 23 | 24 | 25 | @unique 26 | class Shape(Enum): 27 | """Represent an element shape.""" 28 | 29 | Box = "Box" 30 | RoundedBox = "RoundedBox" 31 | Circle = "Circle" 32 | Ellipse = "Ellipse" 33 | Hexagon = "Hexagon" 34 | Cylinder = "Cylinder" 35 | Pipe = "Pipe" 36 | Person = "Person" 37 | Robot = "Robot" 38 | Folder = "Folder" 39 | WebBrowser = "WebBrowser" 40 | MobileDevicePortrait = "MobileDevicePortrait" 41 | MobileDeviceLandscape = "MobileDeviceLandscape" 42 | -------------------------------------------------------------------------------- /src/structurizr/view/styles.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a collection of styles.""" 17 | 18 | 19 | from typing import Iterable, List, Union 20 | 21 | from pydantic import Field 22 | 23 | from ..abstract_base import AbstractBase 24 | from ..base_model import BaseModel 25 | from .element_style import ElementStyle, ElementStyleIO 26 | from .relationship_style import RelationshipStyle, RelationshipStyleIO 27 | 28 | 29 | __all__ = ("Styles", "StylesIO") 30 | 31 | 32 | class StylesIO(BaseModel): 33 | """Represent a collection of styles.""" 34 | 35 | elements: List[ElementStyleIO] = Field(default=()) 36 | relationships: List[RelationshipStyleIO] = Field(default=()) 37 | 38 | 39 | class Styles(AbstractBase): 40 | """Represent a collection of styles.""" 41 | 42 | def __init__( 43 | self, 44 | *, 45 | elements: Iterable[ElementStyle] = (), 46 | relationships: Iterable[RelationshipStyle] = (), 47 | **kwargs, 48 | ) -> None: 49 | """Initialize the element and relationship styles.""" 50 | super().__init__(**kwargs) 51 | self.elements = list(elements) 52 | self.relationships = list(relationships) 53 | 54 | def add(self, style: Union[ElementStyle, RelationshipStyle]) -> None: 55 | """Add a new ElementStyle or RelationshipStyle.""" 56 | if isinstance(style, ElementStyle): 57 | self.elements.append(style) 58 | elif isinstance(style, RelationshipStyle): 59 | self.relationships.append(style) 60 | else: 61 | raise ValueError( 62 | f"Can't add unknown type of style '{type(style).__name__}'." 63 | ) 64 | 65 | def add_element_style(self, **kwargs) -> None: 66 | """ 67 | Add a new element style. 68 | 69 | See `ElementStyle` for arguments. 70 | """ 71 | self.elements.append(ElementStyle(**kwargs)) 72 | 73 | def clear_element_styles(self) -> None: 74 | """Remove all element styles.""" 75 | self.elements.clear() 76 | 77 | def add_relationship_style(self, **kwargs) -> None: 78 | """ 79 | Add a new relationship style. 80 | 81 | See `RelationshipStyle` for arguments. 82 | """ 83 | self.relationships.append(RelationshipStyle(**kwargs)) 84 | 85 | def clear_relationships_styles(self) -> None: 86 | """Remove all relationship styles.""" 87 | self.relationships.clear() 88 | 89 | @classmethod 90 | def hydrate(cls, styles_io: StylesIO) -> "Styles": 91 | """Hydrate a new Styles instance from its IO.""" 92 | return cls( 93 | elements=map(ElementStyle.hydrate, styles_io.elements), 94 | relationships=map(RelationshipStyle.hydrate, styles_io.relationships), 95 | ) 96 | -------------------------------------------------------------------------------- /src/structurizr/view/system_context_view.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a system context view.""" 17 | 18 | 19 | from pydantic import Field 20 | 21 | from ..model import Element, Person, SoftwareSystem 22 | from .static_view import StaticView, StaticViewIO 23 | 24 | 25 | __all__ = ("SystemContextView", "SystemContextViewIO") 26 | 27 | 28 | class SystemContextViewIO(StaticViewIO): 29 | """ 30 | Represent the system context view from the C4 model. 31 | 32 | Show how a software system fits into its environment, in terms of the users (people) 33 | and other software system dependencies. 34 | 35 | Attributes: 36 | enterprise_boundary_visible (bool): 37 | 38 | """ 39 | 40 | enterprise_boundary_visible: bool = Field(True, alias="enterpriseBoundaryVisible") 41 | 42 | 43 | class SystemContextView(StaticView): 44 | """ 45 | Represent the system context view from the C4 model. 46 | 47 | Show how a software system fits into its environment, in terms of the users (people) 48 | and other software system dependencies. 49 | 50 | Attributes: 51 | enterprise_boundary_visible (bool): 52 | 53 | """ 54 | 55 | def __init__(self, *, enterprise_boundary_visible: bool = True, **kwargs) -> None: 56 | """Initialize a system context view.""" 57 | super().__init__(**kwargs) 58 | self.enterprise_boundary_visible = enterprise_boundary_visible 59 | 60 | def add_all_elements(self) -> None: 61 | """Add all software systems and all people to this view.""" 62 | self.add_all_software_systems() 63 | self.add_all_people() 64 | 65 | def add_nearest_neighbours(self, element: Element): 66 | """Add all softare systems and people directly connected to the element.""" 67 | super().add_nearest_neighbours(element, SoftwareSystem) 68 | super().add_nearest_neighbours(element, Person) 69 | 70 | @classmethod 71 | def hydrate( 72 | cls, 73 | system_context_view_io: SystemContextViewIO, 74 | software_system: SoftwareSystem, 75 | ) -> "SystemContextView": 76 | """Hydrate a new SystemContextView instance from its IO.""" 77 | return cls( 78 | **cls.hydrate_arguments(system_context_view_io), 79 | software_system=software_system, 80 | enterprise_boundary_visible=( 81 | system_context_view_io.enterprise_boundary_visible 82 | ), 83 | # software_system=system_context_view_io.software_system, 84 | ) 85 | -------------------------------------------------------------------------------- /src/structurizr/view/system_landscape_view.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a system landscape view.""" 17 | 18 | 19 | from pydantic import Field 20 | 21 | from ..mixin import ModelRefMixin 22 | from ..model import Model 23 | from .static_view import StaticView, StaticViewIO 24 | 25 | 26 | __all__ = ("SystemLandscapeView", "SystemLandscapeViewIO") 27 | 28 | 29 | class SystemLandscapeViewIO(StaticViewIO): 30 | """ 31 | Represent a system landscape view that sits above the C4 model. 32 | 33 | This is the "big picture" view, showing the software systems and people in a given 34 | environment. The permitted elements in this view are software systems and people. 35 | 36 | Attributes: 37 | enterprise_boundary_visible (bool): Determines whether the enterprise boundary 38 | (to differentiate "internal" elements from "external" elements") should be 39 | visible on the resulting diagram. 40 | 41 | """ 42 | 43 | enterprise_boundary_visible: bool = Field(True, alias="enterpriseBoundaryVisible") 44 | 45 | 46 | class SystemLandscapeView(ModelRefMixin, StaticView): 47 | """ 48 | Represent a system landscape view that sits above the C4 model. 49 | 50 | This is the "big picture" view, showing the software systems and people in a given 51 | environment. The permitted elements in this view are software systems and people. 52 | 53 | Attributes: 54 | enterprise_boundary_visible (bool): 55 | 56 | """ 57 | 58 | def __init__( 59 | self, *, model: Model, enterprise_boundary_visible: bool = True, **kwargs 60 | ) -> None: 61 | """Initialize a system landscape view.""" 62 | super().__init__(**kwargs) 63 | self.enterprise_boundary_visible = enterprise_boundary_visible 64 | self.set_model(model) 65 | 66 | def add_all_elements(self) -> None: 67 | """Add all software systems and all people to this view.""" 68 | self.add_all_software_systems() 69 | self.add_all_people() 70 | 71 | @classmethod 72 | def hydrate( 73 | cls, 74 | system_landscape_view_io: SystemLandscapeViewIO, 75 | model: Model, 76 | ) -> "SystemLandscapeView": 77 | """Hydrate a new SystemLandscapeView instance from its IO.""" 78 | return cls( 79 | **cls.hydrate_arguments(system_landscape_view_io), 80 | model=model, 81 | enterprise_boundary_visible=( 82 | system_landscape_view_io.enterprise_boundary_visible 83 | ), 84 | ) 85 | -------------------------------------------------------------------------------- /src/structurizr/view/terminology.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a container for an element instance in a view.""" 17 | 18 | 19 | from typing import Optional 20 | 21 | from pydantic import Field 22 | 23 | from ..abstract_base import AbstractBase 24 | from ..base_model import BaseModel 25 | 26 | 27 | __all__ = ("Terminology", "TerminologyIO") 28 | 29 | 30 | class TerminologyIO(BaseModel): 31 | """Represent a way for the terminology on diagrams, etc. to be modified.""" 32 | 33 | enterprise: Optional[str] 34 | person: Optional[str] 35 | software_system: Optional[str] = Field(default=None, alias="softwareSystem") 36 | container: Optional[str] 37 | component: Optional[str] 38 | code: Optional[str] 39 | deployment_node: Optional[str] = Field(default=None, alias="deploymentNode") 40 | relationship: Optional[str] 41 | 42 | 43 | class Terminology(AbstractBase): 44 | """Provide a way for the terminology on diagrams, etc. to be modified.""" 45 | 46 | def __init__( 47 | self, 48 | *, 49 | enterprise: Optional[str] = None, 50 | person: Optional[str] = None, 51 | software_system: Optional[str] = None, 52 | container: Optional[str] = None, 53 | component: Optional[str] = None, 54 | code: Optional[str] = None, 55 | deployment_node: Optional[str] = None, 56 | relationship: Optional[str] = None, 57 | **kwargs 58 | ) -> None: 59 | """Initialize an element view.""" 60 | super().__init__(**kwargs) 61 | self.enterprise = enterprise 62 | self.person = person 63 | self.software_system = software_system 64 | self.container = container 65 | self.component = component 66 | self.code = code 67 | self.deployment_node = deployment_node 68 | self.relationship = relationship 69 | -------------------------------------------------------------------------------- /src/structurizr/view/vertex.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Ilai Fallach. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide a vertex model.""" 17 | from typing import Optional 18 | 19 | from pydantic import Field 20 | 21 | from ..abstract_base import AbstractBase 22 | from ..base_model import BaseModel 23 | 24 | 25 | __all__ = ("Vertex", "VertexIO") 26 | 27 | 28 | class VertexIO(BaseModel): 29 | """Define a wrapper for a vertex.""" 30 | 31 | x: Optional[int] = Field(default=None) 32 | y: Optional[int] = Field(default=None) 33 | 34 | 35 | class Vertex(AbstractBase): 36 | """Define a wrapper for a vertex.""" 37 | 38 | def __init__(self, *, x: int, y: int, **kwargs) -> None: 39 | """Initialize an automatic layout.""" 40 | super().__init__(**kwargs) 41 | self.x = x 42 | self.y = y 43 | 44 | @classmethod 45 | def hydrate(cls, vertex_io: VertexIO) -> "Vertex": 46 | """Hydrate a new Vertex instance from its IO.""" 47 | return cls(x=vertex_io.x, y=vertex_io.y) 48 | -------------------------------------------------------------------------------- /src/structurizr/view/view_sort_order.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Provide different ways sort the order of views.""" 17 | 18 | 19 | from enum import Enum, unique 20 | 21 | 22 | __all__ = ("ViewSortOrder",) 23 | 24 | 25 | @unique 26 | class ViewSortOrder(Enum): 27 | """ 28 | Customize a view sort order. 29 | 30 | Attributes: 31 | Default: Views are grouped by the software system they are associated with, and 32 | then sorted by type (System Landscape, System Context, Container, Component, 33 | Dynamic and Deployment) within these groups. 34 | Type: Views are sorted by type (System Landscape, System Context, Container, 35 | Component, Dynamic and Deployment). 36 | Key: Views are sorted by the view key (alphabetical, ascending). 37 | 38 | """ 39 | 40 | Default = "Default" 41 | Type = "Type" 42 | Key = "Key" 43 | -------------------------------------------------------------------------------- /tests/integration/data/workspace_definition/GettingStarted.json: -------------------------------------------------------------------------------- 1 | { 2 | "model": { 3 | "people": [ 4 | { 5 | "location": "Unspecified", 6 | "name": "User", 7 | "description": "A user of my software system.", 8 | "relationships": [ 9 | { 10 | "description": "Uses", 11 | "sourceId": "1", 12 | "destinationId": "2", 13 | "id": "3", 14 | "tags": "Relationship,Synchronous", 15 | "properties": {}, 16 | "perspectives": [] 17 | } 18 | ], 19 | "id": "1", 20 | "tags": "Element,Person", 21 | "properties": {}, 22 | "perspectives": [] 23 | } 24 | ], 25 | "softwareSystems": [ 26 | { 27 | "location": "Unspecified", 28 | "containers": [], 29 | "name": "Software System", 30 | "description": "My software system.", 31 | "relationships": [], 32 | "id": "2", 33 | "tags": "Element,Software System", 34 | "properties": {}, 35 | "perspectives": [] 36 | } 37 | ], 38 | "deploymentNodes": [] 39 | }, 40 | "views": { 41 | "systemLandscapeViews": [], 42 | "systemContextViews": [ 43 | { 44 | "animations": [], 45 | "key": "SystemContext", 46 | "softwareSystemId": "2", 47 | "description": "An example of a System Context diagram.", 48 | "paperSize": "A5_Landscape", 49 | "elements": [ 50 | { 51 | "properties": {}, 52 | "id": "2" 53 | }, 54 | { 55 | "properties": {}, 56 | "id": "1" 57 | } 58 | ], 59 | "relationships": [ 60 | { 61 | "properties": {}, 62 | "id": "3" 63 | } 64 | ] 65 | } 66 | ], 67 | "containerViews": [], 68 | "componentViews": [], 69 | "dynamicViews": [], 70 | "deploymentViews": [], 71 | "filteredViews": [], 72 | "configuration": { 73 | "styles": { 74 | "relationships": [], 75 | "elements": [ 76 | { 77 | "tag": "Software System", 78 | "background": "#1168bd", 79 | "color": "#ffffff" 80 | }, 81 | { 82 | "tag": "Person", 83 | "background": "#08427b", 84 | "color": "#ffffff", 85 | "shape": "Person" 86 | } 87 | ] 88 | }, 89 | "branding": {}, 90 | "terminology": {}, 91 | "viewSortOrder": "Default" 92 | } 93 | }, 94 | "documentation": { 95 | "sections": [], 96 | "decisions": [], 97 | "images": [] 98 | }, 99 | "name": "Getting Started", 100 | "description": "This is a model of my software system.", 101 | "configuration": { 102 | "users": [] 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/integration/data/workspace_definition/Trivial.json: -------------------------------------------------------------------------------- 1 | {"id":53491,"name":"Sandbox","description":"A workspace for testing the structurizr-python client.","revision":2,"lastModifiedDate":"2020-04-17T06:30:42Z","lastModifiedUser":"midnighter@posteo.net","lastModifiedAgent":"structurizr-web/1848","model":{"enterprise":{"name":"structurizr-python"}},"documentation":{},"views":{"configuration":{"branding":{},"styles":{},"terminology":{}}}} -------------------------------------------------------------------------------- /tests/integration/data/workspace_validation/ChildDeploymentNodeNamesAreNotUnique.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : 0, 3 | "name" : "Name", 4 | "description" : "Description", 5 | "configuration" : { }, 6 | "model" : { 7 | "deploymentNodes" : [ { 8 | "id" : "1", 9 | "name" : "Deployment Node", 10 | "environment" : "Default", 11 | "instances" : 1, 12 | "children" : [ { 13 | "id" : "2", 14 | "name" : "Deployment Node", 15 | "environment" : "Default", 16 | "instances" : 1, 17 | "children" : [ { 18 | "id" : "4", 19 | "name" : "Child", 20 | "environment" : "Default", 21 | "instances" : 1 22 | }, { 23 | "id" : "3", 24 | "name" : "Child", 25 | "environment" : "Default", 26 | "instances" : 1 27 | } ] 28 | } ] 29 | } ] 30 | }, 31 | "documentation" : { }, 32 | "views" : { 33 | "configuration" : { 34 | "branding" : { }, 35 | "styles" : { }, 36 | "terminology" : { } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /tests/integration/data/workspace_validation/ComponentNamesAreNotUnique.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : 0, 3 | "name" : "Name", 4 | "description" : "Description", 5 | "configuration" : { }, 6 | "model" : { 7 | "softwareSystems" : [ { 8 | "id" : "1", 9 | "tags" : "Element,Software System", 10 | "name" : "Software System", 11 | "location" : "Unspecified", 12 | "containers" : [ { 13 | "id" : "2", 14 | "tags" : "Element,Container", 15 | "name" : "Container", 16 | "components" : [ { 17 | "id" : "3", 18 | "tags" : "Element,Component", 19 | "name" : "Component", 20 | "size" : 0 21 | }, { 22 | "id" : "4", 23 | "tags" : "Element,Component", 24 | "name" : "Component", 25 | "size" : 0 26 | } ] 27 | } ] 28 | } ] 29 | }, 30 | "documentation" : { }, 31 | "views" : { 32 | "configuration" : { 33 | "branding" : { }, 34 | "styles" : { }, 35 | "terminology" : { } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /tests/integration/data/workspace_validation/ContainerAssociatedWithComponentViewIsMissingFromTheModel.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : 0, 3 | "name" : "Name", 4 | "description" : "Description", 5 | "configuration" : { }, 6 | "model" : { 7 | "softwareSystems" : [ { 8 | "id" : "1", 9 | "tags" : "Element,Software System", 10 | "name" : "Software System", 11 | "location" : "Unspecified", 12 | "containers" : [ { 13 | "id" : "2", 14 | "tags" : "Element,Container", 15 | "name" : "Container" 16 | } ] 17 | } ] 18 | }, 19 | "documentation" : { }, 20 | "views" : { 21 | "componentViews" : [ { 22 | "softwareSystemId" : "1", 23 | "description" : "Description", 24 | "key" : "Components", 25 | "containerId" : "3" 26 | } ], 27 | "configuration" : { 28 | "branding" : { }, 29 | "styles" : { }, 30 | "terminology" : { } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /tests/integration/data/workspace_validation/ContainerNamesAreNotUnique.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : 0, 3 | "name" : "Name", 4 | "description" : "Description", 5 | "configuration" : { }, 6 | "model" : { 7 | "softwareSystems" : [ { 8 | "id" : "1", 9 | "tags" : "Element,Software System", 10 | "name" : "Software System", 11 | "location" : "Unspecified", 12 | "containers" : [ { 13 | "id" : "2", 14 | "tags" : "Element,Container", 15 | "name" : "Container" 16 | }, { 17 | "id" : "3", 18 | "tags" : "Element,Container", 19 | "name" : "Container" 20 | } ] 21 | } ] 22 | }, 23 | "documentation" : { }, 24 | "views" : { 25 | "configuration" : { 26 | "branding" : { }, 27 | "styles" : { }, 28 | "terminology" : { } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /tests/integration/data/workspace_validation/ElementAssociatedWithDecisionIsMissingFromTheModel.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : 0, 3 | "name" : "Name", 4 | "description" : "Description", 5 | "configuration" : { }, 6 | "model" : { 7 | "softwareSystems" : [ { 8 | "id" : "1", 9 | "tags" : "Element,Software System", 10 | "name" : "Software System", 11 | "location" : "Unspecified" 12 | } ] 13 | }, 14 | "documentation" : { 15 | "decisions" : [ { 16 | "elementId" : "2", 17 | "id" : "1", 18 | "date" : "2019-10-24T19:17:46Z", 19 | "title" : "Use Java", 20 | "status" : "Proposed", 21 | "content" : "Content", 22 | "format" : "Markdown" 23 | } ] 24 | }, 25 | "views" : { 26 | "configuration" : { 27 | "branding" : { }, 28 | "styles" : { }, 29 | "terminology" : { } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /tests/integration/data/workspace_validation/ElementAssociatedWithDocumentationSectionIsMissingFromTheModel.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : 0, 3 | "name" : "Name", 4 | "description" : "Description", 5 | "configuration" : { }, 6 | "model" : { 7 | "softwareSystems" : [ { 8 | "id" : "1", 9 | "tags" : "Element,Software System", 10 | "name" : "Software System", 11 | "location" : "Unspecified" 12 | } ] 13 | }, 14 | "documentation" : { 15 | "sections" : [ { 16 | "elementId" : "2", 17 | "title" : "Context", 18 | "order" : 1, 19 | "format" : "Markdown", 20 | "content" : "Content" 21 | } ], 22 | "template" : { 23 | "name" : "Software Guidebook", 24 | "author" : "Simon Brown", 25 | "url" : "https://leanpub.com/visualising-software-architecture" 26 | } 27 | }, 28 | "views" : { 29 | "configuration" : { 30 | "branding" : { }, 31 | "styles" : { }, 32 | "terminology" : { } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /tests/integration/data/workspace_validation/ElementAssociatedWithDynamicViewIsMissingFromTheModel.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : 0, 3 | "name" : "Name", 4 | "description" : "Description", 5 | "configuration" : { }, 6 | "model" : { 7 | "softwareSystems" : [ { 8 | "id" : "1", 9 | "tags" : "Element,Software System", 10 | "name" : "Software System", 11 | "location" : "Unspecified" 12 | } ] 13 | }, 14 | "documentation" : { }, 15 | "views" : { 16 | "dynamicViews" : [ { 17 | "description" : "Description", 18 | "key" : "Dynamic", 19 | "elementId" : "2" 20 | } ], 21 | "configuration" : { 22 | "branding" : { }, 23 | "styles" : { }, 24 | "terminology" : { } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /tests/integration/data/workspace_validation/ElementIdsAreNotUnique.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : 0, 3 | "name" : "Name", 4 | "description" : "Description", 5 | "configuration" : { }, 6 | "model" : { 7 | "softwareSystems" : [ { 8 | "id" : "1", 9 | "tags" : "Element,Software System", 10 | "name" : "Software System 1", 11 | "location" : "Unspecified" 12 | }, { 13 | "id" : "1", 14 | "tags" : "Element,Software System", 15 | "name" : "Software System 2", 16 | "location" : "Unspecified" 17 | } ] 18 | }, 19 | "documentation" : { }, 20 | "views" : { 21 | "configuration" : { 22 | "branding" : { }, 23 | "styles" : { }, 24 | "terminology" : { } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /tests/integration/data/workspace_validation/ElementReferencedByViewIsMissingFromTheModel.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : 0, 3 | "name" : "Name", 4 | "description" : "Description", 5 | "configuration" : { }, 6 | "model" : { 7 | "people" : [ { 8 | "id" : "1", 9 | "tags" : "Element,Person", 10 | "name" : "Person", 11 | "location" : "Unspecified" 12 | } ] 13 | }, 14 | "documentation" : { }, 15 | "views" : { 16 | "systemLandscapeViews" : [ { 17 | "key" : "SystemLandscape", 18 | "enterpriseBoundaryVisible" : true, 19 | "elements" : [ { 20 | "id" : "2", 21 | "x" : 0, 22 | "y" : 0 23 | } ] 24 | } ], 25 | "configuration" : { 26 | "branding" : { }, 27 | "styles" : { }, 28 | "terminology" : { } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /tests/integration/data/workspace_validation/PeopleAndSoftwareSystemNamesAreNotUnique.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : 0, 3 | "name" : "Name", 4 | "description" : "Description", 5 | "configuration" : { }, 6 | "model" : { 7 | "people" : [ { 8 | "id" : "1", 9 | "tags" : "Element,Person", 10 | "name" : "Name", 11 | "location" : "Unspecified" 12 | } ], 13 | "softwareSystems" : [ { 14 | "id" : "2", 15 | "tags" : "Element,Software System", 16 | "name" : "Name", 17 | "location" : "Unspecified" 18 | } ] 19 | }, 20 | "documentation" : { }, 21 | "views" : { 22 | "configuration" : { 23 | "branding" : { }, 24 | "styles" : { }, 25 | "terminology" : { } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /tests/integration/data/workspace_validation/RelationshipDescriptionsAreNotUnique.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : 0, 3 | "name" : "Name", 4 | "description" : "Description", 5 | "configuration" : { }, 6 | "model" : { 7 | "people" : [ { 8 | "id" : "1", 9 | "tags" : "Element,Person", 10 | "name" : "User", 11 | "relationships" : [ { 12 | "id" : "3", 13 | "tags" : "Relationship,Synchronous", 14 | "sourceId" : "1", 15 | "destinationId" : "2", 16 | "description" : "Uses", 17 | "interactionStyle" : "Synchronous" 18 | }, { 19 | "id" : "4", 20 | "tags" : "Relationship,Synchronous", 21 | "sourceId" : "1", 22 | "destinationId" : "2", 23 | "description" : "Uses", 24 | "interactionStyle" : "Synchronous" 25 | } ], 26 | "location" : "Unspecified" 27 | } ], 28 | "softwareSystems" : [ { 29 | "id" : "2", 30 | "tags" : "Element,Software System", 31 | "name" : "Software System", 32 | "location" : "Unspecified" 33 | } ] 34 | }, 35 | "documentation" : { }, 36 | "views" : { 37 | "configuration" : { 38 | "branding" : { }, 39 | "styles" : { }, 40 | "terminology" : { } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /tests/integration/data/workspace_validation/RelationshipIdsAreNotUnique.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : 0, 3 | "name" : "Name", 4 | "description" : "Description", 5 | "configuration" : { }, 6 | "model" : { 7 | "people" : [ { 8 | "id" : "1", 9 | "tags" : "Element,Person", 10 | "name" : "User", 11 | "relationships" : [ { 12 | "id" : "3", 13 | "tags" : "Relationship,Synchronous", 14 | "sourceId" : "1", 15 | "destinationId" : "2", 16 | "description" : "Uses 1", 17 | "interactionStyle" : "Synchronous" 18 | }, { 19 | "id" : "3", 20 | "tags" : "Relationship,Synchronous", 21 | "sourceId" : "1", 22 | "destinationId" : "2", 23 | "description" : "Uses 2", 24 | "interactionStyle" : "Synchronous" 25 | } ], 26 | "location" : "Unspecified" 27 | } ], 28 | "softwareSystems" : [ { 29 | "id" : "2", 30 | "tags" : "Element,Software System", 31 | "name" : "Software System", 32 | "location" : "Unspecified" 33 | } ] 34 | }, 35 | "documentation" : { }, 36 | "views" : { 37 | "configuration" : { 38 | "branding" : { }, 39 | "styles" : { }, 40 | "terminology" : { } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /tests/integration/data/workspace_validation/RelationshipReferencedByViewIsMissingFromTheModel.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : 0, 3 | "name" : "Name", 4 | "description" : "Description", 5 | "configuration" : { }, 6 | "model" : { 7 | "people" : [ { 8 | "id" : "1", 9 | "tags" : "Element,Person", 10 | "name" : "Person", 11 | "relationships" : [ { 12 | "id" : "3", 13 | "tags" : "Relationship,Synchronous", 14 | "sourceId" : "1", 15 | "destinationId" : "2", 16 | "description" : "Uses", 17 | "interactionStyle" : "Synchronous" 18 | } ], 19 | "location" : "Unspecified" 20 | } ], 21 | "softwareSystems" : [ { 22 | "id" : "2", 23 | "tags" : "Element,Software System", 24 | "name" : "Software System", 25 | "location" : "Unspecified" 26 | } ] 27 | }, 28 | "documentation" : { }, 29 | "views" : { 30 | "systemLandscapeViews" : [ { 31 | "key" : "SystemLandscape", 32 | "enterpriseBoundaryVisible" : true, 33 | "elements" : [ { 34 | "id" : "1", 35 | "x" : 0, 36 | "y" : 0 37 | }, { 38 | "id" : "2", 39 | "x" : 0, 40 | "y" : 0 41 | } ], 42 | "relationships" : [ { 43 | "id" : "4" 44 | } ] 45 | } ], 46 | "configuration" : { 47 | "branding" : { }, 48 | "styles" : { }, 49 | "terminology" : { } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /tests/integration/data/workspace_validation/SoftwareSystemAssociatedWithContainerViewIsMissingFromTheModel.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : 0, 3 | "name" : "Name", 4 | "description" : "Description", 5 | "configuration" : { }, 6 | "model" : { 7 | "softwareSystems" : [ { 8 | "id" : "1", 9 | "tags" : "Element,Software System", 10 | "name" : "Software System", 11 | "location" : "Unspecified" 12 | } ] 13 | }, 14 | "documentation" : { }, 15 | "views" : { 16 | "containerViews" : [ { 17 | "softwareSystemId" : "2", 18 | "description" : "Description", 19 | "key" : "Containers" 20 | } ], 21 | "configuration" : { 22 | "branding" : { }, 23 | "styles" : { }, 24 | "terminology" : { } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /tests/integration/data/workspace_validation/SoftwareSystemAssociatedWithDeploymentViewIsMissingFromTheModel.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : 0, 3 | "name" : "Name", 4 | "description" : "Description", 5 | "configuration" : { }, 6 | "model" : { 7 | "softwareSystems" : [ { 8 | "id" : "1", 9 | "tags" : "Element,Software System", 10 | "name" : "Software System", 11 | "location" : "Unspecified" 12 | } ] 13 | }, 14 | "documentation" : { }, 15 | "views" : { 16 | "deploymentViews" : [ { 17 | "softwareSystemId" : "2", 18 | "description" : "Description", 19 | "key" : "Deployment" 20 | } ], 21 | "configuration" : { 22 | "branding" : { }, 23 | "styles" : { }, 24 | "terminology" : { } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /tests/integration/data/workspace_validation/SoftwareSystemAssociatedWithSystemContextViewIsMissingFromTheModel.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : 0, 3 | "name" : "Name", 4 | "description" : "Description", 5 | "configuration" : { }, 6 | "model" : { 7 | "softwareSystems" : [ { 8 | "id" : "1", 9 | "tags" : "Element,Software System", 10 | "name" : "Software System", 11 | "location" : "Unspecified" 12 | } ] 13 | }, 14 | "documentation" : { }, 15 | "views" : { 16 | "systemContextViews" : [ { 17 | "softwareSystemId" : "2", 18 | "description" : "Description", 19 | "key" : "SystemContext", 20 | "enterpriseBoundaryVisible" : true, 21 | "elements" : [ { 22 | "id" : "1", 23 | "x" : 0, 24 | "y" : 0 25 | } ] 26 | } ], 27 | "configuration" : { 28 | "branding" : { }, 29 | "styles" : { }, 30 | "terminology" : { } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /tests/integration/data/workspace_validation/TopLevelDeploymentNodeNamesAreNotUnique.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : 0, 3 | "name" : "Name", 4 | "description" : "Description", 5 | "configuration" : { }, 6 | "model" : { 7 | "deploymentNodes" : [ { 8 | "id" : "1", 9 | "name" : "Deployment Node", 10 | "environment" : "Default", 11 | "instances" : 1 12 | }, { 13 | "id" : "2", 14 | "name" : "Deployment Node", 15 | "environment" : "Default", 16 | "instances" : 1 17 | } ] 18 | }, 19 | "documentation" : { }, 20 | "views" : { 21 | "configuration" : { 22 | "branding" : { }, 23 | "styles" : { }, 24 | "terminology" : { } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /tests/integration/data/workspace_validation/TopLevelDeploymentNodeNamesAreNotUniqueButTheyExistInDifferentEnvironments.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : 0, 3 | "name" : "Name", 4 | "description" : "Description", 5 | "configuration" : { }, 6 | "model" : { 7 | "deploymentNodes" : [ { 8 | "id" : "1", 9 | "name" : "Deployment Node", 10 | "environment" : "Development", 11 | "instances" : 1 12 | }, { 13 | "id" : "2", 14 | "name" : "Deployment Node", 15 | "environment" : "Production", 16 | "instances" : 1 17 | } ] 18 | }, 19 | "documentation" : { }, 20 | "views" : { 21 | "configuration" : { 22 | "branding" : { }, 23 | "styles" : { }, 24 | "terminology" : { } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /tests/integration/data/workspace_validation/ViewAssociatedWithFilteredViewIsMissingFromTheWorkspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : 0, 3 | "name" : "Name", 4 | "description" : "Description", 5 | "configuration" : { }, 6 | "model" : { }, 7 | "documentation" : { }, 8 | "views" : { 9 | "systemLandscapeViews" : [ { 10 | "key" : "SystemLandscape", 11 | "enterpriseBoundaryVisible" : true 12 | } ], 13 | "filteredViews" : [ { 14 | "baseViewKey" : "SystemContext", 15 | "key" : "Filtered", 16 | "mode" : "Include" 17 | } ], 18 | "configuration" : { 19 | "branding" : { }, 20 | "styles" : { }, 21 | "terminology" : { } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /tests/integration/data/workspace_validation/ViewKeysAreNotUnique.json: -------------------------------------------------------------------------------- 1 | { 2 | "id" : 0, 3 | "name" : "Name", 4 | "description" : "Description", 5 | "configuration" : { }, 6 | "model" : { }, 7 | "documentation" : { }, 8 | "views" : { 9 | "systemLandscapeViews" : [ { 10 | "key" : "key", 11 | "enterpriseBoundaryVisible" : true 12 | }, { 13 | "key" : "key", 14 | "enterpriseBoundaryVisible" : true 15 | } ], 16 | "configuration" : { 17 | "branding" : { }, 18 | "styles" : { }, 19 | "terminology" : { } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /tests/integration/test_api.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Ensure a consistent public package interface.""" 17 | 18 | 19 | from importlib import import_module 20 | 21 | import pytest 22 | 23 | 24 | @pytest.mark.parametrize( 25 | "public_module, symbol", 26 | [ 27 | ("structurizr", "show_versions"), 28 | ("structurizr", "Workspace"), 29 | ("structurizr", "StructurizrClient"), 30 | ("structurizr", "StructurizrClientException"), 31 | ("structurizr", "StructurizrClientSettings"), 32 | ], 33 | ) 34 | def test_public_api(public_module, symbol): 35 | """Expect the given public package interface.""" 36 | public_module = import_module(public_module) 37 | assert hasattr(public_module, symbol) 38 | -------------------------------------------------------------------------------- /tests/integration/test_grouping.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # https://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | """Ensure grouping works with example workspace.""" 14 | 15 | from pathlib import Path 16 | 17 | from structurizr import Workspace 18 | 19 | 20 | DEFINITIONS = Path(__file__).parent / "data" / "workspace_definition" 21 | 22 | 23 | def test_loading_workspace_with_groups(): 24 | """Check loading an example workspace with groupings defined.""" 25 | path = DEFINITIONS / "Grouping.json" 26 | 27 | workspace = Workspace.load(path) 28 | consumer_a = workspace.model.get_element("1") 29 | consumer_d = workspace.model.get_element("4") 30 | assert consumer_a.name == "Consumer A" 31 | assert consumer_a.group == "Consumers - Group 1" 32 | assert consumer_d.name == "Consumer D" 33 | assert consumer_d.group == "Consumers - Group 2" 34 | 35 | service_2_api = workspace.model.get_element("9") 36 | assert service_2_api.name == "Service 2 API" 37 | assert service_2_api.group == "Service 2" 38 | -------------------------------------------------------------------------------- /tests/integration/test_model_deployment_node_deserialization.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # https://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | """ 14 | Ensure that relationships in Elements and in the Model are consistent. 15 | 16 | See https://github.com/Midnighter/structurizr-python/issues/31. 17 | """ 18 | 19 | from pathlib import Path 20 | 21 | import pytest 22 | 23 | from structurizr import Workspace 24 | 25 | 26 | DEFINITIONS = Path(__file__).parent / "data" / "workspace_definition" 27 | 28 | 29 | @pytest.mark.parametrize( 30 | "filename", 31 | ["BigBank.json"], 32 | ) 33 | def test_model_deserialises_deployment_nodes(filename: str): 34 | """Ensure deserialisaton of deployment nodes works.""" 35 | path = DEFINITIONS / filename 36 | workspace = Workspace.load(path) 37 | model = workspace.model 38 | 39 | db_server = model.get_element("59") 40 | assert db_server.name == "Docker Container - Database Server" 41 | assert db_server is not None 42 | assert db_server.model is model 43 | assert db_server.parent.name == "Developer Laptop" 44 | assert db_server.parent.parent is None 45 | -------------------------------------------------------------------------------- /tests/integration/test_model_elements.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Ensure that elements are correctly handled by the model.""" 17 | 18 | 19 | import pytest 20 | 21 | from structurizr.model import Person, SoftwareSystem 22 | from structurizr.model.model import Model 23 | 24 | 25 | @pytest.fixture(scope="function") 26 | def model() -> Model: 27 | """Manufacture an empty model for test cases.""" 28 | return Model() 29 | 30 | 31 | @pytest.mark.parametrize( 32 | "attributes", 33 | [{"name": "User"}], 34 | ) 35 | def test_add_person_from_args(attributes: dict, model: Model): 36 | """Expect that a person can be added to the model.""" 37 | person = model.add_person(**attributes) 38 | assert person.id == "1" 39 | assert len(model.people) == 1 40 | for attr, expected in attributes.items(): 41 | assert getattr(person, attr) == expected 42 | 43 | 44 | @pytest.mark.parametrize( 45 | "attributes", 46 | [{"name": "User"}], 47 | ) 48 | def test_add_person(attributes: dict, model: Model): 49 | """Expect that a person can be added to the model.""" 50 | person = Person(**attributes) 51 | model += person 52 | assert person.id == "1" 53 | assert len(model.people) == 1 54 | for attr, expected in attributes.items(): 55 | assert getattr(person, attr) == expected 56 | 57 | 58 | @pytest.mark.parametrize( 59 | "attributes", 60 | [{"name": "SkyNet"}], 61 | ) 62 | def test_add_software_system_from_args(attributes: dict, model: Model): 63 | """Expect that a software system can be added to the model.""" 64 | software_system = model.add_software_system(**attributes) 65 | assert software_system.id == "1" 66 | assert len(model.software_systems) == 1 67 | for attr, expected in attributes.items(): 68 | assert getattr(software_system, attr) == expected 69 | 70 | 71 | @pytest.mark.parametrize( 72 | "attributes", 73 | [{"name": "SkyNet"}], 74 | ) 75 | def test_add_software_system(attributes: dict, model: Model): 76 | """Expect that a software system can be added to the model.""" 77 | software_system = SoftwareSystem(**attributes) 78 | model += software_system 79 | assert software_system.id == "1" 80 | assert len(model.software_systems) == 1 81 | for attr, expected in attributes.items(): 82 | assert getattr(software_system, attr) == expected 83 | -------------------------------------------------------------------------------- /tests/unit/api/test_api_response.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Ensure the expected behaviour of the API response.""" 17 | 18 | 19 | import pytest 20 | 21 | from structurizr.api.api_response import APIResponse 22 | 23 | 24 | @pytest.mark.parametrize( 25 | "attributes", 26 | [ 27 | {"success": True, "message": "well done"}, 28 | {"success": False, "message": "what a pity!"}, 29 | {"success": True, "message": "well done", "revision": 2}, 30 | {"success": False, "message": "what a pity!", "revision": 2}, 31 | ], 32 | ) 33 | def test_init_from_arguments(attributes: dict): 34 | """Expect proper initialization from arguments.""" 35 | response = APIResponse(**attributes) 36 | for attr, expected in attributes.items(): 37 | assert getattr(response, attr) == expected 38 | -------------------------------------------------------------------------------- /tests/unit/model/test_code_element.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Ensure the expected behaviour of the code element.""" 17 | 18 | 19 | import pytest 20 | 21 | from structurizr.model.code_element import CodeElement 22 | 23 | 24 | @pytest.mark.parametrize( 25 | "attributes", 26 | [{}], 27 | ) 28 | def test_code_element_init(attributes): 29 | """Expect proper initialization from arguments.""" 30 | element = CodeElement(**attributes) 31 | for attr, expected in attributes.items(): 32 | assert getattr(element, attr) == expected 33 | -------------------------------------------------------------------------------- /tests/unit/model/test_code_element_role.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Ensure the expected behaviour of the code element role enumeration.""" 17 | 18 | 19 | import pytest 20 | 21 | from structurizr.model.code_element_role import CodeElementRole 22 | 23 | 24 | @pytest.mark.parametrize( 25 | "role, expected", 26 | [("Primary", CodeElementRole.Primary), ("Supporting", CodeElementRole.Supporting)], 27 | ) 28 | def test_location(role: str, expected: CodeElementRole): 29 | """Expect proper initialization from string.""" 30 | assert CodeElementRole(role) == expected 31 | -------------------------------------------------------------------------------- /tests/unit/model/test_element.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Ensure the expected behaviour of the model element.""" 17 | 18 | 19 | import pytest 20 | 21 | from structurizr.mixin.childless_mixin import ChildlessMixin 22 | from structurizr.model.element import Element 23 | 24 | 25 | class ConcreteElement(ChildlessMixin, Element): 26 | """Implement a concrete `Element` class for testing purposes.""" 27 | 28 | pass 29 | 30 | 31 | class MockModel: 32 | """Implement a mock model for reference testing.""" 33 | 34 | def add_relationship(self, relationship, create_implied_relationships): 35 | """Provide mock implementation.""" 36 | return relationship 37 | 38 | 39 | @pytest.mark.parametrize( 40 | "attributes", 41 | [ 42 | pytest.param({}, marks=pytest.mark.raises(exception=TypeError)), 43 | {"name": "Important Element"}, 44 | ], 45 | ) 46 | def test_element_init(attributes): 47 | """Expect proper initialization from arguments.""" 48 | element = ConcreteElement(**attributes) 49 | for attr, expected in attributes.items(): 50 | assert getattr(element, attr) == expected 51 | 52 | 53 | def test_model_reference(): 54 | """Expect that setting the model creates a reference.""" 55 | model = MockModel() 56 | element = ConcreteElement(name="Element") 57 | element.set_model(model) 58 | assert element.get_model() is model 59 | 60 | 61 | def test_element_child_elements_default(): 62 | """Ensure that by default, element has no children.""" 63 | element = ConcreteElement(name="Element") 64 | assert element.child_elements == [] 65 | 66 | 67 | def test_element_can_only_add_relationship_to_source(): 68 | """Make sure that nothing adds a relationship to the wrong element.""" 69 | element1 = ConcreteElement(name="elt1") 70 | element2 = ConcreteElement(name="elt1") 71 | with pytest.raises( 72 | ValueError, 73 | match="Cannot add relationship .* to element .* that is not its source", 74 | ): 75 | element1.add_relationship(source=element2) 76 | 77 | 78 | def test_element_add_relationship_can_omit_source(): 79 | """Expect that creating a relationship uses the default source.""" 80 | element1 = ConcreteElement(name="elt1") 81 | element2 = ConcreteElement(name="elt1") 82 | model = MockModel() 83 | element1.set_model(model) 84 | relationship = element1.add_relationship(destination=element2) 85 | assert relationship.source is element1 86 | 87 | 88 | def test_element_add_relationship_twice_is_ok(): 89 | """Ensure that adding the same relationship twice is fine.""" 90 | element1 = ConcreteElement(name="elt1") 91 | element2 = ConcreteElement(name="elt1") 92 | model = MockModel() 93 | element1.set_model(model) 94 | relationship = element1.add_relationship(destination=element2) 95 | element1.add_relationship(relationship) 96 | assert element1.relationships == {relationship} 97 | -------------------------------------------------------------------------------- /tests/unit/model/test_enterprise.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Ensure the expected behaviour of the enterprise model.""" 17 | 18 | 19 | import pytest 20 | 21 | from structurizr.model.enterprise import Enterprise 22 | 23 | 24 | @pytest.mark.parametrize( 25 | "attributes", 26 | [ 27 | pytest.param({}, marks=pytest.mark.raises(exception=TypeError)), 28 | {"name": "Umbrella Corporation"}, 29 | ], 30 | ) 31 | def test_enterprise_init(attributes): 32 | """Expect proper initialization from arguments.""" 33 | enterprise = Enterprise(**attributes) 34 | for attr, expected in attributes.items(): 35 | assert getattr(enterprise, attr) == expected 36 | -------------------------------------------------------------------------------- /tests/unit/model/test_groupable_element.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # https://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | 14 | """Ensure the expected behaviour of GroupableElement.""" 15 | 16 | from structurizr.mixin.childless_mixin import ChildlessMixin 17 | from structurizr.model.groupable_element import GroupableElement, GroupableElementIO 18 | 19 | 20 | class ConcreteElement(ChildlessMixin, GroupableElement): 21 | """Implement a concrete `GroupableElement` class for testing purposes.""" 22 | 23 | pass 24 | 25 | 26 | def test_group_name_normalisation(): 27 | """Test that empty group names are normalised to None.""" 28 | assert ConcreteElement(name="Name").group is None 29 | assert ConcreteElement(name="Name", group=None).group is None 30 | assert ConcreteElement(name="Name", group="").group is None 31 | assert ConcreteElement(name="Name", group=" ").group is None 32 | assert ConcreteElement(name="Name", group=" g1 ").group == "g1" 33 | 34 | 35 | def test_group_in_json(): 36 | """Test the group field is output to JSON.""" 37 | element = ConcreteElement(name="Name", group="Group 1") 38 | io = GroupableElementIO.from_orm(element) 39 | assert '"group": "Group 1"' in io.json() 40 | 41 | 42 | def test_group_omitted_from_json_if_empty(): 43 | """Test the group field is not output if empty.""" 44 | element = ConcreteElement(name="Name") 45 | io = GroupableElementIO.from_orm(element) 46 | assert '"group"' not in io.json() 47 | 48 | 49 | def test_hydration(): 50 | """Test hydration picks up the group field.""" 51 | element = ConcreteElement(name="Name", group="Group 1") 52 | io = GroupableElementIO.from_orm(element) 53 | d = GroupableElement.hydrate_arguments(io) 54 | assert d["group"] == "Group 1" 55 | -------------------------------------------------------------------------------- /tests/unit/model/test_infrastructure_node.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # https://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | 14 | """Ensure the expected behaviour of the container element.""" 15 | 16 | 17 | import pytest 18 | 19 | from structurizr.model.infrastructure_node import ( 20 | InfrastructureNode, 21 | InfrastructureNodeIO, 22 | ) 23 | from structurizr.model.tags import Tags 24 | 25 | 26 | class MockModel: 27 | """Implement a mock model for testing.""" 28 | 29 | def __init__(self): 30 | """Initialize the mock.""" 31 | pass 32 | 33 | def __iadd__(self, node): 34 | """Simulate the model assigning IDs to new elements.""" 35 | if not node.id: 36 | node.id = "id" 37 | node.set_model(self) 38 | return self 39 | 40 | 41 | @pytest.fixture(scope="function") 42 | def empty_model() -> MockModel: 43 | """Provide an new empty model on demand for test cases to use.""" 44 | return MockModel() 45 | 46 | 47 | @pytest.mark.parametrize( 48 | "attributes", 49 | [ 50 | pytest.param({}, marks=pytest.mark.raises(exception=TypeError)), 51 | {"name": "Node1", "technology": "tech1"}, 52 | ], 53 | ) 54 | def test_infrastructure_node_init(attributes): 55 | """Expect proper initialization from arguments.""" 56 | node = InfrastructureNode(**attributes) 57 | for attr, expected in attributes.items(): 58 | assert getattr(node, attr) == expected 59 | 60 | 61 | def test_infrastructure_node_tags(): 62 | """Check default tags.""" 63 | node = InfrastructureNode(name="Node") 64 | assert Tags.ELEMENT in node.tags 65 | assert Tags.INFRASTRUCTURE_NODE in node.tags 66 | 67 | 68 | def test_infrastructure_node_hydration(): 69 | """Check hydrating an infrastructure node from its IO.""" 70 | io = InfrastructureNodeIO(name="node1", technology="tech") 71 | parent = object() 72 | 73 | node = InfrastructureNode.hydrate(io, parent=parent) 74 | 75 | assert node.name == "node1" 76 | assert node.technology == "tech" 77 | assert node.parent is parent 78 | -------------------------------------------------------------------------------- /tests/unit/model/test_interaction_style.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Ensure the expected behaviour of the interaction style enumeration.""" 17 | 18 | 19 | import pytest 20 | 21 | from structurizr.model.interaction_style import InteractionStyle 22 | 23 | 24 | @pytest.mark.parametrize( 25 | "style, expected", 26 | [ 27 | ("Synchronous", InteractionStyle.Synchronous), 28 | ("Asynchronous", InteractionStyle.Asynchronous), 29 | ], 30 | ) 31 | def test_interaction_style(style: str, expected: InteractionStyle): 32 | """Expect proper initialization from interaction style strings.""" 33 | assert InteractionStyle(style) == expected 34 | -------------------------------------------------------------------------------- /tests/unit/model/test_location.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Ensure the expected behaviour of the location enumeration.""" 17 | 18 | 19 | import pytest 20 | 21 | from structurizr.model.location import Location 22 | 23 | 24 | @pytest.mark.parametrize( 25 | "location, expected", 26 | [ 27 | ("Unspecified", Location.Unspecified), 28 | ("Internal", Location.Internal), 29 | ("External", Location.External), 30 | ], 31 | ) 32 | def test_location(location: str, expected: Location): 33 | """Expect proper initialization from location string.""" 34 | assert Location(location) == expected 35 | -------------------------------------------------------------------------------- /tests/unit/model/test_person.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Ensure the expected behaviour of the person element.""" 17 | 18 | 19 | import pytest 20 | 21 | from structurizr.model.person import Person 22 | 23 | 24 | @pytest.mark.parametrize( 25 | "attributes", 26 | [ 27 | pytest.param({}, marks=pytest.mark.raises(exception=TypeError)), 28 | {"name": "User"}, 29 | ], 30 | ) 31 | def test_person_init(attributes): 32 | """Expect proper initialization from arguments.""" 33 | person = Person(**attributes) 34 | for attr, expected in attributes.items(): 35 | assert getattr(person, attr) == expected 36 | -------------------------------------------------------------------------------- /tests/unit/model/test_perspective.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Ensure the expected behaviour of the architectural perspective model.""" 17 | 18 | 19 | import pytest 20 | 21 | from structurizr.model.perspective import Perspective 22 | 23 | 24 | @pytest.mark.parametrize( 25 | "attributes", 26 | [ 27 | pytest.param({}, marks=pytest.mark.raises(exception=TypeError)), 28 | pytest.param({}, marks=pytest.mark.raises(exception=TypeError)), 29 | { 30 | "name": "Accessibility", 31 | "description": "The ability of the system to be used by people with " 32 | "disabilities.", 33 | }, 34 | ], 35 | ) 36 | def test_perspective_init(attributes): 37 | """Expect proper initialization from arguments.""" 38 | perspective = Perspective(**attributes) 39 | for attr, expected in attributes.items(): 40 | assert getattr(perspective, attr) == expected 41 | -------------------------------------------------------------------------------- /tests/unit/model/test_relationship.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Ensure the expected behaviour of relationships.""" 17 | 18 | import pytest 19 | 20 | from structurizr.model.interaction_style import InteractionStyle 21 | from structurizr.model.relationship import Relationship 22 | from structurizr.model.tags import Tags 23 | 24 | 25 | @pytest.mark.parametrize( 26 | "attributes", 27 | [{}], 28 | ) 29 | def test_relationship_init(attributes): 30 | """Expect proper initialization from arguments.""" 31 | relationship = Relationship(**attributes) 32 | for attr, expected in attributes.items(): 33 | assert getattr(relationship, attr) == expected 34 | 35 | 36 | def test_relationship_interaction_style(): 37 | """Test that interaction style is consistent with tags.""" 38 | relationship = Relationship(interaction_style=InteractionStyle.Synchronous) 39 | assert Tags.SYNCHRONOUS in relationship.tags 40 | assert Tags.ASYNCHRONOUS not in relationship.tags 41 | assert relationship.interaction_style == InteractionStyle.Synchronous 42 | 43 | relationship = Relationship(interaction_style=InteractionStyle.Asynchronous) 44 | assert Tags.SYNCHRONOUS not in relationship.tags 45 | assert Tags.ASYNCHRONOUS in relationship.tags 46 | assert relationship.interaction_style == InteractionStyle.Asynchronous 47 | -------------------------------------------------------------------------------- /tests/unit/test_abstract_base.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Ensure the expected behaviour of the abstract base class.""" 17 | 18 | 19 | from structurizr.abstract_base import AbstractBase 20 | 21 | 22 | class ConcreteBase(AbstractBase): 23 | """Implement a concrete class for testing purposes.""" 24 | 25 | pass 26 | 27 | 28 | def test_base_init(): 29 | """Expect proper initialization from arguments.""" 30 | ConcreteBase() 31 | 32 | 33 | def test_hash_int(): 34 | """Expect that a concrete class's hash is an integer.""" 35 | assert isinstance(hash(ConcreteBase()), int) 36 | 37 | 38 | def test_set_collection(): 39 | """Expect that a concrete class can be collected in a set.""" 40 | {ConcreteBase(), ConcreteBase(), ConcreteBase()} # noqa: B018 41 | -------------------------------------------------------------------------------- /tests/unit/test_base_model.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Ensure the expected behaviour of the base model.""" 17 | 18 | 19 | from structurizr.base_model import BaseModel 20 | 21 | 22 | def test_base_init(): 23 | """Expect proper initialization from arguments.""" 24 | BaseModel() 25 | -------------------------------------------------------------------------------- /tests/unit/test_helpers.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Ensure the expected outcomes of helper functions.""" 17 | 18 | 19 | from structurizr import helpers 20 | 21 | 22 | def test_show_versions(capsys): 23 | """Expect a semi-defined output of package information.""" 24 | helpers.show_versions() 25 | captured = capsys.readouterr() 26 | lines = captured.out.split("\n") 27 | assert lines[1].startswith("System Information") 28 | assert lines[2].startswith("==================") 29 | assert lines[3].startswith("OS") 30 | assert lines[4].startswith("OS-release") 31 | assert lines[5].startswith("Python") 32 | 33 | assert lines[7].startswith("Package Versions") 34 | assert lines[8].startswith("================") 35 | assert any(line.startswith("structurizr-python") for line in lines[9:]) 36 | -------------------------------------------------------------------------------- /tests/unit/test_workspace.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Ensure the expected behaviour of the workspace model.""" 17 | 18 | 19 | import pytest 20 | 21 | from structurizr.workspace import Workspace, WorkspaceIO 22 | 23 | 24 | @pytest.mark.parametrize( 25 | "attributes", 26 | [ 27 | {}, 28 | {"id": 42, "name": "Marvin", "description": "depressed robot"}, 29 | ], 30 | ) 31 | def test_workspace_io_init(attributes: dict): 32 | """Expect proper initialization from arguments.""" 33 | workspace = WorkspaceIO(**attributes) 34 | for attr, expected in attributes.items(): 35 | assert getattr(workspace, attr) == expected 36 | 37 | 38 | @pytest.mark.parametrize( 39 | "attributes", 40 | [ 41 | pytest.param( 42 | {}, 43 | marks=pytest.mark.raises(exception=TypeError), 44 | ), 45 | {"id": 42, "name": "Marvin", "description": "depressed robot"}, 46 | ], 47 | ) 48 | def test_workspace_init(attributes: dict): 49 | """Expect proper initialization from arguments.""" 50 | workspace = Workspace(**attributes) 51 | for attr, expected in attributes.items(): 52 | assert getattr(workspace, attr) == expected 53 | -------------------------------------------------------------------------------- /tests/unit/view/test_color.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, Moritz E. Beber, Ilai Fallach. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Ensure the expected behaviour of the color type.""" 17 | 18 | 19 | import pydantic 20 | import pytest 21 | 22 | from structurizr.view.color import Color 23 | 24 | 25 | @pytest.mark.parametrize( 26 | "value, expected", 27 | [ 28 | ("#ffffff", "#ffffff"), 29 | ("#fff", "#ffffff"), 30 | ("#f0f0f0", "#f0f0f0"), 31 | ("#000", "#000000"), 32 | ("#000000", "#000000"), 33 | ("green", "#008000"), 34 | ("white", "#ffffff"), 35 | pytest.param( 36 | "never-gonna-let-you-down", 37 | "", 38 | marks=pytest.mark.raises( 39 | exception=pydantic.errors.ColorError, 40 | message=( 41 | "value is not a valid color: string not " 42 | "recognised as a valid color" 43 | ), 44 | ), 45 | ), 46 | ], 47 | ) 48 | def test_color_str_value(value: str, expected: str) -> None: 49 | """Expect that the color string value is a six character hex code.""" 50 | assert str(Color(value)) == expected 51 | -------------------------------------------------------------------------------- /tests/unit/view/test_container_view.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # https://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | 14 | """Ensure the expected behaviour of DeploymentView.""" 15 | 16 | 17 | import pytest 18 | 19 | from structurizr.model.model import Model 20 | from structurizr.view.container_view import ContainerView, ContainerViewIO 21 | 22 | 23 | def test_external_system_boundary_preserved(): 24 | """ 25 | Test the externalSoftwareSystemBoundariesVisible flag appears in the JSON. 26 | 27 | Not having this set means that Structurizr assumes it is false (see 28 | https://github.com/Midnighter/structurizr-python/issues/67). When exported 29 | from Structurizr, the flag is present whether true or false, so check that 30 | is also the case here. 31 | """ 32 | view = ContainerView( 33 | key="key", 34 | description="description", 35 | external_software_system_boundary_visible=True, 36 | ) 37 | json = ContainerViewIO.from_orm(view).json() 38 | assert '"externalSoftwareSystemBoundariesVisible": true' in json 39 | 40 | view = ContainerView( 41 | key="key", 42 | description="description", 43 | external_software_system_boundary_visible=False, 44 | ) 45 | json = ContainerViewIO.from_orm(view).json() 46 | assert '"externalSoftwareSystemBoundariesVisible": false' in json 47 | 48 | 49 | # See https://github.com/Midnighter/structurizr-python/issues/79 50 | @pytest.mark.xfail(strict=True) 51 | def test_element_constraints(): 52 | """Test that only valid elements can be added to the view.""" 53 | model = Model() 54 | system = model.add_software_system(name="System 1") 55 | view = ContainerView(key="container1", description="Test", software_system=system) 56 | 57 | with pytest.raises( 58 | ValueError, 59 | match="he software system in scope cannot be added to a container view", 60 | ): 61 | view.add(system) 62 | -------------------------------------------------------------------------------- /tests/unit/view/test_filtered_view.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # https://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | 14 | """Ensure the expected behaviour of FilteredView.""" 15 | 16 | 17 | from structurizr.view.container_view import ContainerView 18 | from structurizr.view.filtered_view import FilteredView, FilteredViewIO, FilterMode 19 | 20 | 21 | def test_uses_view_key_if_view_specified(): 22 | """Test the logic around base_view_key.""" 23 | filtered_view = FilteredView( 24 | base_view_key="key1", description="test", mode=FilterMode.Exclude, tags=[] 25 | ) 26 | assert filtered_view.base_view_key == "key1" 27 | 28 | filtered_view.view = ContainerView(key="static_key", description="container") 29 | assert filtered_view.base_view_key == "static_key" 30 | 31 | 32 | def test_serialisation(): 33 | """Test serialisation and deserialisation works.""" 34 | container_view = ContainerView(key="static_key", description="container") 35 | filtered_view = FilteredView( 36 | key="filter1", 37 | view=container_view, 38 | description="test", 39 | mode=FilterMode.Exclude, 40 | tags=["v1"], 41 | ) 42 | io = FilteredViewIO.from_orm(filtered_view) 43 | view2 = FilteredView.hydrate(io) 44 | 45 | assert view2.base_view_key == "static_key" 46 | assert view2.key == "filter1" 47 | assert view2.description == "test" 48 | assert view2.mode == FilterMode.Exclude 49 | assert view2.tags == {"v1"} 50 | 51 | 52 | def test_tags_are_serialised_as_an_array(): 53 | """Ensure that tags are serialised as an array, not comma-separated.""" 54 | container_view = ContainerView(key="static_key", description="container") 55 | filtered_view = FilteredView( 56 | key="filter1", 57 | view=container_view, 58 | description="test", 59 | mode=FilterMode.Exclude, 60 | tags=["v1", "test"], 61 | ) 62 | io = FilteredViewIO.from_orm(filtered_view).json() 63 | assert '"tags": ["v1", "test"]' in io 64 | -------------------------------------------------------------------------------- /tests/unit/view/test_relationship_view.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # https://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | """Ensure the correct behaviour of RelationshipView.""" 14 | 15 | from structurizr.view.relationship_view import RelationshipView, RelationshipViewIO 16 | 17 | 18 | def test_dynamic_view_specifics_serialise(): 19 | """Ensure that the fields used by DynamicView get (de)serialised OK.""" 20 | view = RelationshipView(id="id1", order="5", response=True) 21 | io = RelationshipViewIO.from_orm(view) 22 | json = io.json() 23 | assert '"order": "5"' in json 24 | assert '"response": true' in json 25 | 26 | view2 = RelationshipView.hydrate(io) 27 | assert view2.order == "5" 28 | assert view2.response 29 | 30 | # Check response is suppressed in json when False 31 | view.response = False 32 | io = RelationshipViewIO.from_orm(view) 33 | json = io.json() 34 | assert "response" not in json 35 | view2 = RelationshipView.hydrate(io) 36 | assert not view2.response 37 | -------------------------------------------------------------------------------- /tests/unit/view/test_sequence_number.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # https://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | """Ensure the correct behaviour of SequenceNumber.""" 14 | 15 | from structurizr.view.sequence_number import SequenceNumber 16 | 17 | 18 | def test_basic_sequence(): 19 | """Test simple incrementing sequence.""" 20 | seq = SequenceNumber() 21 | assert seq.get_next() == "1" 22 | assert seq.get_next() == "2" 23 | assert seq.get_next() == "3" 24 | 25 | 26 | def test_subsequences(): 27 | """Test subsequences.""" 28 | seq = SequenceNumber() 29 | assert seq.get_next() == "1" 30 | 31 | seq.start_subsequence() 32 | assert seq.get_next() == "1.1" 33 | assert seq.get_next() == "1.2" 34 | seq.start_subsequence() 35 | assert seq.get_next() == "1.2.1" 36 | assert seq.get_next() == "1.2.2" 37 | seq.end_subsequence() 38 | assert seq.get_next() == "1.3" 39 | assert seq.get_next() == "1.4" 40 | seq.end_subsequence() 41 | assert seq.get_next() == "2" 42 | 43 | 44 | def test_parallel_sequences(): 45 | """Test "parallel" sequences.""" 46 | seq = SequenceNumber() 47 | assert seq.get_next() == "1" 48 | 49 | seq.start_parallel_sequence() 50 | assert seq.get_next() == "2" 51 | seq.end_parallel_sequence(False) 52 | 53 | seq.start_parallel_sequence() 54 | assert seq.get_next() == "2" 55 | seq.end_parallel_sequence(True) 56 | 57 | assert seq.get_next() == "3" 58 | -------------------------------------------------------------------------------- /tests/unit/view/test_static_view.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """Ensure the expected behaviour of StaticView.""" 17 | 18 | 19 | from structurizr.model import Model, Person, SoftwareSystem 20 | from structurizr.view.static_view import StaticView 21 | 22 | 23 | class DerivedView(StaticView): 24 | """Mock class for testing.""" 25 | 26 | def add_all_elements(self) -> None: 27 | """Stub method because base is abstract.""" 28 | pass 29 | 30 | 31 | def test_add_nearest_neighbours(): 32 | """Test basic behaviour of add_nearest_neighbours.""" 33 | model = Model() 34 | sys1 = model.add_software_system(name="System 1") 35 | sys2 = model.add_software_system(name="System 2") 36 | person = model.add_person(name="Person 1") 37 | sys1.uses(sys2) 38 | person.uses(sys1) 39 | 40 | # Check neighbours from outbound relationships 41 | view = DerivedView(software_system=sys1, description="") 42 | view.add_nearest_neighbours(sys1, SoftwareSystem) 43 | assert any((elt_view.element is sys1 for elt_view in view.element_views)) 44 | assert any((elt_view.element is sys2 for elt_view in view.element_views)) 45 | assert not any((elt_view.element is person for elt_view in view.element_views)) 46 | assert len(view.relationship_views) == 1 47 | 48 | # Check neighbours from inbound relationships 49 | view = DerivedView(software_system=sys1, description="") 50 | view.add_nearest_neighbours(sys2, SoftwareSystem) 51 | assert any((elt_view.element is sys1 for elt_view in view.element_views)) 52 | assert any((elt_view.element is sys2 for elt_view in view.element_views)) 53 | assert not any((elt_view.element is person for elt_view in view.element_views)) 54 | assert len(view.relationship_views) == 1 55 | 56 | 57 | def test_add_nearest_neighbours_doesnt_dupe_relationships(): 58 | """Test relationships aren't duplicated if neighbours added more than once. 59 | 60 | See https://github.com/Midnighter/structurizr-python/issues/63. 61 | """ 62 | model = Model() 63 | sys1 = model.add_software_system(name="System 1") 64 | sys2 = model.add_software_system(name="System 2") 65 | sys1.uses(sys2) 66 | view = DerivedView(software_system=sys1, description="") 67 | view.add_nearest_neighbours(sys1, SoftwareSystem) 68 | assert len(view.relationship_views) == 1 69 | 70 | # The next line should not add any new relationships 71 | view.add_nearest_neighbours(sys1, Person) 72 | assert len(view.relationship_views) == 1 73 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = isort, black, flake8, docs, safety, py3{6,7,8,9} 3 | isolated_build = true 4 | 5 | [gh-actions] 6 | python = 7 | 3.6: py36 8 | 3.7: py37 9 | 3.8: py38 10 | 3.9: py39 11 | 12 | [testenv] 13 | # Always download the latest pip version. This has not been released yet thus the below 14 | # environment variable VIRTUALENV_PIP is needed. See 15 | # https://github.com/tox-dev/tox/issues/1768#issuecomment-787075584 16 | download = true 17 | deps = 18 | pytest 19 | pytest-cov 20 | pytest-mock 21 | pytest-raises 22 | passenv = 23 | STRUCTURIZR_* 24 | commands = 25 | pytest --cov=structurizr --cov-report=term {posargs} 26 | 27 | [testenv:isort] 28 | skip_install = True 29 | deps= 30 | isort 31 | commands= 32 | isort --check-only --diff {toxinidir}/src/structurizr {toxinidir}/tests {toxinidir}/setup.py 33 | 34 | [testenv:black] 35 | skip_install = True 36 | deps= 37 | black 38 | commands= 39 | black --check --diff {toxinidir}/src/structurizr {toxinidir}/tests {toxinidir}/setup.py 40 | 41 | [testenv:flake8] 42 | skip_install = True 43 | deps= 44 | flake8 45 | flake8-docstrings 46 | flake8-bugbear 47 | commands= 48 | flake8 {toxinidir}/src/structurizr {toxinidir}/tests {toxinidir}/setup.py 49 | 50 | [testenv:safety] 51 | deps= 52 | safety 53 | commands= 54 | safety check --full-report 55 | 56 | [testenv:mypy] 57 | skip_install = True 58 | deps= 59 | mypy 60 | commands= 61 | mypy {toxinidir}/src/structurizr {toxinidir}/examples 62 | 63 | [testenv:docs] 64 | deps= 65 | -r{toxinidir}/docs/requirements.txt 66 | commands= 67 | mkdocs build --strict 68 | 69 | ################################################################################ 70 | # Testing tools configuration # 71 | ################################################################################ 72 | 73 | [pytest] 74 | testpaths = 75 | tests 76 | markers = 77 | raises 78 | 79 | [coverage:paths] 80 | source = 81 | src/structurizr 82 | */site-packages/structurizr 83 | 84 | [coverage:run] 85 | branch = true 86 | parallel = true 87 | omit = 88 | src/structurizr/_version.py 89 | 90 | [coverage:report] 91 | exclude_lines = 92 | # Have to re-enable the standard pragma 93 | pragma: no cover 94 | precision = 2 95 | omit = 96 | src/structurizr/_version.py 97 | 98 | [flake8] 99 | max-line-length = 88 100 | exclude = 101 | __init__.py 102 | # The following conflict with `black` which is the more pedantic. 103 | ignore = 104 | E203 105 | W503 106 | D202 107 | 108 | [isort] 109 | skip = 110 | __init__.py 111 | profile = black 112 | lines_after_imports = 2 113 | known_first_party = structurizr 114 | known_third_party = 115 | depinfo 116 | httpx 117 | pydantic 118 | pytest 119 | python-dotenv 120 | setuptools 121 | versioneer 122 | 123 | --------------------------------------------------------------------------------