Eve-Swagger 0.2.1__py3-none-any.whl

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.
@@ -0,0 +1,11 @@
1
+ """
2
+ eve-swagger
3
+ ~~~~~~~~~~~
4
+ swagger.io extension for Eve-powered REST APIs.
5
+
6
+ :copyright: (c) 2015 by Nicola Iarocci.
7
+ :license: BSD, see LICENSE for more details.
8
+ """
9
+
10
+ from .swagger import get_swagger_blueprint, add_documentation # noqa
11
+ from .definitions import INFO, HOST # noqa
@@ -0,0 +1,283 @@
1
+ """
2
+ eve-swagger.definitions
3
+ ~~~~~~~~~~~~~~~~~~~~~~~
4
+ swagger.io extension for Eve-powered REST APIs.
5
+
6
+ :copyright: (c) 2016 by Nicola Iarocci.
7
+ :license: BSD, see LICENSE for more details.
8
+ """
9
+
10
+ from collections import OrderedDict
11
+ from flask import current_app as app
12
+
13
+ INFO = "SWAGGER_INFO"
14
+ HOST = "SWAGGER_HOST"
15
+
16
+
17
+ def definitions():
18
+ definitions = OrderedDict()
19
+ dr_sources = {}
20
+ for rd in app.config["DOMAIN"].values():
21
+ dr_sources.update(_get_dr_sources(rd["schema"]))
22
+
23
+ for rd in app.config["DOMAIN"].values():
24
+ if rd.get("disable_documentation"):
25
+ continue
26
+ title = rd["item_title"]
27
+ definitions[title] = _object(rd, dr_sources)
28
+ if "description" in rd:
29
+ definitions[title]["description"] = rd["description"]
30
+
31
+ # add data_relation source fields to #/components/schemas/
32
+ definitions.update(dr_sources)
33
+ # add default error response eve schema
34
+ error_schema = {
35
+ "type": "object",
36
+ "properties": {
37
+ "_status": {"type": "string", "example": "OK"},
38
+ "_error": {
39
+ "type": "object",
40
+ "properties": {
41
+ "code": {"type": "integer"},
42
+ "message": {"type": "string"},
43
+ },
44
+ },
45
+ },
46
+ "required": ["_status", "_error"],
47
+ }
48
+ definitions["Error"] = error_schema
49
+ # add default meta response eve schema
50
+ response_metadata_schema = {
51
+ "type": "object",
52
+ "properties": {
53
+ "page": {"type": "string"},
54
+ "total": {"type": "integer"},
55
+ "max_results": {"type": "integer"},
56
+ },
57
+ }
58
+ definitions["response_metadata"] = response_metadata_schema
59
+
60
+ response_links_schema = {
61
+ "type": "object",
62
+ "properties": {
63
+ "parent": {
64
+ "type": "object",
65
+ "properties": {
66
+ "title": {"type": "string", "format": "uri"},
67
+ "href": {"type": "string", "format": "uri"},
68
+ },
69
+ },
70
+ "self": {
71
+ "type": "object",
72
+ "properties": {
73
+ "title": {"type": "string", "format": "uri"},
74
+ "href": {"type": "string", "format": "uri"},
75
+ },
76
+ },
77
+ },
78
+ }
79
+
80
+ definitions["response_links"] = response_links_schema
81
+
82
+ return definitions
83
+
84
+
85
+ def _object(rd, dr_sources):
86
+ props = {}
87
+ required = []
88
+ for field, rules in rd["schema"].items():
89
+ if rules.get("required") is True:
90
+ required.append(field)
91
+
92
+ def_name = rd["item_title"] + "_" + field
93
+ props[field] = _field_props(rules, dr_sources, def_name)
94
+
95
+ if def_name in dr_sources:
96
+ # the current field is a source of a data_relation
97
+
98
+ # replace None in dr_sources with the field properties
99
+ dr_sources[def_name] = OrderedDict(props[field])
100
+ props[field] = {"$ref": "#/components/schemas/{}".format(def_name)}
101
+
102
+ if "data_relation" in rules:
103
+ # the current field is a copy of another field
104
+ dr = rules["data_relation"]
105
+ if dr["resource"] not in app.config["DOMAIN"]:
106
+ # source of data_relation does not exist
107
+ continue
108
+ title = app.config["DOMAIN"][dr["resource"]]["item_title"]
109
+ source_def_name = title + "_" + dr["field"]
110
+ props[field] = {"$ref": "#/components/schemas/{}".format(source_def_name)}
111
+
112
+ field_def = {}
113
+ field_def["type"] = "object"
114
+ field_def["properties"] = props
115
+ if len(required):
116
+ field_def["required"] = required
117
+ return field_def
118
+
119
+
120
+ def _field_props(rules, dr_sources, prefix):
121
+ resp = {}
122
+ map = {
123
+ "dict": ("object",),
124
+ "list": ("array",),
125
+ "objectid": ("string", "objectid"),
126
+ "datetime": ("string", "date-time"),
127
+ "float": ("number", "float"),
128
+ "point": ("geometry", "Point"),
129
+ "multipoint": ("geometry", "MultiPoint"),
130
+ "linestring": ("geometry", "LineString"),
131
+ "multilinestring": ("geometry", "MultiLineString"),
132
+ "polygon": ("geometry", "Polygon"),
133
+ "multipolygon": ("geometry", "MultiPolygon"),
134
+ "geometrycollection": ("geometry", "GeometryCollection"),
135
+ }
136
+ if app.config["RETURN_MEDIA_AS_URL"]:
137
+ map["media"] = ("string",)
138
+ else:
139
+ map["media"] = ("string", "base64")
140
+
141
+ eve_type = rules.get("type")
142
+ if eve_type is None:
143
+ return resp
144
+
145
+ if "description" in rules:
146
+ resp["description"] = rules["description"]
147
+
148
+ if "example" in rules:
149
+ resp["example"] = rules["example"]
150
+
151
+ if "allowed" in rules:
152
+ resp["enum"] = rules["allowed"]
153
+
154
+ if "default" in rules:
155
+ resp["default"] = rules["default"]
156
+
157
+ if "minlength" in rules:
158
+ if eve_type == "string":
159
+ resp["minLength"] = rules["minlength"]
160
+ elif eve_type == "list":
161
+ resp["minItems"] = rules["minlength"]
162
+
163
+ if "maxlength" in rules:
164
+ if eve_type == "string":
165
+ resp["maxLength"] = rules["maxlength"]
166
+ elif eve_type == "list":
167
+ resp["maxItems"] = rules["maxlength"]
168
+
169
+ if "min" in rules:
170
+ if eve_type in ["number", "integer", "float"]:
171
+ resp["minimum"] = rules["min"]
172
+
173
+ if "max" in rules:
174
+ if eve_type in ["number", "integer", "float"]:
175
+ resp["maximum"] = rules["max"]
176
+
177
+ if "readonly" in rules:
178
+ resp["readOnly"] = rules["readonly"]
179
+
180
+ if "regex" in rules:
181
+ resp["pattern"] = rules["regex"]
182
+
183
+ if "nullable" in rules:
184
+ resp["nullable"] = rules["nullable"]
185
+
186
+ type = map.get(eve_type, (eve_type,))
187
+
188
+ resp["type"] = type[0]
189
+ if type[0] == "object":
190
+ if "schema" in rules:
191
+ # set prefix as item_title to avoid name collisions of nested
192
+ # fields with higher up fields
193
+ pseudo_rd = {"item_title": prefix, "schema": rules["schema"]}
194
+ resp.update(_object(pseudo_rd, dr_sources))
195
+ # TODO add support for more elaborate cases using additionalProperties
196
+ elif "valueschema" in rules:
197
+ keytype = {"type": "string"}
198
+ if "keyschema" in rules:
199
+ keytype = rules["keyschema"]
200
+
201
+ pseudo_rd = {
202
+ "item_title": prefix,
203
+ "properties": {"keys": keytype, "values": rules["valueschema"]},
204
+ }
205
+ resp.update(pseudo_rd)
206
+ elif type[0] == "array":
207
+ type = "array"
208
+ if "schema" in rules:
209
+ resp["items"] = _field_props(rules["schema"], dr_sources, prefix)
210
+ else:
211
+ # 'items' is mandatory for swagger, we assume it's a list of
212
+ # strings
213
+ resp["items"] = {"type": "string"}
214
+ elif type[0] == "geometry":
215
+ # create a pseudo object
216
+ pseudo_rd = {
217
+ "item_title": prefix,
218
+ "schema": {
219
+ "type": {"type": "string", "allowed": [type[1]], "required": True},
220
+ "coordinates": {"type": "list", "required": True},
221
+ },
222
+ }
223
+ point2D_rd = {"schema": {"type": "integer"}, "minlength": 2, "maxlength": 2}
224
+ array_rd = {"schema": {"type": "list"}}
225
+ s = "schema"
226
+ c = "coordinates"
227
+ if type[1] == "Point":
228
+ # 1st level array of point 2D
229
+ pseudo_rd[s][c].update(point2D_rd)
230
+ elif type[1] in ["LineString", "MultiPoint"]:
231
+ # 2nd level array of point 2D
232
+ pseudo_rd[s][c].update(array_rd)
233
+ pseudo_rd[s][c][s].update(point2D_rd)
234
+ elif type[1] in ["MultiLineString", "Polygon"]:
235
+ # 3rd level array of point 2D
236
+ pseudo_rd[s][c].update(array_rd)
237
+ pseudo_rd[s][c][s].update(array_rd)
238
+ pseudo_rd[s][c][s][s].update(point2D_rd)
239
+ elif type[1] in ["MultiPolygon"]:
240
+ # 4th level array of point 2D
241
+ pseudo_rd[s][c].update(array_rd)
242
+ pseudo_rd[s][c][s].update(array_rd)
243
+ pseudo_rd[s][c][s][s].update(array_rd)
244
+ pseudo_rd[s][c][s][s][s].update(point2D_rd)
245
+ elif type[1] in ["GeometryCollection"]:
246
+ # GeometryCollection require external defs
247
+ pass
248
+ resp.update(_object(pseudo_rd, dr_sources))
249
+ else:
250
+ try:
251
+ resp["format"] = type[1]
252
+ except IndexError:
253
+ pass
254
+
255
+ return resp
256
+
257
+
258
+ def _get_dr_sources(schema):
259
+ """
260
+ Returns a dict of names for sources of data_relations:
261
+
262
+ e.g. the name field in the user resource relates to (is a copy of) the
263
+ name field of the person resource
264
+ => return {'person_name': None}
265
+ """
266
+ dr_sources = {}
267
+ for rules in schema.values():
268
+ if "data_relation" in rules:
269
+ dr = rules["data_relation"]
270
+ if dr["resource"] not in app.config["DOMAIN"]:
271
+ # source of data_relation does not exist
272
+ continue
273
+ title = app.config["DOMAIN"][dr["resource"]]["item_title"]
274
+ def_name = title + "_" + dr["field"]
275
+ dr_sources[def_name] = None
276
+ elif "schema" in rules:
277
+ if rules.get("type") in "dict":
278
+ # recursively handle data_relations in subdicts
279
+ dr_sources.update(_get_dr_sources(rules["schema"]))
280
+ elif rules.get("type") == "list" and "schema" in rules["schema"]:
281
+ # recursively handle data_relations in lists
282
+ dr_sources.update(_get_dr_sources(rules["schema"]["schema"]))
283
+ return dr_sources
eve_swagger/objects.py ADDED
@@ -0,0 +1,313 @@
1
+ """
2
+ eve-swagger.objects
3
+ ~~~~~~~~~~~~~~~~~~~
4
+ swagger.io extension for Eve-powered REST APIs.
5
+
6
+ :copyright: (c) 2015 by Nicola Iarocci.
7
+ :license: BSD, see LICENSE for more details.
8
+ """
9
+
10
+ import sys
11
+ from collections import OrderedDict
12
+ from flask import request, current_app as app
13
+ from eve.auth import BasicAuth, TokenAuth
14
+
15
+ from .validation import validate_info
16
+ from .paths import get_ref_schema
17
+ from .definitions import INFO, HOST
18
+
19
+
20
+ def info():
21
+ validate_info()
22
+
23
+ cfg = app.config[INFO]
24
+
25
+ def node(parent, cfg, key):
26
+ value = cfg.get(key)
27
+ if value:
28
+ parent[key] = cfg[key]
29
+
30
+ info = OrderedDict()
31
+ node(info, cfg, "title")
32
+ node(info, cfg, "description")
33
+ node(info, cfg, "termsOfService")
34
+ node(info, cfg, "contact")
35
+ node(info, cfg, "license")
36
+ node(info, cfg, "version")
37
+
38
+ return info
39
+
40
+
41
+ def servers():
42
+ url = app.config.get(HOST) or "%s://%s" % (request.scheme, request.host)
43
+ if app.config["URL_PREFIX"]:
44
+ url = url + "/" + app.config["URL_PREFIX"]
45
+ if app.config["API_VERSION"]:
46
+ url = url + "/" + app.config["API_VERSION"]
47
+ return [{"url": url}]
48
+
49
+
50
+ def responses():
51
+ return {
52
+ "error": {
53
+ "description": "An error message",
54
+ "content": {
55
+ "application/json": {"schema": {"$ref": "#/components/schemas/Error"}}
56
+ },
57
+ }
58
+ }
59
+
60
+
61
+ def parameters():
62
+ parameters = OrderedDict()
63
+ # resource parameters
64
+ for resource_name, rd in app.config["DOMAIN"].items():
65
+ if resource_name.endswith("_versions") or rd.get("disable_documentation"):
66
+ continue
67
+
68
+ title = rd["item_title"]
69
+ lookup_field = rd["item_lookup_field"]
70
+ if lookup_field not in rd["schema"]:
71
+ rd["schema"][lookup_field] = {"type": "objectid"}
72
+ eve_type = rd["schema"][lookup_field]["type"]
73
+ descr = rd["schema"][lookup_field].get("description") or ""
74
+ example = rd["schema"][lookup_field].get("example") or ""
75
+ if "data_relation" in rd["schema"][lookup_field]:
76
+ # the lookup field is a copy of another field
77
+ dr = rd["schema"][lookup_field]["data_relation"]
78
+
79
+ # resource definition of the data relation source
80
+ source_rd = app.config["DOMAIN"][dr["resource"]]
81
+
82
+ # schema of the data relation source field
83
+ source_def = source_rd["schema"][dr["field"]]
84
+
85
+ # key in #/definitions/...
86
+ source_def_name = source_rd["item_title"] + "_" + dr["field"]
87
+
88
+ # copy description if necessary
89
+ descr = descr or source_def.get("description")
90
+ descr = descr + " (links to {})".format(source_def_name)
91
+
92
+ p = OrderedDict()
93
+ p["in"] = "path"
94
+ p["name"] = title.lower() + "Id"
95
+ p["required"] = True
96
+ p["description"] = descr
97
+ if "SWAGGER_EXAMPLE_FIELD_REMOVE" not in app.config:
98
+ p["example"] = example
99
+ ptype = ""
100
+ if eve_type == "objectid" or eve_type == "datetime":
101
+ ptype = "string"
102
+ elif eve_type == "float":
103
+ ptype = "number"
104
+ else:
105
+ # TODO define default
106
+ pass
107
+
108
+ p["schema"] = {"type": ptype}
109
+ parameters[title + "_" + lookup_field] = p
110
+
111
+ # additional_lookup
112
+ if "additional_lookup" in rd:
113
+ lookup_field = rd["additional_lookup"]["field"]
114
+ descr = rd["schema"][lookup_field].get("description") or ""
115
+ example = rd["schema"][lookup_field].get("example") or ""
116
+
117
+ p = OrderedDict()
118
+ p["in"] = "path"
119
+ p["name"] = rd["additional_lookup"]["field"].title()
120
+ p["required"] = True
121
+ p["description"] = descr
122
+ p["example"] = example
123
+ p["schema"] = {"type": "string"}
124
+
125
+ parameters[title + "_" + lookup_field] = p
126
+
127
+ # add header parameters
128
+ parameters.update(_header_parameters())
129
+ # add query parameters
130
+ parameters.update(_query_parameters())
131
+ return parameters
132
+
133
+
134
+ def _query_parameters():
135
+ params = {}
136
+
137
+ r = OrderedDict()
138
+ r["in"] = "query"
139
+ r["name"] = app.config["QUERY_WHERE"]
140
+ r["description"] = 'the filters query parameter (ex.: {"number": 10})'
141
+ r["schema"] = {"type": "string"}
142
+ params["query__where"] = r
143
+
144
+ r = OrderedDict()
145
+ r["in"] = "query"
146
+ r["name"] = app.config["QUERY_PROJECTION"]
147
+ r["description"] = 'the projections query parameter (ex.: {"name": 1})'
148
+ r["schema"] = {"type": "string"}
149
+ params["query__projections"] = r
150
+
151
+ r = OrderedDict()
152
+ r["in"] = "query"
153
+ r["name"] = app.config["QUERY_SORT"]
154
+ r["description"] = 'the sort query parameter (ex.: "city,-lastname")'
155
+ r["schema"] = {"type": "string"}
156
+ params["query__sort"] = r
157
+
158
+ r = OrderedDict()
159
+ r["in"] = "query"
160
+ r["name"] = app.config["QUERY_PAGE"]
161
+ r["description"] = "the pages query parameter"
162
+ r["schema"] = {"type": "integer", "example": 1}
163
+ params["query__page"] = r
164
+
165
+ r = OrderedDict()
166
+ r["in"] = "query"
167
+ r["name"] = app.config["QUERY_MAX_RESULTS"]
168
+ r["description"] = "the max results query parameter"
169
+ r["schema"] = {"type": "integer", "example": 25}
170
+ params["query__max_results"] = r
171
+
172
+ return params
173
+
174
+
175
+ def _header_parameters():
176
+ r = OrderedDict()
177
+ r["in"] = "header"
178
+ r["name"] = "If-Match"
179
+ r["description"] = "Current value of the _etag field"
180
+ r["required"] = app.config["IF_MATCH"] and app.config["ENFORCE_IF_MATCH"]
181
+ r["schema"] = {"type": "string"}
182
+ return {"If-Match": r}
183
+
184
+
185
+ def examples():
186
+ examples = OrderedDict()
187
+
188
+ for resource_name, rd in app.config["DOMAIN"].items():
189
+ if resource_name.endswith("_versions") or rd.get("disable_documentation"):
190
+ continue
191
+
192
+ title = rd["item_title"]
193
+ ex = OrderedDict()
194
+ ex["summary"] = "An example {0} document."
195
+ ex["description"] = (
196
+ "An example for {0} documents request bodies."
197
+ " Used in POST, PUT, PATCH methods."
198
+ ).format(title)
199
+ if "example" in rd:
200
+ ex["value"] = rd["example"]
201
+
202
+ examples[title] = ex
203
+
204
+ return examples
205
+
206
+
207
+ def request_bodies():
208
+ def _get_ref_examples(rd):
209
+ return {"$ref": "#/components/examples/%s" % rd["item_title"]}
210
+
211
+ rbodies = OrderedDict()
212
+
213
+ for resource_name, rd in app.config["DOMAIN"].items():
214
+ if resource_name.endswith("_versions") or rd.get("disable_documentation"):
215
+ continue
216
+
217
+ title = rd["item_title"]
218
+ rb = OrderedDict()
219
+ description = "A {} document.".format(title)
220
+ if rd["bulk_enabled"]:
221
+ description = "A {0} or list of {0} documents".format(title)
222
+
223
+ rb["description"] = description
224
+ rb["required"] = True
225
+ rb["content"] = {
226
+ # TODO what about other methods
227
+ "application/json": {
228
+ "schema": get_ref_schema(rd),
229
+ "examples": {title: _get_ref_examples(rd)},
230
+ }
231
+ }
232
+ rbodies[title] = rb
233
+
234
+ return rbodies
235
+
236
+
237
+ def headers():
238
+ pass
239
+
240
+
241
+ def security_schemes():
242
+ if "flask_oauthlib.provider" in sys.modules.keys():
243
+ url = app.config.get(HOST) or request.host
244
+ return {
245
+ "oAuth2": {
246
+ "type": "oauth2",
247
+ "description": "oAuth2 password credentials.",
248
+ "flows": {
249
+ "password": {
250
+ # TODO why does this not work with a relative path?
251
+ "tokenUrl": url
252
+ + app.config["SENTINEL_ROUTE_PREFIX"]
253
+ + app.config["SENTINEL_TOKEN_URL"],
254
+ "scopes": {},
255
+ }
256
+ },
257
+ }
258
+ }
259
+ if "eve_auth_jwt" in sys.modules.keys():
260
+ return {
261
+ "BearerAuth": {
262
+ "type": "http",
263
+ "scheme": "bearer",
264
+ "bearerFormat": "JWT",
265
+ "description": "Bearer authentication scheme.",
266
+ }
267
+ }
268
+ elif isinstance(app.auth, TokenAuth):
269
+ return {"BearerAuth": {"type": "http", "scheme": "bearer"}}
270
+ elif isinstance(app.auth, BasicAuth):
271
+ return {"BasicAuth": {"type": "http", "scheme": "basic"}}
272
+ else:
273
+ pass # FIXME
274
+ # TODO use app.auth to build the security scheme
275
+ # can not auto generate oauth, maybe should use add_documentation({...})
276
+
277
+
278
+ def links():
279
+ pass
280
+
281
+
282
+ def callbacks():
283
+ pass
284
+
285
+
286
+ def security():
287
+ if "flask_oauthlib.provider" in sys.modules.keys():
288
+ return [{"oAuth2": []}]
289
+ elif "eve_auth_jwt" in sys.modules.keys():
290
+ return [{"BearerAuth": []}]
291
+ elif isinstance(app.auth, TokenAuth):
292
+ return [{"BearerAuth": []}]
293
+ elif isinstance(app.auth, BasicAuth):
294
+ return [{"BasicAuth": []}]
295
+
296
+
297
+ def tags():
298
+ tags = []
299
+ for resource_name, rd in app.config["DOMAIN"].items():
300
+ if resource_name.endswith("_versions") or rd.get("disable_documentation"):
301
+ continue
302
+
303
+ tagInfo = {"name": rd["item_title"]}
304
+
305
+ if "description" in rd:
306
+ tagInfo["description"] = rd["description"]
307
+
308
+ tags.append(tagInfo)
309
+ return tags
310
+
311
+
312
+ def external_docs():
313
+ pass