├── .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 |
--------------------------------------------------------------------------------