Skip to content

Commit

Permalink
feat: using typing library #51
Browse files Browse the repository at this point in the history
Merge pull request #67 from AVATEAM-IT-SYSTEMHAUS/51-using-typing-library

add type checker
  • Loading branch information
oniboni authored Jul 26, 2024
2 parents a94687c + 7299dfe commit e2a1fbd
Show file tree
Hide file tree
Showing 17 changed files with 253 additions and 146 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ jobs:
pip install --upgrade hatch
- name: Lint
run: hatch fmt --check
- name: Check types
run: hatch run types:check
12 changes: 11 additions & 1 deletion .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ env:
FORCE_COLOR: "1"

jobs:
deploy-test:
deploy:

runs-on: ubuntu-latest

Expand All @@ -27,6 +27,16 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install --upgrade hatch
- name: Check project version
run: |
PROJECT_VERSION=$(TERM=dumb hatch version)
if [ "$PROJECT_VERSION" != "${TAG_NAME#v}" ]; then
echo "Wrong project version, cannot release: $PROJECT_VERSION, ${TAG_NAME#v} expected"
exit 1
fi
env:
TAG_NAME: ${{ github.ref }}
- name: Build package
run: hatch build
- name: Publish package
Expand Down
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ repos:
language: system
pass_filenames: false
types: [ python ]
- id: check-types
name: run mypy
entry: hatch run types:check
language: system
pass_filenames: false
types: [ python ]
- id: format
name: run the formatter
entry: hatch fmt -f
Expand Down
19 changes: 19 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
services:
kroki:
image: docker.io/yuzutech/kroki
depends_on:
- mermaid
- bpmn
- excalidraw
environment:
- KROKI_MERMAID_HOST=mermaid
- KROKI_BPMN_HOST=bpmn
- KROKI_EXCALIDRAW_HOST=excalidraw
ports:
- "127.0.0.1:8080:8000"
mermaid:
image: docker.io/yuzutech/kroki-mermaid
bpmn:
image: docker.io/yuzutech/kroki-bpmn
excalidraw:
image: docker.io/yuzutech/kroki-excalidraw
4 changes: 2 additions & 2 deletions kroki/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ class DownloadedContent:
def _ugly_temp_excalidraw_fix(self) -> None:
"""TODO: remove me, when excalidraw container works again..
ref: https://github.com/excalidraw/excalidraw/issues/7366"""
self.file_content = self.file_content.replace(
self.file_content: bytes = self.file_content.replace(
b"https://unpkg.com/@excalidraw/excalidraw@undefined/dist",
b"https://unpkg.com/@excalidraw/[email protected]/dist",
)

def __init__(self, file_content: bytes, file_extension: str, additional_metadata: None | dict) -> None:
file_uuid = uuid3(NAMESPACE_OID, f"{additional_metadata}{file_content}")
file_uuid = uuid3(NAMESPACE_OID, f"{additional_metadata}{file_content!r}")

self.file_name = f"{FILE_PREFIX}{file_uuid}.{file_extension}"
self.file_content = file_content
Expand Down
5 changes: 4 additions & 1 deletion kroki/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@


class DeprecatedDownloadImagesCompat(config_options.Deprecated):
def pre_validation(self, config: "KrokiPluginConfig", key_name: str) -> None:
def pre_validation(self, config: MkDocsBaseConfig, key_name: str) -> None:
"""Set `HttpMethod: 'POST'`, if enabled"""
if not isinstance(config, KrokiPluginConfig):
return

if config.get(key_name) is None:
return

Expand Down
83 changes: 53 additions & 30 deletions kroki/diagram_types.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from collections import ChainMap
from typing import ClassVar

from mkdocs.exceptions import PluginError

from kroki.logging import log


class KrokiDiagramTypes:
_kroki_base: ClassVar[dict[str, list[str]]] = {
_base_diagrams: ClassVar[dict[str, list[str]]] = {
"bytefield": ["svg"],
"ditaa": ["png", "svg"],
"erd": ["png", "svg", "jpeg", "pdf"],
Expand All @@ -27,7 +29,7 @@ class KrokiDiagramTypes:
"wireviz": ["png", "svg"],
}

_kroki_blockdiag: ClassVar[dict[str, list[str]]] = {
_blockdiag: ClassVar[dict[str, list[str]]] = {
"blockdiag": ["png", "svg", "pdf"],
"seqdiag": ["png", "svg", "pdf"],
"actdiag": ["png", "svg", "pdf"],
Expand All @@ -36,19 +38,19 @@ class KrokiDiagramTypes:
"rackdiag": ["png", "svg", "pdf"],
}

_kroki_bpmn: ClassVar[dict[str, list[str]]] = {
_bpmn: ClassVar[dict[str, list[str]]] = {
"bpmn": ["svg"],
}

_kroki_excalidraw: ClassVar[dict[str, list[str]]] = {
_excalidraw: ClassVar[dict[str, list[str]]] = {
"excalidraw": ["svg"],
}

_kroki_mermaid: ClassVar[dict[str, list[str]]] = {
_mermaid: ClassVar[dict[str, list[str]]] = {
"mermaid": ["png", "svg"],
}

_kroki_diagramsnet: ClassVar[dict[str, list[str]]] = {
_diagramsnet: ClassVar[dict[str, list[str]]] = {
"diagramsnet": ["svg"],
}

Expand All @@ -66,39 +68,60 @@ def __init__(
):
self._fence_prefix = fence_prefix

kroki_types = ChainMap(
self._kroki_base,
self._kroki_blockdiag if blockdiag_enabled else {},
self._kroki_bpmn if bpmn_enabled else {},
self._kroki_excalidraw if excalidraw_enabled else {},
self._kroki_mermaid if mermaid_enabled else {},
self._kroki_diagramsnet if diagramsnet_enabled else {},
diagram_type_file_ext_map = ChainMap(
self._base_diagrams,
self._blockdiag if blockdiag_enabled else {},
self._bpmn if bpmn_enabled else {},
self._excalidraw if excalidraw_enabled else {},
self._mermaid if mermaid_enabled else {},
self._diagramsnet if diagramsnet_enabled else {},
)

self._file_ext_mapping: dict[str, str] = self._get_file_ext_mapping(
diagram_type_file_ext_map, file_types, file_type_overrides
)

self._kroki_types_supporting_file = {}
log.debug("File and Diagram types configured: %s", self._file_ext_mapping)

for kroki_type, kroki_file_types in kroki_types.items():
kroki_file_type = next(filter(lambda file: file in kroki_file_types, file_types), None)
if kroki_file_type is not None:
self._kroki_types_supporting_file[kroki_type] = next(
filter(lambda file: file in kroki_file_types, file_types), None
def _get_file_ext_mapping(
self,
diagram_type_file_ext_map: ChainMap[str, list[str]],
file_types: list[str],
file_type_overrides: dict[str, str],
) -> dict[str, str]:
def get_file_type(diagram_type: str) -> str:
supported_file_types = diagram_type_file_ext_map[diagram_type]
file_type_override = file_type_overrides.get(diagram_type)
if file_type_override is not None:
if file_type_override not in supported_file_types:
err_msg = (
f"{diagram_type}: {file_type_override} not in supported file types: " f"{supported_file_types}"
)
raise PluginError(err_msg)
return file_type_override

target_file_type = next((t for t in file_types if t in supported_file_types), None)
if target_file_type is None:
err_msg = (
f"{diagram_type}: Not able to satisfy any of {file_types}, "
f"supported file types: {supported_file_types}"
)
raise PluginError(err_msg)

for kroki_type, kroki_file_type in file_type_overrides.items():
self._kroki_types_supporting_file[kroki_type] = kroki_file_type
return target_file_type

log.debug("File and Diagram types configured: %s", self._kroki_types_supporting_file)
return {diagram_type: get_file_type(diagram_type) for diagram_type in diagram_type_file_ext_map}

def get_file_ext(self, kroki_type: str) -> str:
return self._kroki_types_supporting_file[kroki_type]
return self._file_ext_mapping[kroki_type]

def get_kroki_type(self, block_type: None | str) -> None | str:
def get_kroki_type(self, block_type: None | str) -> str | None:
if block_type is None:
return
return None
if not block_type.startswith(self._fence_prefix):
return
kroki_type = block_type.removeprefix(self._fence_prefix).lower()
if kroki_type not in self._kroki_types_supporting_file:
return
return None
diagram_type = block_type.removeprefix(self._fence_prefix).lower()
if diagram_type not in self._file_ext_mapping:
return None

return kroki_type
return diagram_type
3 changes: 2 additions & 1 deletion kroki/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ def _get_media_type(self, file_ext: str) -> str:
case "pdf":
return "application/pdf"
case _:
raise NotImplementedError(file_ext)
err_msg = f"Not implemented: '{file_ext}"
raise PluginError(err_msg)

def _image_response(self, image_src: ImageSrc) -> str:
media_type = self._get_media_type(image_src.file_ext)
Expand Down
17 changes: 15 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,14 @@ classifiers = [
"License :: OSI Approved :: MIT License",
"Programming Language :: Python",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"mkdocs>=1.5.0",
"requests>=2.27.0",
"result",
"result>=0.17.0",
]

[project.entry-points."mkdocs.plugins"]
Expand Down Expand Up @@ -68,6 +67,8 @@ include = [
[tool.hatch.envs.test]
template = "hatch-test"
[tool.hatch.envs.test.env-vars]
# start local test kroki:
# docker-compose up
KROKI_SERVER_URL = "http://localhost:8080"

[tool.hatch.envs.hatch-test]
Expand All @@ -87,3 +88,15 @@ cov-report = [
"coverage xml",
"coverage report --omit='tests/*'",
]

[tool.hatch.envs.types]
template = "hatch-test"
extra-dependencies = [
"mypy",
"types-PyYAML",
"types-beautifulsoup4",
"types-requests",
"types-babel",
]
[tool.hatch.envs.types.scripts]
check = "mypy --install-types --non-interactive {args:kroki tests}"
3 changes: 3 additions & 0 deletions tests/data/happy_path/docs/assets/diagram.plantuml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@startuml
Bob -> Alice : hello
@enduml
8 changes: 8 additions & 0 deletions tests/data/happy_path/docs/index.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Test

## inline

```c4plantuml
!include <C4/C4_Context>
!include <C4/C4_Container>
Expand Down Expand Up @@ -29,3 +31,9 @@ Rel(site_host, db, "read")
Rel(site_host, reader, "serve")
```

## from file

```plantuml
@from_file:assets/diagram.plantuml
```
42 changes: 26 additions & 16 deletions tests/test_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,56 +4,66 @@
from tests.utils import MkDocsHelper, get_expected_log_line


def _assert_error_block(err_msg: str, index_html: str):
index_soup = bs4.BeautifulSoup(index_html)
details_tag = index_soup.find("details")
assert isinstance(details_tag, bs4.Tag), "Error message container not in resulting HTML"
summary_tag = details_tag.summary
assert isinstance(summary_tag, bs4.Tag), "Error message container has no summary element"
assert err_msg in summary_tag.text


@pytest.mark.usefixtures("kroki_timeout")
def test_request_timeout() -> None:
# Arrange
with MkDocsHelper("happy_path") as mkdocs_helper:
mkdocs_helper.set_http_method("POST")

# Act
result = mkdocs_helper.invoke_build()

# Assert
assert result.exit_code == 0
assert get_expected_log_line("Request error") in result.output
with open(mkdocs_helper.test_dir / "site/index.html") as index_html_file:
index_soup = bs4.BeautifulSoup(index_html_file.read())
assert "Request error" in index_soup.find("details").summary.text
_assert_error_block("Request error", index_html_file.read())


@pytest.mark.usefixtures("kroki_bad_request")
def test_request_bad_request() -> None:
# Arrange
with MkDocsHelper("happy_path") as mkdocs_helper:
mkdocs_helper.set_http_method("POST")

# Act
result = mkdocs_helper.invoke_build()

# Assert
assert result.exit_code == 0
assert get_expected_log_line("Diagram error!") in result.output
with open(mkdocs_helper.test_dir / "site/index.html") as index_html_file:
index_soup = bs4.BeautifulSoup(index_html_file.read())
assert "Diagram error!" in index_soup.find("details").summary.text
_assert_error_block("Diagram error!", index_html_file.read())


@pytest.mark.usefixtures("kroki_is_a_teapot")
def test_request_other_error() -> None:
# Arrange
with MkDocsHelper("happy_path") as mkdocs_helper:
mkdocs_helper.set_http_method("POST")

# Act
result = mkdocs_helper.invoke_build()

# Assert
assert result.exit_code == 0
assert get_expected_log_line("Could not retrieve image data") in result.output
with open(mkdocs_helper.test_dir / "site/index.html") as index_html_file:
index_soup = bs4.BeautifulSoup(index_html_file.read())
assert "Could not retrieve image data" in index_soup.find("details").summary.text
_assert_error_block("Could not retrieve image data", index_html_file.read())


@pytest.mark.usefixtures("kroki_dummy")
def test_missing_file_from() -> None:
def test_missing_from_file() -> None:
# Arrange
with MkDocsHelper("missing_from_file") as mkdocs_helper:
mkdocs_helper.set_http_method("POST")
# Act
result = mkdocs_helper.invoke_build()

# Assert
assert result.exit_code == 0
assert get_expected_log_line("Can't read file:") in result.output
with open(mkdocs_helper.test_dir / "site/index.html") as index_html_file:
index_soup = bs4.BeautifulSoup(index_html_file.read())
assert "Can't read file: " in index_soup.find("details").summary.text
_assert_error_block("Can't read file: ", index_html_file.read())
Loading

0 comments on commit e2a1fbd

Please sign in to comment.