Skip to content

Commit c40943d

Browse files
committed
move minimal api example to a separate module, auto generate HTTP code snippets (request, response)
1 parent 354686e commit c40943d

12 files changed

+306
-68
lines changed

README.rst

Lines changed: 3 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -23,73 +23,10 @@ Install
2323
pip install Flask-COMBO-JSONAPI
2424

2525

26-
A minimal API
27-
=============
2826

29-
.. code-block:: python
30-
31-
from flask import Flask
32-
from flask_combo_jsonapi import Api, ResourceDetail, ResourceList
33-
from flask_sqlalchemy import SQLAlchemy
34-
from marshmallow_jsonapi.flask import Schema
35-
from marshmallow_jsonapi import fields
36-
37-
# Create the Flask application and the Flask-SQLAlchemy object.
38-
app = Flask(__name__)
39-
app.config['DEBUG'] = True
40-
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
41-
db = SQLAlchemy(app)
42-
43-
# Create model
44-
class Person(db.Model):
45-
id = db.Column(db.Integer, primary_key=True)
46-
name = db.Column(db.String)
47-
48-
# Create the database.
49-
db.create_all()
50-
51-
# Create schema
52-
class PersonSchema(Schema):
53-
class Meta:
54-
type_ = 'person'
55-
self_view = 'person_detail'
56-
self_view_kwargs = {'id': '<id>'}
57-
self_view_many = 'person_list'
58-
59-
id = fields.Integer(as_string=True, dump_only=True)
60-
name = fields.Str()
61-
62-
# Create resource managers
63-
class PersonList(ResourceList):
64-
schema = PersonSchema
65-
data_layer = {'session': db.session,
66-
'model': Person}
67-
68-
class PersonDetail(ResourceDetail):
69-
schema = PersonSchema
70-
data_layer = {'session': db.session,
71-
'model': Person}
72-
73-
# Create the API object
74-
api = Api(app)
75-
api.route(PersonList, 'person_list', '/persons')
76-
api.route(PersonDetail, 'person_detail', '/persons/<int:id>')
77-
78-
# Start the flask loop
79-
if __name__ == '__main__':
80-
app.run()
81-
82-
This example provides the following API structure:
83-
84-
======================== ====== ============= ===========================
85-
URL method endpoint Usage
86-
======================== ====== ============= ===========================
87-
/persons GET person_list Get a collection of persons
88-
/persons POST person_list Create a person
89-
/persons/<int:person_id> GET person_detail Get person details
90-
/persons/<int:person_id> PATCH person_detail Update a person
91-
/persons/<int:person_id> DELETE person_detail Delete a person
92-
======================== ====== ============= ===========================
27+
.. literalinclude:: ./docs/minimal_api_example.rst
28+
:language: rst
29+
9330

9431
Flask-COMBO-JSONAPI vs `Flask-RESTful <https://flask-restful.readthedocs.io/en/latest/>`_
9532
==========================================================================================

docs/http_snippets/README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Generate HTTP code snippets
2+
3+
4+
## Install
5+
6+
```shell
7+
# to use in cli
8+
npm install --global httpsnippet
9+
```
10+
11+
```shell
12+
# to use as a module
13+
npm install --save httpsnippet
14+
```
15+
16+
## Generate
17+
18+
### Create all snippets and run requests for them
19+
20+
```shell
21+
# run and create for all
22+
./run_and_create.sh
23+
```
24+
25+
### Or do it manually:
26+
27+
```shell
28+
# example
29+
httpsnippet example.json --target node --client unirest --output ./snippets
30+
```
31+
32+
```shell
33+
# minimal api to python3
34+
httpsnippet minimal_api__create_person.json --target python --client python3 --output ./snippets
35+
```
36+
37+
```shell
38+
# minimal api to http
39+
httpsnippet minimal_api__create_person.json --target http --output ./snippets
40+
```
41+
42+
43+
```shell
44+
# process multiple
45+
httpsnippet ./*.json --target http --output ./snippets
46+
```
47+
48+
49+
### Create requests and run them, write results
50+
51+
```shell
52+
# create python-requests requests snippets
53+
httpsnippet ./*.json --target python --client requests --output ./snippets
54+
```
55+
56+
```shell
57+
# Run requests, save output
58+
python3 update_snippets_with_responses.py
59+
```
60+
61+
> **Pro tip:** run webserver for specs before running update_snippets_with_responses, otherwise it won't work 😉
62+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"method": "POST",
3+
"url": "http://localhost:5000/persons",
4+
"httpVersion": "HTTP/1.1",
5+
"queryString": [
6+
],
7+
"headers": [
8+
{
9+
"name": "content-type",
10+
"value": "application/vnd.api+json"
11+
}
12+
],
13+
"postData": {
14+
"mimeType": "application/json",
15+
"text": "{\n\t\"data\": {\n\t\t\"type\": \"person\",\n\t\t\"name\": \"John\"\n\t}\n}"
16+
}
17+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/usr/bin/env sh
2+
3+
echo "Create HTTP snippets"
4+
httpsnippet ./*.json --target http --output ./snippets
5+
6+
echo "Create Python (requests) snippets"
7+
httpsnippet ./*.json --target python --client requests --output ./snippets
8+
9+
echo "Run requests"
10+
python3 update_snippets_with_responses.py
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
POST /persons HTTP/1.1
2+
Content-Type: application/vnd.api+json
3+
Host: localhost:5000
4+
Content-Length: 54
5+
6+
{
7+
"data": {
8+
"type": "person",
9+
"name": "John"
10+
}
11+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
HTTP/1.1 201 Created
2+
Content-Type: application/vnd.api+json
3+
4+
{
5+
"data": {
6+
"attributes": {
7+
"name": null
8+
},
9+
"id": "1",
10+
"links": {
11+
"self": "/persons/1"
12+
},
13+
"type": "person"
14+
},
15+
"jsonapi": {
16+
"version": "1.0"
17+
},
18+
"links": {
19+
"self": "/persons/1"
20+
}
21+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import os
2+
import importlib
3+
import logging
4+
from http import HTTPStatus
5+
6+
import requests
7+
import simplejson
8+
9+
logging.basicConfig(level=logging.DEBUG)
10+
11+
log = logging.getLogger(__name__)
12+
13+
SNIPPETS_DIR = "snippets"
14+
SORT_KEYS_ON_DUMP = True
15+
SNIPPET_RESULT_POSTFIX = "_result"
16+
REMOVE_PYTHON_SNIPPET = True
17+
18+
19+
def run_request_for_module(module_name: str):
20+
log.info("Start processing %r", module_name)
21+
22+
module_full_name = ".".join((SNIPPETS_DIR, module_name))
23+
log.debug("import module %s", module_full_name)
24+
module = importlib.import_module(module_full_name)
25+
26+
log.info("Process module %s", module)
27+
response: requests.Response = module.response
28+
log.info("Response %s", response)
29+
http_response_text = []
30+
response_reason = (response.reason or "").title()
31+
http_response_text.append(
32+
# "HTTP/1.1 201 Created"
33+
"{} {} {}".format(
34+
"HTTP/1.1",
35+
response.status_code,
36+
response_reason,
37+
)
38+
)
39+
http_response_text.append(
40+
"{}: {}".format(
41+
"Content-Type",
42+
response.headers.get('content-type'),
43+
)
44+
)
45+
http_response_text.append("")
46+
47+
http_response_text.append(
48+
simplejson.dumps(
49+
response.json(),
50+
sort_keys=SORT_KEYS_ON_DUMP,
51+
indent=2,
52+
),
53+
)
54+
55+
http_response_text.append("")
56+
57+
result_text = "\n".join(http_response_text)
58+
log.debug("Result text:\n%s", result_text)
59+
60+
result_file_name = "/".join((SNIPPETS_DIR, module_name + SNIPPET_RESULT_POSTFIX))
61+
with open(result_file_name, "w") as f:
62+
res = f.write(result_text)
63+
log.info("Wrote text (%s) to %r", res, result_file_name)
64+
65+
log.info("Processed %r", module_name)
66+
67+
68+
def main():
69+
log.warning("Starting")
70+
71+
for module_name in os.listdir(SNIPPETS_DIR):
72+
if module_name.endswith(".py"):
73+
try:
74+
run_request_for_module(module_name[:-3])
75+
except Exception:
76+
log.exception("Could not process module %r, skipping", module_name)
77+
else:
78+
if REMOVE_PYTHON_SNIPPET:
79+
os.unlink("/".join((SNIPPETS_DIR, module_name)))
80+
81+
log.warning("Done")
82+
83+
84+
if __name__ == "__main__":
85+
main()

docs/index.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ Flask-COMBO-JSONAPI with Flask.
6767
oauth
6868
configuration
6969

70+
71+
.. literalinclude:: ./minimal_api_example.rst
72+
:language: rst
73+
74+
7075
API Reference
7176
-------------
7277

docs/minimal_api_example.rst

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
2+
A minimal API
3+
=============
4+
5+
.. literalinclude:: ../examples/api_minimal.py
6+
:language: python
7+
8+
9+
This example provides the following API structure:
10+
11+
======================== ====== ============= ===========================
12+
URL method endpoint Usage
13+
======================== ====== ============= ===========================
14+
/persons GET person_list Get a collection of persons
15+
/persons POST person_list Create a person
16+
/persons/<int:person_id> GET person_detail Get person details
17+
/persons/<int:person_id> PATCH person_detail Update a person
18+
/persons/<int:person_id> DELETE person_detail Delete a person
19+
======================== ====== ============= ===========================
20+
21+
22+
Request:
23+
24+
.. literalinclude:: ./http_snippets/snippets/minimal_api__create_person
25+
:language: HTTP
26+
27+
Response:
28+
29+
.. literalinclude:: ./http_snippets/snippets/minimal_api__create_person_result
30+
:language: HTTP

examples/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
1616

1717
# Initialize SQLAlchemy
18-
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
18+
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/api.db'
1919
db = SQLAlchemy(app)
2020

2121

0 commit comments

Comments
 (0)