flamapy-rest 2.0.0__tar.gz → 2.6.0.dev1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {flamapy-rest-2.0.0 → flamapy_rest-2.6.0.dev1}/PKG-INFO +18 -13
- flamapy_rest-2.6.0.dev1/flamapy/interfaces/rest/operations_routes.py +197 -0
- {flamapy-rest-2.0.0 → flamapy_rest-2.6.0.dev1}/flamapy_rest.egg-info/PKG-INFO +18 -13
- {flamapy-rest-2.0.0 → flamapy_rest-2.6.0.dev1}/flamapy_rest.egg-info/SOURCES.txt +3 -1
- {flamapy-rest-2.0.0 → flamapy_rest-2.6.0.dev1}/flamapy_rest.egg-info/requires.txt +2 -4
- flamapy_rest-2.6.0.dev1/pyproject.toml +77 -0
- flamapy_rest-2.6.0.dev1/setup.py +3 -0
- flamapy_rest-2.6.0.dev1/tests/test_basic_units.py +180 -0
- flamapy-rest-2.0.0/flamapy/interfaces/rest/operations_routes.py +0 -116
- flamapy-rest-2.0.0/setup.py +0 -37
- {flamapy-rest-2.0.0 → flamapy_rest-2.6.0.dev1}/README.md +0 -0
- {flamapy-rest-2.0.0 → flamapy_rest-2.6.0.dev1}/flamapy_rest.egg-info/dependency_links.txt +0 -0
- {flamapy-rest-2.0.0 → flamapy_rest-2.6.0.dev1}/flamapy_rest.egg-info/top_level.txt +0 -0
- {flamapy-rest-2.0.0 → flamapy_rest-2.6.0.dev1}/setup.cfg +0 -0
|
@@ -1,18 +1,25 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: flamapy-rest
|
|
3
|
-
Version: 2.0.
|
|
4
|
-
Summary: Flamapy feature model is a distribution of the flama framework containing all plugins required to analyze feature models. It also offers a
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
License: UNKNOWN
|
|
9
|
-
Platform: UNKNOWN
|
|
10
|
-
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
12
|
-
Classifier: Operating System :: OS Independent
|
|
3
|
+
Version: 2.6.0.dev1
|
|
4
|
+
Summary: Flamapy feature model is a distribution of the flama framework containing all plugins required to analyze feature models. It also offers a richer API and a complete command line interface and documentation.
|
|
5
|
+
Author-email: Flamapy <flamapy@us.es>
|
|
6
|
+
License-Expression: GPL-3.0-or-later
|
|
7
|
+
Project-URL: Homepage, https://github.com/flamapy/flamapy-rest
|
|
13
8
|
Requires-Python: >=3.9
|
|
14
9
|
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: Flask
|
|
11
|
+
Requires-Dist: gunicorn
|
|
12
|
+
Requires-Dist: flamapy~=2.6.0.dev1
|
|
13
|
+
Requires-Dist: flask-swagger-ui
|
|
14
|
+
Requires-Dist: flask-restplus
|
|
15
|
+
Requires-Dist: flask_cors
|
|
16
|
+
Requires-Dist: flasgger
|
|
15
17
|
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest; extra == "dev"
|
|
19
|
+
Requires-Dist: pytest-mock; extra == "dev"
|
|
20
|
+
Requires-Dist: ruff; extra == "dev"
|
|
21
|
+
Requires-Dist: mypy; extra == "dev"
|
|
22
|
+
Requires-Dist: coverage; extra == "dev"
|
|
16
23
|
|
|
17
24
|
<div align="center">
|
|
18
25
|
|
|
@@ -109,5 +116,3 @@ This script will build, install and deploy the API in http://localhost:8000, you
|
|
|
109
116
|
All the documentation is registered with Swagger UI and OAS 3.0. It is accesible through /api/v1/docs. This documentation is generated dinamically by relyin gon flasgger. DO not forget to document your code in the route files!
|
|
110
117
|
|
|
111
118
|
<p align="right">(<a href="#top">back to top</a>)</p>
|
|
112
|
-
|
|
113
|
-
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import inspect
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
import tempfile
|
|
6
|
+
import typing
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
from flask import Blueprint, request, jsonify, abort
|
|
10
|
+
from werkzeug.datastructures import FileStorage
|
|
11
|
+
from werkzeug.utils import secure_filename
|
|
12
|
+
from flamapy.interfaces.python.flamapy_feature_model import FLAMAFeatureModel, Backend
|
|
13
|
+
from flamapy.core.exceptions import FlamaException
|
|
14
|
+
from flamapy.metamodels.configuration_metamodel.models import Configuration
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
operations_bp = Blueprint('operations_bp', __name__, url_prefix='/api/v1/operations')
|
|
18
|
+
|
|
19
|
+
# Backward-compatible multipart field names for parameters that the API exposed
|
|
20
|
+
# before the dispatcher became generic. Any other parameter derives its field
|
|
21
|
+
# name from the parameter itself (file params drop the trailing "_path").
|
|
22
|
+
_LEGACY_FIELD = {
|
|
23
|
+
'feature_name': 'feature',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Parameters with a constrained set of values, surfaced as a Swagger enum so the
|
|
27
|
+
# UI offers a dropdown instead of a free-text field.
|
|
28
|
+
_ENUM_VALUES = {
|
|
29
|
+
'backend': [b.value for b in Backend],
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_SWAGGER_TYPES = {int: 'integer', float: 'number', bool: 'boolean', str: 'string'}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CustomJSONEncoder(json.JSONEncoder):
|
|
36
|
+
def default(self, obj: Any) -> Any:
|
|
37
|
+
if isinstance(obj, Configuration):
|
|
38
|
+
return obj.__dict__
|
|
39
|
+
return super().default(obj)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _operation_params(method: Any) -> list[inspect.Parameter]:
|
|
43
|
+
"""Call-relevant parameters of a facade method (excluding ``self``)."""
|
|
44
|
+
return [p for p in inspect.signature(method).parameters.values() if p.name != 'self']
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _is_file_param(param: inspect.Parameter) -> bool:
|
|
48
|
+
"""Parameters whose value is a path are supplied as uploaded files."""
|
|
49
|
+
return param.name.endswith('_path')
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _field_name(param: inspect.Parameter) -> str:
|
|
53
|
+
"""The multipart field name a client sends for this parameter."""
|
|
54
|
+
if _is_file_param(param):
|
|
55
|
+
return param.name[: -len('_path')] # configuration_path -> configuration
|
|
56
|
+
return _LEGACY_FIELD.get(param.name, param.name)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _scalar_type(annotation: Any) -> type:
|
|
60
|
+
"""Resolve the scalar type of a (possibly ``Optional[...]``) annotation."""
|
|
61
|
+
if annotation is inspect.Parameter.empty:
|
|
62
|
+
return str
|
|
63
|
+
if typing.get_origin(annotation) is typing.Union:
|
|
64
|
+
non_none = [a for a in typing.get_args(annotation) if a is not type(None)]
|
|
65
|
+
annotation = non_none[0] if non_none else str
|
|
66
|
+
return annotation if annotation in (int, float, bool, str) else str
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _convert(raw: str, scalar_type: type) -> Any:
|
|
70
|
+
if scalar_type is bool:
|
|
71
|
+
return raw.strip().lower() in ('1', 'true', 'yes', 'on')
|
|
72
|
+
if scalar_type is int:
|
|
73
|
+
return int(raw)
|
|
74
|
+
if scalar_type is float:
|
|
75
|
+
return float(raw)
|
|
76
|
+
return raw
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _save_upload(uploaded: FileStorage, work_dir: str) -> str:
|
|
80
|
+
"""Save an uploaded file into ``work_dir`` under a sanitized name and return
|
|
81
|
+
its path. ``secure_filename`` strips any directory components, so a client
|
|
82
|
+
cannot escape ``work_dir`` via crafted names like ``../../etc/passwd``."""
|
|
83
|
+
filename = secure_filename(uploaded.filename or '')
|
|
84
|
+
if not filename:
|
|
85
|
+
abort(400, "Uploaded file has an invalid name")
|
|
86
|
+
path = os.path.join(work_dir, filename)
|
|
87
|
+
uploaded.save(path)
|
|
88
|
+
return path
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _resolve_kwargs(operation: Any, work_dir: str) -> dict[str, Any]:
|
|
92
|
+
"""Build the keyword arguments for a facade method from the request, saving
|
|
93
|
+
any uploaded files into ``work_dir``."""
|
|
94
|
+
kwargs: dict[str, Any] = {}
|
|
95
|
+
for param in _operation_params(operation):
|
|
96
|
+
field = _field_name(param)
|
|
97
|
+
required = param.default is inspect.Parameter.empty
|
|
98
|
+
if _is_file_param(param):
|
|
99
|
+
uploaded = request.files.get(field)
|
|
100
|
+
if uploaded is not None and uploaded.filename:
|
|
101
|
+
kwargs[param.name] = _save_upload(uploaded, work_dir)
|
|
102
|
+
elif required:
|
|
103
|
+
abort(400, f"Missing required file '{field}'")
|
|
104
|
+
else:
|
|
105
|
+
raw = request.form.get(field)
|
|
106
|
+
if raw is not None and raw != '':
|
|
107
|
+
allowed = _ENUM_VALUES.get(param.name)
|
|
108
|
+
if allowed is not None and raw not in allowed:
|
|
109
|
+
abort(400, f"Invalid value for '{field}'; choose from {allowed}")
|
|
110
|
+
try:
|
|
111
|
+
kwargs[param.name] = _convert(raw, _scalar_type(param.annotation))
|
|
112
|
+
except (TypeError, ValueError):
|
|
113
|
+
abort(400, f"Invalid value for '{field}'")
|
|
114
|
+
elif required:
|
|
115
|
+
abort(400, f"Missing required parameter '{field}'")
|
|
116
|
+
return kwargs
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _api_call(operation_name: str) -> Any:
|
|
120
|
+
uploaded_model = request.files.get('model')
|
|
121
|
+
if uploaded_model is None or not uploaded_model.filename:
|
|
122
|
+
abort(400, "Missing required file 'model'")
|
|
123
|
+
|
|
124
|
+
# Each request gets its own directory so concurrent requests cannot collide
|
|
125
|
+
# on (or delete) each other's uploads; the whole tree is removed afterwards.
|
|
126
|
+
work_dir = tempfile.mkdtemp(prefix='flamapy_')
|
|
127
|
+
try:
|
|
128
|
+
model_path = _save_upload(uploaded_model, work_dir)
|
|
129
|
+
try:
|
|
130
|
+
fm = FLAMAFeatureModel(model_path)
|
|
131
|
+
except FlamaException:
|
|
132
|
+
abort(400, "The uploaded model could not be parsed")
|
|
133
|
+
operation = getattr(fm, operation_name)
|
|
134
|
+
kwargs = _resolve_kwargs(operation, work_dir)
|
|
135
|
+
result = operation(**kwargs)
|
|
136
|
+
finally:
|
|
137
|
+
shutil.rmtree(work_dir, ignore_errors=True)
|
|
138
|
+
|
|
139
|
+
if result is None:
|
|
140
|
+
return jsonify(error='Not valid result'), 404
|
|
141
|
+
return jsonify(json.loads(json.dumps(result, cls=CustomJSONEncoder)))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def extract_docstring_with_swagger_info(method: Any) -> str:
|
|
145
|
+
"""Build the flasgger YAML spec for a facade method by introspecting its
|
|
146
|
+
signature: a required ``model`` file plus one field per parameter."""
|
|
147
|
+
lines = [
|
|
148
|
+
" ---",
|
|
149
|
+
" tags:",
|
|
150
|
+
f" - {method.__name__}",
|
|
151
|
+
" consumes:",
|
|
152
|
+
" - multipart/form-data",
|
|
153
|
+
" parameters:",
|
|
154
|
+
" - name: model",
|
|
155
|
+
" in: formData",
|
|
156
|
+
" type: file",
|
|
157
|
+
" required: true",
|
|
158
|
+
]
|
|
159
|
+
for param in _operation_params(method):
|
|
160
|
+
ptype = 'file' if _is_file_param(param) else _SWAGGER_TYPES.get(
|
|
161
|
+
_scalar_type(param.annotation), 'string'
|
|
162
|
+
)
|
|
163
|
+
required = param.default is inspect.Parameter.empty
|
|
164
|
+
lines += [
|
|
165
|
+
f" - name: {_field_name(param)}",
|
|
166
|
+
" in: formData",
|
|
167
|
+
f" type: {ptype}",
|
|
168
|
+
f" required: {str(required).lower()}",
|
|
169
|
+
]
|
|
170
|
+
if param.name in _ENUM_VALUES:
|
|
171
|
+
lines.append(" enum:")
|
|
172
|
+
lines += [f" - {value}" for value in _ENUM_VALUES[param.name]]
|
|
173
|
+
lines += [
|
|
174
|
+
" responses:",
|
|
175
|
+
" 200:",
|
|
176
|
+
" description: Result of the operation",
|
|
177
|
+
]
|
|
178
|
+
docstring = (method.__doc__ or "").replace('\n', ' ').strip()
|
|
179
|
+
return docstring + "\n" + "\n".join(lines) + "\n"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def create_route(operation_name: str, docstring: str) -> Callable[[], Any]:
|
|
183
|
+
def route_function() -> Any:
|
|
184
|
+
return _api_call(operation_name)
|
|
185
|
+
|
|
186
|
+
route_function.__name__ = operation_name
|
|
187
|
+
route_function.__doc__ = docstring
|
|
188
|
+
return route_function
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# Introspect FLAMAFeatureModel to expose every public method as a POST route,
|
|
192
|
+
# generating its Swagger spec from the method signature.
|
|
193
|
+
for name, method in inspect.getmembers(FLAMAFeatureModel, predicate=inspect.isfunction):
|
|
194
|
+
if name.startswith('_'):
|
|
195
|
+
continue
|
|
196
|
+
docstring = extract_docstring_with_swagger_info(method)
|
|
197
|
+
operations_bp.route(f'/{name}', methods=['POST'])(create_route(name, docstring))
|
|
@@ -1,18 +1,25 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: flamapy-rest
|
|
3
|
-
Version: 2.0.
|
|
4
|
-
Summary: Flamapy feature model is a distribution of the flama framework containing all plugins required to analyze feature models. It also offers a
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
License: UNKNOWN
|
|
9
|
-
Platform: UNKNOWN
|
|
10
|
-
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
12
|
-
Classifier: Operating System :: OS Independent
|
|
3
|
+
Version: 2.6.0.dev1
|
|
4
|
+
Summary: Flamapy feature model is a distribution of the flama framework containing all plugins required to analyze feature models. It also offers a richer API and a complete command line interface and documentation.
|
|
5
|
+
Author-email: Flamapy <flamapy@us.es>
|
|
6
|
+
License-Expression: GPL-3.0-or-later
|
|
7
|
+
Project-URL: Homepage, https://github.com/flamapy/flamapy-rest
|
|
13
8
|
Requires-Python: >=3.9
|
|
14
9
|
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: Flask
|
|
11
|
+
Requires-Dist: gunicorn
|
|
12
|
+
Requires-Dist: flamapy~=2.6.0.dev1
|
|
13
|
+
Requires-Dist: flask-swagger-ui
|
|
14
|
+
Requires-Dist: flask-restplus
|
|
15
|
+
Requires-Dist: flask_cors
|
|
16
|
+
Requires-Dist: flasgger
|
|
15
17
|
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest; extra == "dev"
|
|
19
|
+
Requires-Dist: pytest-mock; extra == "dev"
|
|
20
|
+
Requires-Dist: ruff; extra == "dev"
|
|
21
|
+
Requires-Dist: mypy; extra == "dev"
|
|
22
|
+
Requires-Dist: coverage; extra == "dev"
|
|
16
23
|
|
|
17
24
|
<div align="center">
|
|
18
25
|
|
|
@@ -109,5 +116,3 @@ This script will build, install and deploy the API in http://localhost:8000, you
|
|
|
109
116
|
All the documentation is registered with Swagger UI and OAS 3.0. It is accesible through /api/v1/docs. This documentation is generated dinamically by relyin gon flasgger. DO not forget to document your code in the route files!
|
|
110
117
|
|
|
111
118
|
<p align="right">(<a href="#top">back to top</a>)</p>
|
|
112
|
-
|
|
113
|
-
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
README.md
|
|
2
|
+
pyproject.toml
|
|
2
3
|
setup.py
|
|
3
4
|
flamapy/interfaces/rest/operations_routes.py
|
|
4
5
|
flamapy_rest.egg-info/PKG-INFO
|
|
5
6
|
flamapy_rest.egg-info/SOURCES.txt
|
|
6
7
|
flamapy_rest.egg-info/dependency_links.txt
|
|
7
8
|
flamapy_rest.egg-info/requires.txt
|
|
8
|
-
flamapy_rest.egg-info/top_level.txt
|
|
9
|
+
flamapy_rest.egg-info/top_level.txt
|
|
10
|
+
tests/test_basic_units.py
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "flamapy-rest"
|
|
7
|
+
version = "2.6.0.dev1"
|
|
8
|
+
description = "Flamapy feature model is a distribution of the flama framework containing all plugins required to analyze feature models. It also offers a richer API and a complete command line interface and documentation."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "GPL-3.0-or-later"
|
|
11
|
+
authors = [{ name = "Flamapy", email = "flamapy@us.es" }]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"Flask",
|
|
15
|
+
"gunicorn",
|
|
16
|
+
"flamapy~=2.6.0.dev1",
|
|
17
|
+
"flask-swagger-ui",
|
|
18
|
+
"flask-restplus",
|
|
19
|
+
"flask_cors",
|
|
20
|
+
"flasgger",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = [
|
|
25
|
+
"pytest",
|
|
26
|
+
"pytest-mock",
|
|
27
|
+
"ruff",
|
|
28
|
+
"mypy",
|
|
29
|
+
"coverage",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/flamapy/flamapy-rest"
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.packages.find]
|
|
36
|
+
include = ["flamapy.*"]
|
|
37
|
+
|
|
38
|
+
[tool.ruff]
|
|
39
|
+
line-length = 100
|
|
40
|
+
target-version = "py39"
|
|
41
|
+
|
|
42
|
+
[tool.ruff.lint]
|
|
43
|
+
select = ["E", "W", "F", "C", "PL", "RUF"]
|
|
44
|
+
ignore = [
|
|
45
|
+
"RUF012",
|
|
46
|
+
"RUF002",
|
|
47
|
+
"RUF001",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[tool.ruff.lint.mccabe]
|
|
51
|
+
max-complexity = 10
|
|
52
|
+
|
|
53
|
+
[tool.ruff.lint.per-file-ignores]
|
|
54
|
+
"tests/*" = ["ALL"]
|
|
55
|
+
"build/*" = ["ALL"]
|
|
56
|
+
"resources/*" = ["ALL"]
|
|
57
|
+
".mypy_cache/*" = ["ALL"]
|
|
58
|
+
"venv/*" = ["ALL"]
|
|
59
|
+
".venv/*" = ["ALL"]
|
|
60
|
+
"env/*" = ["ALL"]
|
|
61
|
+
"__pycache__/*" = ["ALL"]
|
|
62
|
+
|
|
63
|
+
[tool.mypy]
|
|
64
|
+
explicit_package_bases = true
|
|
65
|
+
scripts_are_modules = true
|
|
66
|
+
show_traceback = true
|
|
67
|
+
check_untyped_defs = true
|
|
68
|
+
disallow_untyped_defs = true
|
|
69
|
+
disallow_any_generics = true
|
|
70
|
+
warn_no_return = true
|
|
71
|
+
strict_optional = true
|
|
72
|
+
no_implicit_optional = true
|
|
73
|
+
warn_redundant_casts = true
|
|
74
|
+
warn_unused_ignores = true
|
|
75
|
+
show_error_codes = true
|
|
76
|
+
ignore_missing_imports = true
|
|
77
|
+
warn_unreachable = true
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import warnings
|
|
3
|
+
import pytest
|
|
4
|
+
from flamapy.interfaces.python.flamapy_feature_model import FLAMAFeatureModel
|
|
5
|
+
|
|
6
|
+
VALID_MODEL = "./resources/models/simple/valid_model.uvl"
|
|
7
|
+
|
|
8
|
+
VALID_CONFIG = "./resources/configurations/valid_configuration.csvconf"
|
|
9
|
+
|
|
10
|
+
def test_atomic_sets() -> None:
|
|
11
|
+
# Prepare
|
|
12
|
+
flamafm=FLAMAFeatureModel(VALID_MODEL)
|
|
13
|
+
expected_result=[['Cart', 'eCommerce', 'Server', 'Web', 'Security', 'Catalog', 'v74', 'Payment', 'Shopping', 'PHP', 'Storage', 'Search'], ['LOW'], ['ENOUGH'], ['BASIC'], ['ADVANCED'], ['PayPal'], ['CreditCard'], ['Mobile'], ['HIGH'], ['STANDARD'], ['Backup'], ['Marketing'], ['SEO'], ['Socials'], ['Twitter'], ['Facebook'], ['YouTube']]
|
|
14
|
+
|
|
15
|
+
# Act
|
|
16
|
+
result = flamafm.atomic_sets()
|
|
17
|
+
|
|
18
|
+
# Assert if the size are the same (this is to speed up the test)
|
|
19
|
+
assert len(result) == len(expected_result)
|
|
20
|
+
|
|
21
|
+
def test_average_branching_factor() -> None:
|
|
22
|
+
# Prepare
|
|
23
|
+
flamafm=FLAMAFeatureModel(VALID_MODEL)
|
|
24
|
+
|
|
25
|
+
# Act
|
|
26
|
+
result = flamafm.average_branching_factor()
|
|
27
|
+
|
|
28
|
+
# Assert
|
|
29
|
+
assert result == 2.45
|
|
30
|
+
|
|
31
|
+
def test_core_features() -> None:
|
|
32
|
+
# Prepare
|
|
33
|
+
flamafm=FLAMAFeatureModel(VALID_MODEL)
|
|
34
|
+
expected_result=['eCommerce', 'Server', 'Web', 'Catalog', 'Search', 'Shopping', 'Security', 'Cart', 'Payment', 'PHP', 'Storage', 'v74']
|
|
35
|
+
# Act
|
|
36
|
+
result = flamafm.core_features()
|
|
37
|
+
|
|
38
|
+
# Assert
|
|
39
|
+
assert sorted(result) == sorted(expected_result)
|
|
40
|
+
|
|
41
|
+
def test_count_leafs() -> None:
|
|
42
|
+
# Prepare
|
|
43
|
+
flamafm=FLAMAFeatureModel(VALID_MODEL)
|
|
44
|
+
|
|
45
|
+
# Act
|
|
46
|
+
result = flamafm.count_leafs()
|
|
47
|
+
|
|
48
|
+
# Assert
|
|
49
|
+
assert result == 17
|
|
50
|
+
|
|
51
|
+
def test_estimated_number_of_configurations() -> None:
|
|
52
|
+
# Prepare
|
|
53
|
+
flamafm=FLAMAFeatureModel(VALID_MODEL)
|
|
54
|
+
|
|
55
|
+
# Act
|
|
56
|
+
result = flamafm.estimated_number_of_configurations()
|
|
57
|
+
|
|
58
|
+
# Assert
|
|
59
|
+
assert result == 1904
|
|
60
|
+
|
|
61
|
+
def test_feature_ancestors() -> None:
|
|
62
|
+
# Prepare
|
|
63
|
+
flamafm=FLAMAFeatureModel(VALID_MODEL)
|
|
64
|
+
|
|
65
|
+
# Act
|
|
66
|
+
result = flamafm.feature_ancestors("v74")
|
|
67
|
+
|
|
68
|
+
# Assert
|
|
69
|
+
assert result == ['PHP', 'Server', 'eCommerce']
|
|
70
|
+
|
|
71
|
+
def test_leaf_features() -> None:
|
|
72
|
+
# Prepare
|
|
73
|
+
flamafm=FLAMAFeatureModel(VALID_MODEL)
|
|
74
|
+
|
|
75
|
+
# Act
|
|
76
|
+
result = flamafm.leaf_features()
|
|
77
|
+
|
|
78
|
+
# Assert
|
|
79
|
+
assert sorted(result) == sorted(['v74', 'LOW', 'ENOUGH', 'Catalog', 'BASIC', 'ADVANCED', 'Cart', 'PayPal', 'CreditCard', 'Mobile', 'HIGH', 'STANDARD', 'Backup', 'SEO', 'Twitter', 'Facebook', 'YouTube'])
|
|
80
|
+
|
|
81
|
+
def test_maxdep() -> None:
|
|
82
|
+
# Prepare
|
|
83
|
+
flamafm=FLAMAFeatureModel(VALID_MODEL)
|
|
84
|
+
|
|
85
|
+
# Act
|
|
86
|
+
result = flamafm.max_depth()
|
|
87
|
+
|
|
88
|
+
# Assert
|
|
89
|
+
assert result == 4
|
|
90
|
+
|
|
91
|
+
def test_dead_features() -> None:
|
|
92
|
+
# Prepare
|
|
93
|
+
flamafm=FLAMAFeatureModel(VALID_MODEL)
|
|
94
|
+
|
|
95
|
+
# Act
|
|
96
|
+
result = flamafm.dead_features()
|
|
97
|
+
|
|
98
|
+
# Assert
|
|
99
|
+
assert sorted(result) == sorted([])
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_false_optional_features() -> None:
|
|
103
|
+
# Prepare
|
|
104
|
+
flamafm=FLAMAFeatureModel(VALID_MODEL)
|
|
105
|
+
|
|
106
|
+
# Act
|
|
107
|
+
result = flamafm.false_optional_features()
|
|
108
|
+
|
|
109
|
+
# Assert
|
|
110
|
+
assert sorted(result) == sorted([])
|
|
111
|
+
|
|
112
|
+
def test_filter() -> None:
|
|
113
|
+
# Prepare
|
|
114
|
+
flamafm=FLAMAFeatureModel(VALID_MODEL)
|
|
115
|
+
|
|
116
|
+
# Act
|
|
117
|
+
result = flamafm.filter(VALID_CONFIG)
|
|
118
|
+
|
|
119
|
+
# Assert
|
|
120
|
+
assert len(result) == 68
|
|
121
|
+
|
|
122
|
+
def test_products_number() -> None:
|
|
123
|
+
# Prepare
|
|
124
|
+
flamafm=FLAMAFeatureModel(VALID_MODEL)
|
|
125
|
+
|
|
126
|
+
# Act
|
|
127
|
+
result = flamafm.configurations_number()
|
|
128
|
+
|
|
129
|
+
# Assert
|
|
130
|
+
assert result == 816
|
|
131
|
+
|
|
132
|
+
def test_products() -> None:
|
|
133
|
+
# Prepare
|
|
134
|
+
flamafm=FLAMAFeatureModel(VALID_MODEL)
|
|
135
|
+
|
|
136
|
+
# Act
|
|
137
|
+
result = flamafm.configurations()
|
|
138
|
+
|
|
139
|
+
# Assert
|
|
140
|
+
assert len(result) == 816
|
|
141
|
+
|
|
142
|
+
def test_commonality() -> None:
|
|
143
|
+
# Prepare
|
|
144
|
+
flamafm=FLAMAFeatureModel(VALID_MODEL)
|
|
145
|
+
|
|
146
|
+
# Act
|
|
147
|
+
result = flamafm.commonality(VALID_CONFIG)
|
|
148
|
+
|
|
149
|
+
# Assert
|
|
150
|
+
assert 1.0 == result
|
|
151
|
+
|
|
152
|
+
def test_valid_configuration() -> None:
|
|
153
|
+
# Prepare
|
|
154
|
+
flamafm=FLAMAFeatureModel(VALID_MODEL)
|
|
155
|
+
|
|
156
|
+
# Act
|
|
157
|
+
result = flamafm.satisfiable_configuration(VALID_CONFIG)
|
|
158
|
+
|
|
159
|
+
# Assert
|
|
160
|
+
assert result == True
|
|
161
|
+
|
|
162
|
+
def test_satisfiable_configuration() -> None:
|
|
163
|
+
# Prepare
|
|
164
|
+
flamafm=FLAMAFeatureModel(VALID_MODEL)
|
|
165
|
+
|
|
166
|
+
# Act
|
|
167
|
+
result = flamafm.satisfiable_configuration(VALID_CONFIG)
|
|
168
|
+
|
|
169
|
+
# Assert
|
|
170
|
+
assert result == True
|
|
171
|
+
|
|
172
|
+
def test_satisfiable() -> None:
|
|
173
|
+
# Prepare
|
|
174
|
+
flamafm=FLAMAFeatureModel(VALID_MODEL)
|
|
175
|
+
|
|
176
|
+
# Act
|
|
177
|
+
result = flamafm.satisfiable()
|
|
178
|
+
|
|
179
|
+
# Assert
|
|
180
|
+
assert result == True
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import inspect
|
|
3
|
-
import json
|
|
4
|
-
|
|
5
|
-
from flask import Blueprint, request, jsonify
|
|
6
|
-
from flamapy.interfaces.python.flamapy_feature_model import FLAMAFeatureModel
|
|
7
|
-
from flamapy.metamodels.configuration_metamodel.models import Configuration
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
operations_bp = Blueprint('operations_bp', __name__, url_prefix='/api/v1/operations')
|
|
11
|
-
|
|
12
|
-
MODEL_FOLDER = './resources/models/'
|
|
13
|
-
|
|
14
|
-
class CustomJSONEncoder(json.JSONEncoder):
|
|
15
|
-
def default(self, obj):
|
|
16
|
-
if isinstance(obj, Configuration):
|
|
17
|
-
return obj.__dict__
|
|
18
|
-
return super().default(obj)
|
|
19
|
-
|
|
20
|
-
def _api_call(operation_name: str):
|
|
21
|
-
# Get files
|
|
22
|
-
uploaded_model = request.files['model']
|
|
23
|
-
|
|
24
|
-
# Check if file is provided
|
|
25
|
-
if uploaded_model.filename != '':
|
|
26
|
-
# Save file
|
|
27
|
-
uploaded_model.save(os.path.join(MODEL_FOLDER, uploaded_model.filename))
|
|
28
|
-
|
|
29
|
-
fm = FLAMAFeatureModel(os.path.join(MODEL_FOLDER, uploaded_model.filename))
|
|
30
|
-
operation = getattr(fm, operation_name)
|
|
31
|
-
|
|
32
|
-
# Extract the method signature
|
|
33
|
-
sig = inspect.signature(operation)
|
|
34
|
-
params = sig.parameters
|
|
35
|
-
|
|
36
|
-
args = []
|
|
37
|
-
for param in params.values():
|
|
38
|
-
if param.name == 'feature_name':
|
|
39
|
-
args.append(request.form['feature'])
|
|
40
|
-
elif param.name == 'configuration_path':
|
|
41
|
-
configuration = request.files['configuration']
|
|
42
|
-
configuration.save(os.path.join(MODEL_FOLDER, configuration.filename))
|
|
43
|
-
args.append(os.path.join(MODEL_FOLDER, configuration.filename))
|
|
44
|
-
|
|
45
|
-
result = operation(*args)
|
|
46
|
-
|
|
47
|
-
# Remove file
|
|
48
|
-
os.remove(os.path.join(MODEL_FOLDER, uploaded_model.filename))
|
|
49
|
-
|
|
50
|
-
if 'configuration' in request.files:
|
|
51
|
-
os.remove(os.path.join(MODEL_FOLDER, request.files['configuration'].filename))
|
|
52
|
-
|
|
53
|
-
# Return result
|
|
54
|
-
if result is None:
|
|
55
|
-
return jsonify(error='Not valid result'), 404
|
|
56
|
-
else:
|
|
57
|
-
return jsonify(json.loads(json.dumps(result, cls=CustomJSONEncoder)))
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def extract_docstring_with_swagger_info(method):
|
|
61
|
-
docstring = method.__doc__ or ""
|
|
62
|
-
parameters = """
|
|
63
|
-
parameters:
|
|
64
|
-
- name: model
|
|
65
|
-
in: formData
|
|
66
|
-
type: file
|
|
67
|
-
required: true
|
|
68
|
-
"""
|
|
69
|
-
|
|
70
|
-
# Extract the method signature
|
|
71
|
-
sig = inspect.signature(method)
|
|
72
|
-
for param in sig.parameters.values():
|
|
73
|
-
if param.name == 'feature_name':
|
|
74
|
-
parameters += """
|
|
75
|
-
- name: feature
|
|
76
|
-
in: formData
|
|
77
|
-
type: string
|
|
78
|
-
required: true
|
|
79
|
-
"""
|
|
80
|
-
elif param.name == 'configuration_path':
|
|
81
|
-
parameters += """
|
|
82
|
-
- name: configuration
|
|
83
|
-
in: formData
|
|
84
|
-
type: file
|
|
85
|
-
required: true
|
|
86
|
-
"""
|
|
87
|
-
|
|
88
|
-
swagger_info = """
|
|
89
|
-
---
|
|
90
|
-
tags:
|
|
91
|
-
- {operation_name}
|
|
92
|
-
{parameters}
|
|
93
|
-
responses:
|
|
94
|
-
200:
|
|
95
|
-
description: Result of the operation
|
|
96
|
-
examples:
|
|
97
|
-
result: Example result
|
|
98
|
-
""".format(operation_name=method.__name__, parameters=parameters)
|
|
99
|
-
# Replace new lines with HTML line breaks
|
|
100
|
-
docstring = docstring.replace('\n', '')
|
|
101
|
-
return docstring + swagger_info
|
|
102
|
-
|
|
103
|
-
def create_route(operation_name: str, docstring: str):
|
|
104
|
-
def route_function():
|
|
105
|
-
return _api_call(operation_name)
|
|
106
|
-
|
|
107
|
-
route_function.__name__ = operation_name
|
|
108
|
-
route_function.__doc__ = docstring
|
|
109
|
-
return route_function
|
|
110
|
-
|
|
111
|
-
# Introspect FLAMAFeatureModel class to find all callable methods and create routes dynamically
|
|
112
|
-
for name, method in inspect.getmembers(FLAMAFeatureModel, predicate=inspect.isfunction):
|
|
113
|
-
if name.startswith('_'):
|
|
114
|
-
continue
|
|
115
|
-
docstring = extract_docstring_with_swagger_info(method)
|
|
116
|
-
operations_bp.route(f'/{name}', methods=['POST'])(create_route(name, docstring))
|
flamapy-rest-2.0.0/setup.py
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import setuptools
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
with open("README.md", "r") as fh:
|
|
5
|
-
long_description = fh.read()
|
|
6
|
-
|
|
7
|
-
def read_requirements(file):
|
|
8
|
-
with open(file, "r") as fh:
|
|
9
|
-
return fh.read().splitlines()
|
|
10
|
-
|
|
11
|
-
# Read requirements from the requirements.txt file
|
|
12
|
-
requirements = read_requirements("requirements.txt")
|
|
13
|
-
|
|
14
|
-
# Read development requirements from the dev-requirements.txt file
|
|
15
|
-
dev_requirements = read_requirements("requirements-dev.txt")
|
|
16
|
-
|
|
17
|
-
setuptools.setup(
|
|
18
|
-
name="flamapy-rest",
|
|
19
|
-
version="2.0.0",
|
|
20
|
-
author="Flamapy",
|
|
21
|
-
author_email="flamapy@us.es",
|
|
22
|
-
description="Flamapy feature model is a distribution of the flama framework containing all plugins required to analyze feature models. It also offers a richier API and a complete command line interface and documentation.",
|
|
23
|
-
long_description=long_description,
|
|
24
|
-
long_description_content_type="text/markdown",
|
|
25
|
-
url="https://github.com/flamapy/flamapy-feature-model",
|
|
26
|
-
packages=setuptools.find_namespace_packages(include=['flamapy.*']),
|
|
27
|
-
classifiers=[
|
|
28
|
-
"Programming Language :: Python :: 3",
|
|
29
|
-
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
|
30
|
-
"Operating System :: OS Independent",
|
|
31
|
-
],
|
|
32
|
-
python_requires='>=3.9',
|
|
33
|
-
install_requires=requirements,
|
|
34
|
-
extras_require={
|
|
35
|
-
'dev': dev_requirements
|
|
36
|
-
}
|
|
37
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|