Skip to content

Commit

Permalink
Merge branch 'dev' into feature/OPT-850
Browse files Browse the repository at this point in the history
  • Loading branch information
daFont-iriusrisk committed May 10, 2023
2 parents 2239c5e + 0dfa56d commit 6e21243
Show file tree
Hide file tree
Showing 35 changed files with 980 additions and 183 deletions.
18 changes: 18 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[run]
source = .
branch = false
omit =
./venv/*
*/tests/*
*__init__.py
setup.py
run_tests.py

[report]
fail_under = 80

[html]
directory = coveragereport

[xml]
output = coveragereport/coverage.xml
69 changes: 69 additions & 0 deletions .github/workflows/sonarcloud.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

# This workflow helps you trigger a SonarCloud analysis of your code and populates
# GitHub Code Scanning alerts with the vulnerabilities found.
# Free for open source project.

name: SonarCloud analysis

on:
pull_request:
branches: [feature/*]
workflow_dispatch:

permissions:
pull-requests: read # allows SonarCloud to decorate PRs with analysis results

jobs:
Analysis:
runs-on: ubuntu-latest
steps:
- name: Checkout the project from Git
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Python 3.8
uses: actions/setup-python@v3
with:
python-version: "3.8"
- name: Setup Graphviz
uses: ts-graphviz/setup-graphviz@v1
- name: Install dependencies
run: pip install -e ".[setup,test]"
- name: Run test using coverage
run: coverage run -m pytest
- name: Generate coverage report
run: coverage xml
- name: Analyze with SonarCloud
# You can pin the exact commit or the version.
# uses: SonarSource/sonarcloud-github-action@commithas or tag
uses: SonarSource/[email protected]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # Generate a token on Sonarcloud.io, add it to the secrets of this repo with the name SONAR_TOKEN (Settings > Secrets > Actions > add new repository secret)
with:
# Additional arguments for the sonarcloud scanner
args:
-Dsonar.projectKey=startleft
-Dsonar.organization=continuumsec
-Dsonar.python.version=3.8,3.9,3.10,3.11
-Dsonar.qualitygate.wait=true
-Dsonar.python.coverage.reportPaths=coveragereport/coverage.xml

# Args explanation
# Unique keys of your project and organization. You can find them in SonarCloud > Information (bottom-left menu)
# mandatory
# -Dsonar.projectKey=
# -Dsonar.organization=

# Version of supported python versions to get a more precise analysis
# -Dsonar.python.version=

# Flag to way for Analysis Quality Gate results, if fail the steps it will be marked as failed too.
# -Dsonar.qualitygate.wait=

# The path for coverage report to use in the SonarCloud analysis, it must be in XML format.
# -Dsonar.python.coverage.reportPaths=
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ coverage.xml
.hypothesis/
.pytest_cache/
test-reports/
/coveragereport/

# SonarLint plugin
.scannerwork

# Translations
*.mo
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"test": [
'tox==4.4.6',
'pytest==7.2.2',
'coverage==7.2.3',
'responses==0.22.0',
'deepdiff==6.2.3',
'httpx==0.23.3',
Expand Down
2 changes: 2 additions & 0 deletions slp_visio/slp_visio/load/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .strategies.connector.impl import connector_identifier_by_connects, create_connector_by_connects, \
create_connector_by_line_coordinates
14 changes: 14 additions & 0 deletions slp_visio/slp_visio/load/connector_identifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from vsdx import Shape

from slp_visio.slp_visio.load.strategies.connector.connector_identifier_strategy import ConnectorIdentifierStrategy


class ConnectorIdentifier:

@staticmethod
def is_connector(shape: Shape) -> bool:
for strategy in ConnectorIdentifierStrategy.get_strategies():
if strategy.is_connector(shape):
return True

return False
50 changes: 7 additions & 43 deletions slp_visio/slp_visio/load/objects/visio_diagram_factories.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,10 @@
from typing import Optional

from slp_visio.slp_visio.load.objects.diagram_objects import DiagramComponent, DiagramConnector
from slp_visio.slp_visio.load.strategies.connector.create_connector_strategy import CreateConnectorStrategy
from slp_visio.slp_visio.util.visio import get_shape_text, get_master_shape_text, normalize_label, get_unique_id_text


# if it has two shapes connected and is not pointing itself
def is_valid_connector(connected_shapes) -> bool:
if len(connected_shapes) < 2:
return False
if connected_shapes[0].shape_id == connected_shapes[1].shape_id:
return False
return True


# if its master name includes 'Double Arrow' or has arrows defined in both ends
def is_bidirectional_connector(shape) -> bool:
if shape.master_page.name is not None and 'Double Arrow' in shape.master_page.name:
return True
for arrow_value in [shape.cell_value(att) for att in ['BeginArrow', 'EndArrow']]:
if arrow_value is None or not str(arrow_value).isnumeric() or arrow_value == '0':
return False
return True


def is_created_from(connector) -> bool:
return connector.from_rel == 'BeginX'


def connector_has_arrow_in_origin(shape) -> bool:
begin_arrow_value = shape.cell_value('BeginArrow')
return begin_arrow_value is not None and str(begin_arrow_value).isnumeric() and begin_arrow_value != '0'


class VisioComponentFactory:

def create_component(self, shape, origin, representer) -> DiagramComponent:
Expand All @@ -46,18 +19,9 @@ def create_component(self, shape, origin, representer) -> DiagramComponent:

class VisioConnectorFactory:

def create_connector(self, shape) -> Optional[DiagramConnector]:
connected_shapes = shape.connects
if not is_valid_connector(connected_shapes):
return None

if is_bidirectional_connector(shape):
return DiagramConnector(shape.ID, connected_shapes[0].shape_id, connected_shapes[1].shape_id, True)

has_arrow_in_origin = connector_has_arrow_in_origin(shape)

if (not has_arrow_in_origin and is_created_from(connected_shapes[0])) \
or (has_arrow_in_origin and is_created_from(connected_shapes[1])):
return DiagramConnector(shape.ID, connected_shapes[0].shape_id, connected_shapes[1].shape_id)
else:
return DiagramConnector(shape.ID, connected_shapes[1].shape_id, connected_shapes[0].shape_id)
@staticmethod
def create_connector(shape) -> Optional[DiagramConnector]:
for strategy in CreateConnectorStrategy.get_strategies():
connector = strategy.create_connector(shape)
if connector:
return connector
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import abc

from vsdx import Shape

from slp_visio.slp_visio.load.strategies.strategy import Strategy


class ConnectorIdentifierStrategy(Strategy):
"""
Formal Interface to check if a shape is a connector
"""

@classmethod
def __subclasshook__(cls, subclass):
return (
hasattr(subclass, 'is_connector') and callable(subclass.process)
or NotImplemented)

@abc.abstractmethod
def is_connector(self, shape: Shape) -> bool:
"""return True if the Shape is a connector"""
raise NotImplementedError
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import abc
from typing import Optional

from vsdx import Shape

from slp_visio.slp_visio.load.objects.diagram_objects import DiagramConnector
from slp_visio.slp_visio.load.strategies.strategy import Strategy


class CreateConnectorStrategy(Strategy):
"""
Formal Interface to create an OTM Dataflow from a vsdx shape
"""

@classmethod
def __subclasshook__(cls, subclass):
return (
hasattr(subclass, 'create_connector') and callable(subclass.process)
or NotImplemented)

@abc.abstractmethod
def create_connector(self, shape: Shape, components=None) -> Optional[DiagramConnector]:
"""creates the OTM Dataflow from the vsdx shape"""
raise NotImplementedError
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from vsdx import Shape

from slp_visio.slp_visio.load.strategies.connector.connector_identifier_strategy import ConnectorIdentifierStrategy


class ConnectorIdentifierByConnects(ConnectorIdentifierStrategy):
"""
Strategy to know if a shape is a connector
The shape must have connects and each connector_shape_id must match with the shape id
"""

def is_connector(self, shape: Shape) -> bool:
for connect in shape.connects:
if shape.ID == connect.connector_shape_id:
return True
return False
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from typing import Optional

from vsdx import Shape

from slp_visio.slp_visio.load.objects.diagram_objects import DiagramConnector
from slp_visio.slp_visio.load.strategies.connector.create_connector_strategy import CreateConnectorStrategy


class CreateConnectorByConnects(CreateConnectorStrategy):
"""
Strategy to create a connector from the shape connects
"""

def create_connector(self, shape: Shape, components=None) -> Optional[DiagramConnector]:
connected_shapes = shape.connects
if not self.are_two_different_shapes(connected_shapes):
return None

if self.is_bidirectional_connector(shape):
return DiagramConnector(shape.ID, connected_shapes[0].shape_id, connected_shapes[1].shape_id, True)

has_arrow_in_origin = self.connector_has_arrow_in_origin(shape)

if (not has_arrow_in_origin and self.is_created_from(connected_shapes[0])) \
or (has_arrow_in_origin and self.is_created_from(connected_shapes[1])):
return DiagramConnector(shape.ID, connected_shapes[0].shape_id, connected_shapes[1].shape_id)
else:
return DiagramConnector(shape.ID, connected_shapes[1].shape_id, connected_shapes[0].shape_id)

# if it has two shapes connected and is not pointing itself
@staticmethod
def are_two_different_shapes(connected_shapes) -> bool:
if len(connected_shapes) < 2:
return False
if connected_shapes[0].shape_id == connected_shapes[1].shape_id:
return False
return True

# if its master name includes 'Double Arrow' or has arrows defined in both ends
@staticmethod
def is_bidirectional_connector(shape) -> bool:
if shape.master_page.name is not None and 'Double Arrow' in shape.master_page.name:
return True
for arrow_value in [shape.cell_value(att) for att in ['BeginArrow', 'EndArrow']]:
if arrow_value is None or not str(arrow_value).isnumeric() or arrow_value == '0':
return False
return True

@staticmethod
def connector_has_arrow_in_origin(shape) -> bool:
begin_arrow_value = shape.cell_value('BeginArrow')
return begin_arrow_value is not None and str(begin_arrow_value).isnumeric() and begin_arrow_value != '0'

@staticmethod
def is_created_from(connector) -> bool:
return connector.from_rel == 'BeginX'
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from typing import Optional

from shapely.geometry import Point
from vsdx import Shape

from slp_visio.slp_visio.load.objects.diagram_objects import DiagramConnector
from slp_visio.slp_visio.load.representation.simple_component_representer import SimpleComponentRepresenter
from slp_visio.slp_visio.load.strategies.connector.create_connector_strategy import CreateConnectorStrategy


class CreateConnectorByLineCoordinates(CreateConnectorStrategy):
"""
Strategy to create a connector from the shape begin and end coordinates
We search a component shape that touch the beginning line coordinates of the given shape connector
We search a component shape that touch the ending line coordinates of the given shape connector
If we find both shapes at the end and at the beginning of the line, we create the connector
"""

def __init__(self):
self.tolerance = 0.09
self.representer: SimpleComponentRepresenter() = SimpleComponentRepresenter()

def create_connector(self, shape: Shape, components=None) -> Optional[DiagramConnector]:
if not shape.begin_x or not shape.begin_y or not shape.end_x or not shape.end_y:
return None
begin_line = Point(shape.begin_x, shape.begin_y)
end_line = Point(shape.end_x, shape.end_y)
if not begin_line or not end_line:
return None

if not components:
return None
origin = self.__match_component(begin_line, components)
target = self.__match_component(end_line, components)

if not origin or not target:
return None

return DiagramConnector(shape.ID, origin, target, name=shape.text)

def __match_component(self, point, components):
for component in components:
polygon = self.representer.build_representation(component)
distance = polygon.exterior.distance(point)
if round(distance, 2) <= self.tolerance:
return component.ID
16 changes: 16 additions & 0 deletions slp_visio/slp_visio/load/strategies/strategy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import abc


class Strategy(metaclass=abc.ABCMeta):
"""
Formal Interface to build a strategy
"""

@classmethod
def get_strategies(cls):
return [obj() for obj in cls.__get_subclasses()]

@classmethod
def __get_subclasses(cls):
for subclass in cls.__subclasses__():
yield subclass
Loading

0 comments on commit 6e21243

Please sign in to comment.