flamapy-rest 2.0.0__tar.gz → 2.6.0.dev2__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.
@@ -1,18 +1,25 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: flamapy-rest
3
- Version: 2.0.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 richier API and a complete command line interface and documentation.
5
- Home-page: https://github.com/flamapy/flamapy-feature-model
6
- Author: Flamapy
7
- Author-email: flamapy@us.es
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
13
- Requires-Python: >=3.9
3
+ Version: 2.6.0.dev2
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
8
+ Requires-Python: >=3.11
14
9
  Description-Content-Type: text/markdown
10
+ Requires-Dist: Flask
11
+ Requires-Dist: gunicorn
12
+ Requires-Dist: flamapy~=2.6.0.dev2
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
1
+ Metadata-Version: 2.4
2
2
  Name: flamapy-rest
3
- Version: 2.0.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 richier API and a complete command line interface and documentation.
5
- Home-page: https://github.com/flamapy/flamapy-feature-model
6
- Author: Flamapy
7
- Author-email: flamapy@us.es
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
13
- Requires-Python: >=3.9
3
+ Version: 2.6.0.dev2
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
8
+ Requires-Python: >=3.11
14
9
  Description-Content-Type: text/markdown
10
+ Requires-Dist: Flask
11
+ Requires-Dist: gunicorn
12
+ Requires-Dist: flamapy~=2.6.0.dev2
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
@@ -1,16 +1,14 @@
1
- wheel
2
1
  Flask
3
2
  gunicorn
4
- flamapy==2.0.0
3
+ flamapy~=2.6.0.dev2
5
4
  flask-swagger-ui
6
5
  flask-restplus
7
- pytest
8
6
  flask_cors
9
7
  flasgger
10
8
 
11
9
  [dev]
12
10
  pytest
13
11
  pytest-mock
14
- prospector
12
+ ruff
15
13
  mypy
16
14
  coverage
@@ -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.dev2"
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.11"
13
+ dependencies = [
14
+ "Flask",
15
+ "gunicorn",
16
+ "flamapy~=2.6.0.dev2",
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 = "py311"
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,3 @@
1
+ from setuptools import setup
2
+
3
+ setup()
@@ -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))
@@ -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
- )