flamapy-rest 2.6.0.dev1__tar.gz → 2.6.0.dev6__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.6.0.dev6/PKG-INFO +218 -0
- flamapy_rest-2.6.0.dev6/README.md +193 -0
- flamapy_rest-2.6.0.dev6/flamapy/interfaces/rest/config.py +36 -0
- flamapy_rest-2.6.0.dev6/flamapy/interfaces/rest/extensions.py +52 -0
- {flamapy_rest-2.6.0.dev1 → flamapy_rest-2.6.0.dev6}/flamapy/interfaces/rest/operations_routes.py +73 -10
- flamapy_rest-2.6.0.dev6/flamapy/interfaces/rest/runner.py +70 -0
- flamapy_rest-2.6.0.dev6/flamapy_rest.egg-info/PKG-INFO +218 -0
- {flamapy_rest-2.6.0.dev1 → flamapy_rest-2.6.0.dev6}/flamapy_rest.egg-info/SOURCES.txt +4 -0
- {flamapy_rest-2.6.0.dev1 → flamapy_rest-2.6.0.dev6}/flamapy_rest.egg-info/requires.txt +3 -1
- {flamapy_rest-2.6.0.dev1 → flamapy_rest-2.6.0.dev6}/pyproject.toml +6 -4
- flamapy_rest-2.6.0.dev6/tests/test_api.py +117 -0
- flamapy_rest-2.6.0.dev1/PKG-INFO +0 -118
- flamapy_rest-2.6.0.dev1/README.md +0 -95
- flamapy_rest-2.6.0.dev1/flamapy_rest.egg-info/PKG-INFO +0 -118
- {flamapy_rest-2.6.0.dev1 → flamapy_rest-2.6.0.dev6}/flamapy_rest.egg-info/dependency_links.txt +0 -0
- {flamapy_rest-2.6.0.dev1 → flamapy_rest-2.6.0.dev6}/flamapy_rest.egg-info/top_level.txt +0 -0
- {flamapy_rest-2.6.0.dev1 → flamapy_rest-2.6.0.dev6}/setup.cfg +0 -0
- {flamapy_rest-2.6.0.dev1 → flamapy_rest-2.6.0.dev6}/setup.py +0 -0
- {flamapy_rest-2.6.0.dev1 → flamapy_rest-2.6.0.dev6}/tests/test_basic_units.py +0 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flamapy-rest
|
|
3
|
+
Version: 2.6.0.dev6
|
|
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
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: Flask
|
|
11
|
+
Requires-Dist: gunicorn
|
|
12
|
+
Requires-Dist: flamapy~=2.6.0.dev5
|
|
13
|
+
Requires-Dist: flask-swagger-ui
|
|
14
|
+
Requires-Dist: flask-restplus
|
|
15
|
+
Requires-Dist: flask_cors
|
|
16
|
+
Requires-Dist: flasgger
|
|
17
|
+
Requires-Dist: Flask-Limiter
|
|
18
|
+
Requires-Dist: cachetools
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-mock; extra == "dev"
|
|
22
|
+
Requires-Dist: ruff; extra == "dev"
|
|
23
|
+
Requires-Dist: mypy; extra == "dev"
|
|
24
|
+
Requires-Dist: coverage; extra == "dev"
|
|
25
|
+
|
|
26
|
+
<div align="center">
|
|
27
|
+
|
|
28
|
+
<a href="https://github.com/flamapy/flamapy-rest/actions/workflows/main.yml"></a>
|
|
29
|
+
<a href="https://github.com/flamapy/flamapy-rest/actions/workflows/conventionalpr.yml"></a>
|
|
30
|
+
<a href="https://github.com/flamapy/flamapy-rest/actions/workflows/docker-image.yml"></a>
|
|
31
|
+
<a href="https://pypi.org/project/flamapy-rest/"></a>
|
|
32
|
+
<a href="https://hub.docker.com/r/flamapy/flamapy-rest"></a>
|
|
33
|
+
<a href="https://pypi.org/project/flamapy-rest/"></a>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div id="top"></div>
|
|
37
|
+
<br />
|
|
38
|
+
<div align="center">
|
|
39
|
+
|
|
40
|
+
<h3 align="center">FLAMAPY REST API</h3>
|
|
41
|
+
|
|
42
|
+
<p align="center">
|
|
43
|
+
A new and easy way to use FLAMA
|
|
44
|
+
<br />
|
|
45
|
+
<a href="https://github.com/flamapy/flamapy-rest/issues">Report Bug</a>
|
|
46
|
+
·
|
|
47
|
+
<a href="https://github.com/flamapy/flamapy-rest/issues">Request Feature</a>
|
|
48
|
+
</p>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<!-- ABOUT THE PROJECT -->
|
|
52
|
+
## About The Project
|
|
53
|
+
|
|
54
|
+
`flamapy-rest` is a [Flask](https://flask.palletsprojects.com/) REST API that wraps the
|
|
55
|
+
[FLAMAPY](https://flamapy.github.io/) framework for **feature model analysis**. It dynamically
|
|
56
|
+
exposes FLAMAPY operations (dead features, false-optional features, configurations, satisfiability,
|
|
57
|
+
diagnosis, …) as HTTP endpoints, with self-generated [Swagger](https://swagger.io/) documentation.
|
|
58
|
+
|
|
59
|
+
Feature model analysis has a crucial role in software product line engineering, enabling us to
|
|
60
|
+
understand, design, and validate the complex relationships among features in a software product
|
|
61
|
+
line. These models can be complex and challenging to analyze due to their variability, making it
|
|
62
|
+
difficult to identify conflicts, dead features, and potential optimizations. This REST API makes
|
|
63
|
+
that analysis available to any tool or language that can speak HTTP.
|
|
64
|
+
|
|
65
|
+
Please note: this is a living document and we will continue to update and improve it as we release
|
|
66
|
+
new versions of the plugins and receive feedback from our users. If there's anything you don't
|
|
67
|
+
understand or if you have any suggestions for improvement, don't hesitate to
|
|
68
|
+
[open an issue](https://github.com/flamapy/flamapy-rest/issues). We're here to help!
|
|
69
|
+
|
|
70
|
+
### Built With
|
|
71
|
+
|
|
72
|
+
* [Docker](https://www.docker.com/)
|
|
73
|
+
* [Flask](https://flask.palletsprojects.com/)
|
|
74
|
+
* [FLAMAPY](https://github.com/flamapy)
|
|
75
|
+
* [Flasgger](https://github.com/flasgger/flasgger)
|
|
76
|
+
|
|
77
|
+
<p align="right">(<a href="#top">back to top</a>)</p>
|
|
78
|
+
|
|
79
|
+
<!-- GETTING STARTED -->
|
|
80
|
+
## Getting Started
|
|
81
|
+
|
|
82
|
+
There are three supported ways to run the API. **Using the published Docker image is the easiest** —
|
|
83
|
+
nothing to build, and it already bundles FLAMAPY and all of its analysis plugins.
|
|
84
|
+
|
|
85
|
+
### Option 1 — Run the published Docker image (recommended)
|
|
86
|
+
|
|
87
|
+
The CI publishes a multi-arch image (`linux/amd64` and `linux/arm64`) to Docker Hub at
|
|
88
|
+
[`flamapy/flamapy-rest`](https://hub.docker.com/r/flamapy/flamapy-rest) on every version tag.
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Pull and run the latest release
|
|
92
|
+
docker run --rm -p 8000:8000 flamapy/flamapy-rest
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Then open:
|
|
96
|
+
|
|
97
|
+
* API home: <http://localhost:8000/>
|
|
98
|
+
* Swagger UI / API docs: <http://localhost:8000/docs/>
|
|
99
|
+
|
|
100
|
+
To pin a specific version, use its tag (matching a release, e.g. `v2.6.0`):
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
docker run --rm -p 8000:8000 flamapy/flamapy-rest:v2.6.0
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Run it detached and give the container a name so it's easy to stop later:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
docker run -d --name flamapy-rest -p 8000:8000 flamapy/flamapy-rest
|
|
110
|
+
docker logs -f flamapy-rest # follow the logs
|
|
111
|
+
docker stop flamapy-rest # stop it
|
|
112
|
+
docker rm flamapy-rest # remove it
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Option 2 — Build the Docker image from source
|
|
116
|
+
|
|
117
|
+
If you want to run your local changes, clone the repo and build the image yourself. A helper script
|
|
118
|
+
is provided that builds, runs, and (on exit) cleans up the container and image:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# Linux / macOS
|
|
122
|
+
./start-server.sh
|
|
123
|
+
|
|
124
|
+
# Windows
|
|
125
|
+
start-server.cmd
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Or do it by hand:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
docker build --tag flamapy-rest .
|
|
132
|
+
docker run --rm -p 8000:8000 flamapy-rest
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Option 3 — Install from PyPI and run locally
|
|
136
|
+
|
|
137
|
+
The API is also published on PyPI as [`flamapy-rest`](https://pypi.org/project/flamapy-rest/)
|
|
138
|
+
(requires **Python 3.11+**). Because the WSGI entrypoint lives in `app.py`, clone the repo to get it:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
git clone https://github.com/flamapy/flamapy-rest.git
|
|
142
|
+
cd flamapy-rest
|
|
143
|
+
pip install .
|
|
144
|
+
|
|
145
|
+
# Production server
|
|
146
|
+
gunicorn --bind 0.0.0.0:8000 app:app
|
|
147
|
+
|
|
148
|
+
# …or the Flask development server
|
|
149
|
+
python -m flask run --host=0.0.0.0
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
<p align="right">(<a href="#top">back to top</a>)</p>
|
|
153
|
+
|
|
154
|
+
## Usage
|
|
155
|
+
|
|
156
|
+
Every public operation of FLAMAPY's `FLAMAFeatureModel` facade is exposed as a `POST` endpoint under
|
|
157
|
+
`/api/v1/operations/<operation_name>`. Each call uploads a feature model file (e.g. UVL) as
|
|
158
|
+
multipart form data; some operations also accept an optional `feature` or `configuration` parameter.
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
# Example: count the number of valid configurations of a model
|
|
162
|
+
curl -X POST http://localhost:8000/api/v1/operations/configurations_number \
|
|
163
|
+
-F "model=@resources/models/simple/valid_model.uvl"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The full, interactive list of endpoints — with parameters and "try it out" support — is available
|
|
167
|
+
in the Swagger UI at <http://localhost:8000/docs/>.
|
|
168
|
+
|
|
169
|
+
<p align="right">(<a href="#top">back to top</a>)</p>
|
|
170
|
+
|
|
171
|
+
## Abuse controls & configuration
|
|
172
|
+
|
|
173
|
+
The API ships with per-IP rate limiting, a per-operation time budget, upload size limits and a
|
|
174
|
+
result cache, all configurable through environment variables (e.g. `docker run -e FLAMAPY_...`):
|
|
175
|
+
|
|
176
|
+
| Env var | Default | Meaning |
|
|
177
|
+
|---|---|---|
|
|
178
|
+
| `FLAMAPY_MAX_CONTENT_LENGTH` | `16777216` | Max upload size in bytes; larger requests get **413** |
|
|
179
|
+
| `FLAMAPY_RATELIMIT_ENABLED` | `true` | Turn rate limiting on/off |
|
|
180
|
+
| `FLAMAPY_RATELIMIT_DEFAULT` | `60 per minute` | Per-IP limit for cheap operations |
|
|
181
|
+
| `FLAMAPY_RATELIMIT_EXPENSIVE` | `10 per minute` | Per-IP limit for enumeration/solver-heavy operations (`configurations`, `configurations_number`, `sampling`, …); exceeding a limit returns **429** |
|
|
182
|
+
| `FLAMAPY_RATELIMIT_STORAGE_URI` | `memory://` | Limiter storage; use `redis://host:6379` when running several gunicorn workers so they share counters |
|
|
183
|
+
| `FLAMAPY_OPERATION_TIMEOUT` | `60` | Seconds an operation may run before it is killed and **504** is returned; `0` disables |
|
|
184
|
+
| `FLAMAPY_CACHE_TTL` | `3600` | Seconds a result stays cached (same model + operation + arguments); `0` disables. Responses carry an `X-Cache: HIT|MISS` header |
|
|
185
|
+
| `FLAMAPY_CACHE_MAXSIZE` | `128` | Max cached results per worker |
|
|
186
|
+
| `FLAMAPY_TRUST_PROXY` | `false` | Set to `true` behind a reverse proxy so rate limits see the real client IP (`X-Forwarded-For`) |
|
|
187
|
+
| `WEB_CONCURRENCY` | `2` | gunicorn worker count (Docker image) |
|
|
188
|
+
| `GUNICORN_TIMEOUT` | `120` | gunicorn hard worker timeout; keep it above `FLAMAPY_OPERATION_TIMEOUT` |
|
|
189
|
+
|
|
190
|
+
Every request is logged to stdout (client IP, path, status, duration, upload size, cache result),
|
|
191
|
+
so `docker logs` is enough to spot heavy users.
|
|
192
|
+
|
|
193
|
+
<p align="right">(<a href="#top">back to top</a>)</p>
|
|
194
|
+
|
|
195
|
+
## API Documentation
|
|
196
|
+
|
|
197
|
+
All documentation is registered with Swagger UI and OAS 3.0, served at
|
|
198
|
+
[`/docs/`](http://localhost:8000/docs/). It is generated dynamically by
|
|
199
|
+
[Flasgger](https://github.com/flasgger/flasgger) from the route docstrings and method signatures, so
|
|
200
|
+
**don't forget to document your code in the route files** — new operations show up automatically.
|
|
201
|
+
|
|
202
|
+
<p align="right">(<a href="#top">back to top</a>)</p>
|
|
203
|
+
|
|
204
|
+
## Contributing
|
|
205
|
+
|
|
206
|
+
Contributions are welcome. This repo enforces
|
|
207
|
+
[Conventional Commits](https://www.conventionalcommits.org/) on pull request titles, and runs Ruff,
|
|
208
|
+
mypy, and pytest in CI.
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
pip install .[dev] # install dev tooling
|
|
212
|
+
|
|
213
|
+
ruff check . # lint
|
|
214
|
+
mypy -p flamapy # static type checking
|
|
215
|
+
pytest # tests
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
<p align="right">(<a href="#top">back to top</a>)</p>
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<a href="https://github.com/flamapy/flamapy-rest/actions/workflows/main.yml"></a>
|
|
4
|
+
<a href="https://github.com/flamapy/flamapy-rest/actions/workflows/conventionalpr.yml"></a>
|
|
5
|
+
<a href="https://github.com/flamapy/flamapy-rest/actions/workflows/docker-image.yml"></a>
|
|
6
|
+
<a href="https://pypi.org/project/flamapy-rest/"></a>
|
|
7
|
+
<a href="https://hub.docker.com/r/flamapy/flamapy-rest"></a>
|
|
8
|
+
<a href="https://pypi.org/project/flamapy-rest/"></a>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<div id="top"></div>
|
|
12
|
+
<br />
|
|
13
|
+
<div align="center">
|
|
14
|
+
|
|
15
|
+
<h3 align="center">FLAMAPY REST API</h3>
|
|
16
|
+
|
|
17
|
+
<p align="center">
|
|
18
|
+
A new and easy way to use FLAMA
|
|
19
|
+
<br />
|
|
20
|
+
<a href="https://github.com/flamapy/flamapy-rest/issues">Report Bug</a>
|
|
21
|
+
·
|
|
22
|
+
<a href="https://github.com/flamapy/flamapy-rest/issues">Request Feature</a>
|
|
23
|
+
</p>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<!-- ABOUT THE PROJECT -->
|
|
27
|
+
## About The Project
|
|
28
|
+
|
|
29
|
+
`flamapy-rest` is a [Flask](https://flask.palletsprojects.com/) REST API that wraps the
|
|
30
|
+
[FLAMAPY](https://flamapy.github.io/) framework for **feature model analysis**. It dynamically
|
|
31
|
+
exposes FLAMAPY operations (dead features, false-optional features, configurations, satisfiability,
|
|
32
|
+
diagnosis, …) as HTTP endpoints, with self-generated [Swagger](https://swagger.io/) documentation.
|
|
33
|
+
|
|
34
|
+
Feature model analysis has a crucial role in software product line engineering, enabling us to
|
|
35
|
+
understand, design, and validate the complex relationships among features in a software product
|
|
36
|
+
line. These models can be complex and challenging to analyze due to their variability, making it
|
|
37
|
+
difficult to identify conflicts, dead features, and potential optimizations. This REST API makes
|
|
38
|
+
that analysis available to any tool or language that can speak HTTP.
|
|
39
|
+
|
|
40
|
+
Please note: this is a living document and we will continue to update and improve it as we release
|
|
41
|
+
new versions of the plugins and receive feedback from our users. If there's anything you don't
|
|
42
|
+
understand or if you have any suggestions for improvement, don't hesitate to
|
|
43
|
+
[open an issue](https://github.com/flamapy/flamapy-rest/issues). We're here to help!
|
|
44
|
+
|
|
45
|
+
### Built With
|
|
46
|
+
|
|
47
|
+
* [Docker](https://www.docker.com/)
|
|
48
|
+
* [Flask](https://flask.palletsprojects.com/)
|
|
49
|
+
* [FLAMAPY](https://github.com/flamapy)
|
|
50
|
+
* [Flasgger](https://github.com/flasgger/flasgger)
|
|
51
|
+
|
|
52
|
+
<p align="right">(<a href="#top">back to top</a>)</p>
|
|
53
|
+
|
|
54
|
+
<!-- GETTING STARTED -->
|
|
55
|
+
## Getting Started
|
|
56
|
+
|
|
57
|
+
There are three supported ways to run the API. **Using the published Docker image is the easiest** —
|
|
58
|
+
nothing to build, and it already bundles FLAMAPY and all of its analysis plugins.
|
|
59
|
+
|
|
60
|
+
### Option 1 — Run the published Docker image (recommended)
|
|
61
|
+
|
|
62
|
+
The CI publishes a multi-arch image (`linux/amd64` and `linux/arm64`) to Docker Hub at
|
|
63
|
+
[`flamapy/flamapy-rest`](https://hub.docker.com/r/flamapy/flamapy-rest) on every version tag.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Pull and run the latest release
|
|
67
|
+
docker run --rm -p 8000:8000 flamapy/flamapy-rest
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Then open:
|
|
71
|
+
|
|
72
|
+
* API home: <http://localhost:8000/>
|
|
73
|
+
* Swagger UI / API docs: <http://localhost:8000/docs/>
|
|
74
|
+
|
|
75
|
+
To pin a specific version, use its tag (matching a release, e.g. `v2.6.0`):
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
docker run --rm -p 8000:8000 flamapy/flamapy-rest:v2.6.0
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Run it detached and give the container a name so it's easy to stop later:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
docker run -d --name flamapy-rest -p 8000:8000 flamapy/flamapy-rest
|
|
85
|
+
docker logs -f flamapy-rest # follow the logs
|
|
86
|
+
docker stop flamapy-rest # stop it
|
|
87
|
+
docker rm flamapy-rest # remove it
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Option 2 — Build the Docker image from source
|
|
91
|
+
|
|
92
|
+
If you want to run your local changes, clone the repo and build the image yourself. A helper script
|
|
93
|
+
is provided that builds, runs, and (on exit) cleans up the container and image:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# Linux / macOS
|
|
97
|
+
./start-server.sh
|
|
98
|
+
|
|
99
|
+
# Windows
|
|
100
|
+
start-server.cmd
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Or do it by hand:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
docker build --tag flamapy-rest .
|
|
107
|
+
docker run --rm -p 8000:8000 flamapy-rest
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Option 3 — Install from PyPI and run locally
|
|
111
|
+
|
|
112
|
+
The API is also published on PyPI as [`flamapy-rest`](https://pypi.org/project/flamapy-rest/)
|
|
113
|
+
(requires **Python 3.11+**). Because the WSGI entrypoint lives in `app.py`, clone the repo to get it:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
git clone https://github.com/flamapy/flamapy-rest.git
|
|
117
|
+
cd flamapy-rest
|
|
118
|
+
pip install .
|
|
119
|
+
|
|
120
|
+
# Production server
|
|
121
|
+
gunicorn --bind 0.0.0.0:8000 app:app
|
|
122
|
+
|
|
123
|
+
# …or the Flask development server
|
|
124
|
+
python -m flask run --host=0.0.0.0
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
<p align="right">(<a href="#top">back to top</a>)</p>
|
|
128
|
+
|
|
129
|
+
## Usage
|
|
130
|
+
|
|
131
|
+
Every public operation of FLAMAPY's `FLAMAFeatureModel` facade is exposed as a `POST` endpoint under
|
|
132
|
+
`/api/v1/operations/<operation_name>`. Each call uploads a feature model file (e.g. UVL) as
|
|
133
|
+
multipart form data; some operations also accept an optional `feature` or `configuration` parameter.
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# Example: count the number of valid configurations of a model
|
|
137
|
+
curl -X POST http://localhost:8000/api/v1/operations/configurations_number \
|
|
138
|
+
-F "model=@resources/models/simple/valid_model.uvl"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The full, interactive list of endpoints — with parameters and "try it out" support — is available
|
|
142
|
+
in the Swagger UI at <http://localhost:8000/docs/>.
|
|
143
|
+
|
|
144
|
+
<p align="right">(<a href="#top">back to top</a>)</p>
|
|
145
|
+
|
|
146
|
+
## Abuse controls & configuration
|
|
147
|
+
|
|
148
|
+
The API ships with per-IP rate limiting, a per-operation time budget, upload size limits and a
|
|
149
|
+
result cache, all configurable through environment variables (e.g. `docker run -e FLAMAPY_...`):
|
|
150
|
+
|
|
151
|
+
| Env var | Default | Meaning |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| `FLAMAPY_MAX_CONTENT_LENGTH` | `16777216` | Max upload size in bytes; larger requests get **413** |
|
|
154
|
+
| `FLAMAPY_RATELIMIT_ENABLED` | `true` | Turn rate limiting on/off |
|
|
155
|
+
| `FLAMAPY_RATELIMIT_DEFAULT` | `60 per minute` | Per-IP limit for cheap operations |
|
|
156
|
+
| `FLAMAPY_RATELIMIT_EXPENSIVE` | `10 per minute` | Per-IP limit for enumeration/solver-heavy operations (`configurations`, `configurations_number`, `sampling`, …); exceeding a limit returns **429** |
|
|
157
|
+
| `FLAMAPY_RATELIMIT_STORAGE_URI` | `memory://` | Limiter storage; use `redis://host:6379` when running several gunicorn workers so they share counters |
|
|
158
|
+
| `FLAMAPY_OPERATION_TIMEOUT` | `60` | Seconds an operation may run before it is killed and **504** is returned; `0` disables |
|
|
159
|
+
| `FLAMAPY_CACHE_TTL` | `3600` | Seconds a result stays cached (same model + operation + arguments); `0` disables. Responses carry an `X-Cache: HIT|MISS` header |
|
|
160
|
+
| `FLAMAPY_CACHE_MAXSIZE` | `128` | Max cached results per worker |
|
|
161
|
+
| `FLAMAPY_TRUST_PROXY` | `false` | Set to `true` behind a reverse proxy so rate limits see the real client IP (`X-Forwarded-For`) |
|
|
162
|
+
| `WEB_CONCURRENCY` | `2` | gunicorn worker count (Docker image) |
|
|
163
|
+
| `GUNICORN_TIMEOUT` | `120` | gunicorn hard worker timeout; keep it above `FLAMAPY_OPERATION_TIMEOUT` |
|
|
164
|
+
|
|
165
|
+
Every request is logged to stdout (client IP, path, status, duration, upload size, cache result),
|
|
166
|
+
so `docker logs` is enough to spot heavy users.
|
|
167
|
+
|
|
168
|
+
<p align="right">(<a href="#top">back to top</a>)</p>
|
|
169
|
+
|
|
170
|
+
## API Documentation
|
|
171
|
+
|
|
172
|
+
All documentation is registered with Swagger UI and OAS 3.0, served at
|
|
173
|
+
[`/docs/`](http://localhost:8000/docs/). It is generated dynamically by
|
|
174
|
+
[Flasgger](https://github.com/flasgger/flasgger) from the route docstrings and method signatures, so
|
|
175
|
+
**don't forget to document your code in the route files** — new operations show up automatically.
|
|
176
|
+
|
|
177
|
+
<p align="right">(<a href="#top">back to top</a>)</p>
|
|
178
|
+
|
|
179
|
+
## Contributing
|
|
180
|
+
|
|
181
|
+
Contributions are welcome. This repo enforces
|
|
182
|
+
[Conventional Commits](https://www.conventionalcommits.org/) on pull request titles, and runs Ruff,
|
|
183
|
+
mypy, and pytest in CI.
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
pip install .[dev] # install dev tooling
|
|
187
|
+
|
|
188
|
+
ruff check . # lint
|
|
189
|
+
mypy -p flamapy # static type checking
|
|
190
|
+
pytest # tests
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
<p align="right">(<a href="#top">back to top</a>)</p>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _env_bool(name: str, default: bool) -> bool:
|
|
6
|
+
raw = os.environ.get(name)
|
|
7
|
+
if raw is None:
|
|
8
|
+
return default
|
|
9
|
+
return raw.strip().lower() in ('1', 'true', 'yes', 'on')
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _env_int(name: str, default: int) -> int:
|
|
13
|
+
raw = os.environ.get(name)
|
|
14
|
+
if raw is None or raw.strip() == '':
|
|
15
|
+
return default
|
|
16
|
+
return int(raw)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_config() -> dict[str, Any]:
|
|
20
|
+
"""Abuse-control settings, every one overridable through an env var so the
|
|
21
|
+
Docker image stays a single self-contained container."""
|
|
22
|
+
return {
|
|
23
|
+
'MAX_CONTENT_LENGTH': _env_int('FLAMAPY_MAX_CONTENT_LENGTH', 16 * 1024 * 1024),
|
|
24
|
+
'RATELIMIT_ENABLED': _env_bool('FLAMAPY_RATELIMIT_ENABLED', True),
|
|
25
|
+
'RATELIMIT_STORAGE_URI': os.environ.get('FLAMAPY_RATELIMIT_STORAGE_URI', 'memory://'),
|
|
26
|
+
'RATELIMIT_DEFAULT_OPERATION': os.environ.get(
|
|
27
|
+
'FLAMAPY_RATELIMIT_DEFAULT', '60 per minute'
|
|
28
|
+
),
|
|
29
|
+
'RATELIMIT_EXPENSIVE_OPERATION': os.environ.get(
|
|
30
|
+
'FLAMAPY_RATELIMIT_EXPENSIVE', '10 per minute'
|
|
31
|
+
),
|
|
32
|
+
'OPERATION_TIMEOUT': _env_int('FLAMAPY_OPERATION_TIMEOUT', 60),
|
|
33
|
+
'CACHE_TTL': _env_int('FLAMAPY_CACHE_TTL', 3600),
|
|
34
|
+
'CACHE_MAXSIZE': _env_int('FLAMAPY_CACHE_MAXSIZE', 128),
|
|
35
|
+
'TRUST_PROXY': _env_bool('FLAMAPY_TRUST_PROXY', False),
|
|
36
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
from cachetools import TTLCache
|
|
5
|
+
from flask import Flask, current_app
|
|
6
|
+
from flask_limiter import Limiter
|
|
7
|
+
from flask_limiter.util import get_remote_address
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def default_operation_limit() -> str:
|
|
11
|
+
return current_app.config['RATELIMIT_DEFAULT_OPERATION']
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def expensive_operation_limit() -> str:
|
|
15
|
+
return current_app.config['RATELIMIT_EXPENSIVE_OPERATION']
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Storage URI and enabled flag are picked up from app.config (RATELIMIT_*)
|
|
19
|
+
# during init_app; per-route limits are attached in operations_routes.
|
|
20
|
+
limiter = Limiter(key_func=get_remote_address)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ResultCache:
|
|
24
|
+
"""Per-process TTL cache for operation results. The cache object is created
|
|
25
|
+
on init_app so its size/TTL follow the app configuration; a TTL of 0
|
|
26
|
+
disables caching entirely."""
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
self._cache: Optional[TTLCache[str, Any]] = None
|
|
30
|
+
self._lock = threading.Lock()
|
|
31
|
+
|
|
32
|
+
def init_app(self, app: Flask) -> None:
|
|
33
|
+
ttl = app.config['CACHE_TTL']
|
|
34
|
+
if ttl > 0:
|
|
35
|
+
self._cache = TTLCache(maxsize=app.config['CACHE_MAXSIZE'], ttl=ttl)
|
|
36
|
+
else:
|
|
37
|
+
self._cache = None
|
|
38
|
+
|
|
39
|
+
def get(self, key: str) -> Any:
|
|
40
|
+
if self._cache is None:
|
|
41
|
+
return None
|
|
42
|
+
with self._lock:
|
|
43
|
+
return self._cache.get(key)
|
|
44
|
+
|
|
45
|
+
def set(self, key: str, value: Any) -> None:
|
|
46
|
+
if self._cache is None:
|
|
47
|
+
return
|
|
48
|
+
with self._lock:
|
|
49
|
+
self._cache[key] = value
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
result_cache = ResultCache()
|
{flamapy_rest-2.6.0.dev1 → flamapy_rest-2.6.0.dev6}/flamapy/interfaces/rest/operations_routes.py
RENAMED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import hashlib
|
|
2
3
|
import inspect
|
|
3
4
|
import json
|
|
4
5
|
import shutil
|
|
@@ -6,16 +7,40 @@ import tempfile
|
|
|
6
7
|
import typing
|
|
7
8
|
from typing import Any, Callable
|
|
8
9
|
|
|
9
|
-
from flask import Blueprint, request, jsonify, abort
|
|
10
|
+
from flask import Blueprint, Response, current_app, request, jsonify, abort
|
|
10
11
|
from werkzeug.datastructures import FileStorage
|
|
11
12
|
from werkzeug.utils import secure_filename
|
|
12
13
|
from flamapy.interfaces.python.flamapy_feature_model import FLAMAFeatureModel, Backend
|
|
13
|
-
from flamapy.core.exceptions import FlamaException
|
|
14
14
|
from flamapy.metamodels.configuration_metamodel.models import Configuration
|
|
15
15
|
|
|
16
|
+
from flamapy.interfaces.rest.extensions import (
|
|
17
|
+
limiter,
|
|
18
|
+
result_cache,
|
|
19
|
+
default_operation_limit,
|
|
20
|
+
expensive_operation_limit,
|
|
21
|
+
)
|
|
22
|
+
from flamapy.interfaces.rest.runner import ModelParseError, OperationTimeout, run_operation
|
|
23
|
+
|
|
16
24
|
|
|
17
25
|
operations_bp = Blueprint('operations_bp', __name__, url_prefix='/api/v1/operations')
|
|
18
26
|
|
|
27
|
+
# Operations that enumerate or repeatedly invoke a solver; they get the stricter
|
|
28
|
+
# rate-limit tier because a single call can monopolize a CPU for a long time.
|
|
29
|
+
_EXPENSIVE_OPERATIONS = {
|
|
30
|
+
'configurations',
|
|
31
|
+
'configurations_number',
|
|
32
|
+
'configurations_with_n_features',
|
|
33
|
+
'conflict',
|
|
34
|
+
'diagnosis',
|
|
35
|
+
'feature_inclusion_probability',
|
|
36
|
+
'homogeneity',
|
|
37
|
+
'product_distribution',
|
|
38
|
+
'sampling',
|
|
39
|
+
'unique_features',
|
|
40
|
+
'variability',
|
|
41
|
+
'variant_features',
|
|
42
|
+
}
|
|
43
|
+
|
|
19
44
|
# Backward-compatible multipart field names for parameters that the API exposed
|
|
20
45
|
# before the dispatcher became generic. Any other parameter derives its field
|
|
21
46
|
# name from the parameter itself (file params drop the trailing "_path").
|
|
@@ -116,6 +141,26 @@ def _resolve_kwargs(operation: Any, work_dir: str) -> dict[str, Any]:
|
|
|
116
141
|
return kwargs
|
|
117
142
|
|
|
118
143
|
|
|
144
|
+
def _cache_key(
|
|
145
|
+
operation_name: str, operation: Any, model_path: str, kwargs: dict[str, Any]
|
|
146
|
+
) -> str:
|
|
147
|
+
"""Digest of everything that determines the result: the operation, the model
|
|
148
|
+
contents and every argument (file arguments by content, not by temp path)."""
|
|
149
|
+
file_params = {p.name for p in _operation_params(operation) if _is_file_param(p)}
|
|
150
|
+
digest = hashlib.sha256()
|
|
151
|
+
digest.update(operation_name.encode())
|
|
152
|
+
with open(model_path, 'rb') as model_file:
|
|
153
|
+
digest.update(model_file.read())
|
|
154
|
+
for name in sorted(kwargs):
|
|
155
|
+
digest.update(b'\x00' + name.encode() + b'\x00')
|
|
156
|
+
if name in file_params:
|
|
157
|
+
with open(kwargs[name], 'rb') as uploaded_file:
|
|
158
|
+
digest.update(uploaded_file.read())
|
|
159
|
+
else:
|
|
160
|
+
digest.update(repr(kwargs[name]).encode())
|
|
161
|
+
return digest.hexdigest()
|
|
162
|
+
|
|
163
|
+
|
|
119
164
|
def _api_call(operation_name: str) -> Any:
|
|
120
165
|
uploaded_model = request.files.get('model')
|
|
121
166
|
if uploaded_model is None or not uploaded_model.filename:
|
|
@@ -126,19 +171,34 @@ def _api_call(operation_name: str) -> Any:
|
|
|
126
171
|
work_dir = tempfile.mkdtemp(prefix='flamapy_')
|
|
127
172
|
try:
|
|
128
173
|
model_path = _save_upload(uploaded_model, work_dir)
|
|
174
|
+
operation = getattr(FLAMAFeatureModel, operation_name)
|
|
175
|
+
kwargs = _resolve_kwargs(operation, work_dir)
|
|
176
|
+
|
|
177
|
+
key = _cache_key(operation_name, operation, model_path, kwargs)
|
|
178
|
+
payload = result_cache.get(key)
|
|
179
|
+
if payload is not None:
|
|
180
|
+
response: Response = jsonify(payload)
|
|
181
|
+
response.headers['X-Cache'] = 'HIT'
|
|
182
|
+
return response
|
|
183
|
+
|
|
129
184
|
try:
|
|
130
|
-
|
|
131
|
-
|
|
185
|
+
result = run_operation(
|
|
186
|
+
model_path, operation_name, kwargs, current_app.config['OPERATION_TIMEOUT']
|
|
187
|
+
)
|
|
188
|
+
except ModelParseError:
|
|
132
189
|
abort(400, "The uploaded model could not be parsed")
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
result = operation(**kwargs)
|
|
190
|
+
except OperationTimeout as exc:
|
|
191
|
+
abort(504, str(exc))
|
|
136
192
|
finally:
|
|
137
193
|
shutil.rmtree(work_dir, ignore_errors=True)
|
|
138
194
|
|
|
139
195
|
if result is None:
|
|
140
196
|
return jsonify(error='Not valid result'), 404
|
|
141
|
-
|
|
197
|
+
payload = json.loads(json.dumps(result, cls=CustomJSONEncoder))
|
|
198
|
+
result_cache.set(key, payload)
|
|
199
|
+
response = jsonify(payload)
|
|
200
|
+
response.headers['X-Cache'] = 'MISS'
|
|
201
|
+
return response
|
|
142
202
|
|
|
143
203
|
|
|
144
204
|
def extract_docstring_with_swagger_info(method: Any) -> str:
|
|
@@ -189,9 +249,12 @@ def create_route(operation_name: str, docstring: str) -> Callable[[], Any]:
|
|
|
189
249
|
|
|
190
250
|
|
|
191
251
|
# Introspect FLAMAFeatureModel to expose every public method as a POST route,
|
|
192
|
-
# generating its Swagger spec from the method signature.
|
|
252
|
+
# generating its Swagger spec from the method signature. Limits are passed as
|
|
253
|
+
# callables so they read the app config of whichever app the blueprint joins.
|
|
193
254
|
for name, method in inspect.getmembers(FLAMAFeatureModel, predicate=inspect.isfunction):
|
|
194
255
|
if name.startswith('_'):
|
|
195
256
|
continue
|
|
196
257
|
docstring = extract_docstring_with_swagger_info(method)
|
|
197
|
-
|
|
258
|
+
limit = expensive_operation_limit if name in _EXPENSIVE_OPERATIONS else default_operation_limit
|
|
259
|
+
route_view = limiter.limit(limit)(create_route(name, docstring))
|
|
260
|
+
operations_bp.route(f'/{name}', methods=['POST'])(route_view)
|