Skip to content

Commit a3a56d8

Browse files
authored
Merge pull request #55 from cloudblue/feature/LITE-18976_add_extension_validations
LITE-18976: Add validation command to extension projects
2 parents f87e470 + f2e6d48 commit a3a56d8

File tree

13 files changed

+670
-9
lines changed

13 files changed

+670
-9
lines changed

connect/cli/plugins/project/commands.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from connect.cli.plugins.project.extension_helpers import (
88
bootstrap_extension_project,
9+
validate_extension_project,
910
)
1011
from connect.cli.plugins.project.report_helpers import (
1112
add_report,
@@ -53,7 +54,7 @@ def cmd_bootstrap_report_project(output_dir):
5354
type=click.Path(exists=True, file_okay=False, dir_okay=True),
5455
help='Project directory.',
5556
)
56-
def cmd_validate_project(project_dir):
57+
def cmd_validate_report_project(project_dir):
5758
validate_report_project(project_dir)
5859

5960

@@ -100,5 +101,19 @@ def cmd_bootstrap_extension_project(output_dir):
100101
bootstrap_extension_project(output_dir)
101102

102103

104+
@grp_project_extension.command(
105+
name='validate',
106+
short_help='Validate given extension project.',
107+
)
108+
@click.option(
109+
'--project-dir', '-p',
110+
required=True,
111+
type=click.Path(exists=True, file_okay=False, dir_okay=True),
112+
help='Project directory.',
113+
)
114+
def cmd_validate_extension_project(project_dir):
115+
validate_extension_project(project_dir)
116+
117+
103118
def get_group():
104119
return grp_project

connect/cli/plugins/project/constants.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,29 @@
22

33
PROJECT_REPORT_BOILERPLATE_URL = 'https://github.com/cloudblue/connect-report-python-boilerplate.git'
44
PROJECT_EXTENSION_BOILERPLATE_URL = 'https://github.com/cloudblue/connect-extension-python-boilerplate.git'
5+
6+
CAPABILITY_METHOD_MAP = {
7+
'asset_purchase_request_processing': 'process_asset_purchase_request',
8+
'asset_change_request_processing': 'process_asset_change_request',
9+
'asset_suspend_request_processing': 'process_asset_suspend_request',
10+
'asset_resume_request_processing': 'process_asset_resume_request',
11+
'asset_cancel_request_processing': 'process_asset_cancel_request',
12+
'asset_adjustment_request_processing': 'process_asset_adjustment_request',
13+
'asset_purchase_request_validation': 'validate_asset_purchase_request',
14+
'asset_change_request_validation': 'validate_asset_change_request',
15+
'product_action_execution': 'execute_product_action',
16+
'product_custom_event_processing': 'process_product_custom_event',
17+
'tier_config_setup_request_processing': 'process_tier_config_setup_request',
18+
'tier_config_change_request_processing': 'process_tier_config_change_request',
19+
'tier_config_setup_request_validation': 'validate_tier_config_setup_request',
20+
'tier_config_change_request_validation': 'validate_tier_config_change_request',
21+
}
22+
23+
CAPABILITY_ALLOWED_STATUSES = [
24+
'approved',
25+
'draft',
26+
'failed',
27+
'inquiring',
28+
'pending',
29+
'tiers_setup',
30+
]

connect/cli/plugins/project/extension_helpers.py

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1+
import inspect
2+
import json
3+
import os
4+
15
import click
6+
import pkg_resources
7+
import toml
28
from click.exceptions import ClickException
39
from cookiecutter.exceptions import OutputDirExistsException
410
from cookiecutter.main import cookiecutter
511

6-
from connect.cli.plugins.project.constants import PROJECT_EXTENSION_BOILERPLATE_URL
12+
from connect.cli.plugins.project.constants import (
13+
CAPABILITY_ALLOWED_STATUSES,
14+
CAPABILITY_METHOD_MAP,
15+
PROJECT_EXTENSION_BOILERPLATE_URL,
16+
)
717
from connect.cli.plugins.project import utils
818

919

@@ -41,3 +51,110 @@ def bootstrap_extension_project(data_dir: str):
4151
'\nif you would like to use that name, please delete '
4252
'the directory or use another location.',
4353
)
54+
55+
56+
def validate_extension_project(project_dir: str):
57+
click.secho(f'Validating project {project_dir}...\n', fg='blue')
58+
59+
extension_dict = _project_descriptor_validations(project_dir)
60+
_entrypoint_validations(project_dir, extension_dict)
61+
62+
click.secho(f'Extension Project {project_dir} has been successfully validated.', fg='green')
63+
64+
65+
def _project_descriptor_validations(project_dir):
66+
descriptor_file = os.path.join(project_dir, 'pyproject.toml')
67+
if not os.path.isfile(descriptor_file):
68+
raise ClickException(
69+
f'The directory `{project_dir}` does not look like an extension project directory, '
70+
'the mandatory `pyproject.toml` project descriptor file is not present.',
71+
)
72+
try:
73+
data = toml.load(descriptor_file)
74+
except toml.TomlDecodeError:
75+
raise ClickException(
76+
'The extension project descriptor file `pyproject.toml` is not valid.',
77+
)
78+
79+
extension_dict = data['tool']['poetry']['plugins']['connect.eaas.ext']
80+
if not isinstance(extension_dict, dict):
81+
raise ClickException(
82+
'The extension definition on [tool.poetry.plugins."connect.eaas.ext"] `pyproject.toml` section '
83+
'is not well configured. It should be as following: "extension" = "your_package.extension:YourExtension"',
84+
)
85+
if 'extension' not in extension_dict.keys():
86+
raise ClickException(
87+
'The extension definition on [tool.poetry.plugins."connect.eaas.ext"] `pyproject.toml` section '
88+
'does not have "extension" defined. Reminder: "extension" = "your_package.extension:YourExtension"',
89+
)
90+
return extension_dict
91+
92+
93+
def _entrypoint_validations(project_dir, extension_dict):
94+
package_name = extension_dict['extension'].rsplit('.', 1)[0]
95+
descriptor_file = os.path.join(f'{project_dir}/{package_name}', 'extension.json')
96+
ext_class = next(pkg_resources.iter_entry_points('connect.eaas.ext', 'extension'), None)
97+
if not ext_class:
98+
raise ClickException('\nThe extension could not be loaded, Did you execute `poetry install`?')
99+
100+
CustomExtension = ext_class.load()
101+
if not inspect.isclass(CustomExtension):
102+
raise ClickException(f'\nThe extension class {CustomExtension} does not seem a class, please check it')
103+
104+
all_methods = CustomExtension.__dict__
105+
methods = [method for method in all_methods.keys() if not method.startswith('__')]
106+
107+
try:
108+
ext_descriptor = json.load(open(descriptor_file, 'r'))
109+
except json.JSONDecodeError:
110+
raise ClickException(
111+
'\nThe extension descriptor file `extension.json` could not be loaded.',
112+
)
113+
114+
capabilities = ext_descriptor['capabilities']
115+
116+
errors = _have_capabilities_proper_stats(capabilities)
117+
if errors:
118+
raise ClickException(f'Capability errors: {errors}')
119+
120+
if not _have_methods_proper_capabilities(methods, capabilities):
121+
raise ClickException(
122+
'\nThere is some mismatch between capabilities on `extension.json` '
123+
'and the methods defined on your extension class on `extension.py`, '
124+
'please check it.',
125+
)
126+
127+
_have_methods_proper_type(CustomExtension, capabilities)
128+
129+
130+
def _have_methods_proper_type(cls, capabilities):
131+
guess_async = [
132+
inspect.iscoroutinefunction(getattr(cls, CAPABILITY_METHOD_MAP[name]))
133+
for name in capabilities.keys()
134+
]
135+
if all(guess_async):
136+
return
137+
if not any(guess_async):
138+
return
139+
140+
raise ClickException('An Extension class can only have sync or async methods not a mix of both.')
141+
142+
143+
def _have_capabilities_proper_stats(capabilities):
144+
errors = []
145+
for capability, stats in capabilities.items():
146+
if capability == 'product_action_execution' or capability == 'product_custom_event_processing':
147+
if isinstance(stats, list) and len(stats) != 0:
148+
errors.append(f'Capability `{capability}` must not have status.')
149+
continue
150+
if not stats:
151+
errors.append(f'Capability `{capability}` must have at least one allowed status.')
152+
for stat in stats:
153+
if stat not in CAPABILITY_ALLOWED_STATUSES:
154+
errors.append(f'Status `{stat}` on capability `{capability}` is not allowed.')
155+
return errors
156+
157+
158+
def _have_methods_proper_capabilities(methods, capabilities):
159+
capability_list = list(capabilities.keys())
160+
return [CAPABILITY_METHOD_MAP[capability] for capability in capability_list] == methods

docs/linux_deps_install.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,31 @@
33
## Debian/Ubuntu
44

55
```sh
6-
$ sudo apt-get install build-essential python3-dev python3-pip python3-setuptools python3-wheel python3-cffi libcairo2 libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf2.0-0 libffi-dev shared-mime-info
6+
$ sudo apt-get install build-essential python3-dev python3-pip python3-setuptools python3-wheel python3-cffi libcairo2 libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf2.0-0 libffi-dev shared-mime-info git
77
```
88

99
## Fedora
1010

1111
```sh
12-
$ sudo yum install redhat-rpm-config python-devel python-pip python-setuptools python-wheel python-cffi libffi-devel cairo pango gdk-pixbuf2
12+
$ sudo yum install redhat-rpm-config python-devel python-pip python-setuptools python-wheel python-cffi libffi-devel cairo pango gdk-pixbuf2 git
1313
```
1414

1515
## Gentoo
1616

1717
```sh
18-
$ emerge pip setuptools wheel cairo pango gdk-pixbuf cffi
18+
$ emerge pip setuptools wheel cairo pango gdk-pixbuf cffi dev-vcs/git
1919
```
2020

2121
## Archlinux
2222

2323
```sh
24-
$ sudo pacman -S python-pip python-setuptools python-wheel cairo pango gdk-pixbuf2 libffi pkg-config
24+
$ sudo pacman -S python-pip python-setuptools python-wheel cairo pango gdk-pixbuf2 libffi pkg-config git
2525
```
2626

2727
## Alpine
2828

2929
For Alpine Linux 3.6 or newer:
3030

3131
```sh
32-
$ apk --update --upgrade add gcc musl-dev jpeg-dev zlib-dev libffi-dev cairo-dev pango-dev gdk-pixbuf-dev
32+
$ apk --update --upgrade add gcc musl-dev jpeg-dev zlib-dev libffi-dev cairo-dev pango-dev gdk-pixbuf-dev git
3333
```

docs/osx_deps_install.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ Please install Homebrew if you haven't already.
88
## Install
99

1010
```sh
11-
$ brew install python3 cairo pango gdk-pixbuf libffi
11+
$ brew install python3 cairo pango gdk-pixbuf libffi git
1212
```
1313

resources/ccli.spec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ a = Analysis(
2525
'connect.cli.plugins.product.commands',
2626
'connect.cli.plugins.report.commands',
2727
'connect.cli.plugins.play.commands',
28+
'connect.cli.plugins.project.commands',
2829
],
2930
hookspath=[],
3031
runtime_hooks=[],

tests/conftest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import pytest
66
import responses
7+
import toml
78
from fs.tempfs import TempFS
89
from openpyxl import load_workbook
910

@@ -256,3 +257,14 @@ def mocked_reseller():
256257
@pytest.fixture
257258
def customers_workbook(fs):
258259
return load_workbook('./tests/fixtures/customer/customers.xlsx')
260+
261+
262+
@pytest.fixture(scope='function')
263+
def mocked_extension_project_descriptor(fs):
264+
return toml.load('./tests/fixtures/extensions/basic_ext/pyproject.toml')
265+
266+
267+
@pytest.fixture(scope='function')
268+
def mocked_extension_descriptor(fs):
269+
with open('./tests/fixtures/extensions/basic_ext/connect_ext/extension.json') as response:
270+
return json.load(response)

tests/fixtures/extensions/basic_ext/connect_ext/__init__.py

Whitespace-only changes.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "Basic Extension",
3+
"description": "Project description",
4+
"version": "1.0",
5+
"readme_url": "https://example.com/README.md",
6+
"changelog_url": "https://example.com/CHANGELOG.md",
7+
"capabilities": {
8+
"asset_purchase_request_processing": [
9+
"approved"
10+
],
11+
"tier_config_setup_request_processing": [
12+
"tiers_setup",
13+
"pending",
14+
"inquiring",
15+
"approved",
16+
"failed"
17+
],
18+
"product_action_execution": [],
19+
"product_custom_event_processing": []
20+
}
21+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright (c) 2021, Enterprise
4+
# All rights reserved.
5+
#
6+
from connect.eaas.extension import (
7+
Extension,
8+
ProcessingResponse,
9+
ProductActionResponse,
10+
CustomEventResponse,
11+
)
12+
13+
14+
class BasicExtension(Extension):
15+
16+
def process_asset_purchase_request(self, request):
17+
pass
18+
19+
def process_tier_config_setup_request(self, request):
20+
pass
21+
22+
def execute_product_action(self, request):
23+
pass
24+
25+
def process_product_custom_event(self, request):
26+
pass
27+

0 commit comments

Comments
 (0)