Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
erp5
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Léo-Paul Géneau
erp5
Commits
86eeb759
Commit
86eeb759
authored
May 09, 2024
by
Jérome Perrin
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
open_api: support request body and fix py3 compatibility
parent
5e86291a
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
139 additions
and
25 deletions
+139
-25
bt5/erp5_open_api/DocumentTemplateItem/portal_components/document.erp5.OpenAPIService.py
...ateItem/portal_components/document.erp5.OpenAPIService.py
+36
-3
bt5/erp5_open_api/DocumentTemplateItem/portal_components/document.erp5.OpenAPITypeInformation.py
...portal_components/document.erp5.OpenAPITypeInformation.py
+15
-10
bt5/erp5_open_api/TestTemplateItem/portal_components/test.erp5.testOpenAPIService.py
...ateItem/portal_components/test.erp5.testOpenAPIService.py
+88
-12
No files found.
bt5/erp5_open_api/DocumentTemplateItem/portal_components/document.erp5.OpenAPIService.py
View file @
86eeb759
...
@@ -25,9 +25,13 @@
...
@@ -25,9 +25,13 @@
#
#
##############################################################################
##############################################################################
import
base64
import
binascii
import
json
import
json
import
typing
import
typing
import
six
from
six.moves.urllib.parse
import
unquote
from
six.moves.urllib.parse
import
unquote
if
typing
.
TYPE_CHECKING
:
if
typing
.
TYPE_CHECKING
:
from
typing
import
Any
,
Callable
,
Optional
from
typing
import
Any
,
Callable
,
Optional
from
erp5.component.document.OpenAPITypeInformation
import
OpenAPIOperation
,
OpenAPIParameter
from
erp5.component.document.OpenAPITypeInformation
import
OpenAPIOperation
,
OpenAPIParameter
...
@@ -258,10 +262,8 @@ class OpenAPIService(XMLObject):
...
@@ -258,10 +262,8 @@ class OpenAPIService(XMLObject):
parameter
,
parameter
,
parameter
.
getJSONSchema
(),
parameter
.
getJSONSchema
(),
)
)
requestBody
=
self
.
validateParameter
(
requestBody
=
self
.
validateRequestBody
(
'request body'
,
operation
.
getRequestBodyValue
(
request
),
operation
.
getRequestBodyValue
(
request
),
{},
operation
.
getRequestBodyJSONSchema
(
request
),
operation
.
getRequestBodyJSONSchema
(
request
),
)
)
if
requestBody
:
if
requestBody
:
...
@@ -296,6 +298,37 @@ class OpenAPIService(XMLObject):
...
@@ -296,6 +298,37 @@ class OpenAPIService(XMLObject):
parameter_name
=
parameter_name
,
e
=
e
.
message
),
str
(
e
))
parameter_name
=
parameter_name
,
e
=
e
.
message
),
str
(
e
))
return
parameter_value
return
parameter_value
security
.
declareProtected
(
Permissions
.
AccessContentsInformation
,
'validateRequestBody'
)
def
validateRequestBody
(
self
,
parameter_value
,
schema
):
# type: (str, dict) -> Any
"""Validate the request body raising a ParameterValidationError
when the parameter is not valid according to the corresponding schema.
"""
if
schema
is
not
None
:
if
schema
.
get
(
'type'
)
==
'string'
:
if
schema
.
get
(
'format'
)
==
'base64'
:
try
:
return
base64
.
b64decode
(
parameter_value
)
except
(
binascii
.
Error
,
TypeError
)
as
e
:
if
isinstance
(
e
,
TypeError
):
# BBB on python2 this raises a generic type error
# but we don't want to ignore potential TypeErrors
# on python3 here
if
six
.
PY3
:
raise
raise
ParameterValidationError
(
'Error validating request body: {e}'
.
format
(
e
=
str
(
e
)))
elif
schema
.
get
(
'format'
)
==
'binary'
:
return
parameter_value
or
b''
return
self
.
validateParameter
(
'request body'
,
parameter_value
,
{},
schema
,
)
def
executeMethod
(
self
,
request
):
def
executeMethod
(
self
,
request
):
# type: (HTTPRequest) -> Any
# type: (HTTPRequest) -> Any
operation
=
self
.
getMatchingOperation
(
request
)
operation
=
self
.
getMatchingOperation
(
request
)
...
...
bt5/erp5_open_api/DocumentTemplateItem/portal_components/document.erp5.OpenAPITypeInformation.py
View file @
86eeb759
...
@@ -90,7 +90,7 @@ ModuleSecurityInfo(__name__).declarePublic(
...
@@ -90,7 +90,7 @@ ModuleSecurityInfo(__name__).declarePublic(
)
)
# On python2, make sure we use UTF-8 strings for the json schemas, so that we don't
# On python2, make sure we use UTF-8 strings for the json schemas, so that we don't
# have
ugly u' prefix
s in the reprs. This also transforms the collections.OrderedDict
# have
ugly u' prefixe
s in the reprs. This also transforms the collections.OrderedDict
# to simple dicts, because the former also have an ugly representation.
# to simple dicts, because the former also have an ugly representation.
# http://stackoverflow.com/a/13105359
# http://stackoverflow.com/a/13105359
if
six
.
PY2
:
if
six
.
PY2
:
...
@@ -105,7 +105,7 @@ if six.PY2:
...
@@ -105,7 +105,7 @@ if six.PY2:
return
[
byteify
(
element
)
for
element
in
string
]
return
[
byteify
(
element
)
for
element
in
string
]
elif
isinstance
(
string
,
tuple
):
elif
isinstance
(
string
,
tuple
):
return
tuple
(
byteify
(
element
)
for
element
in
string
)
return
tuple
(
byteify
(
element
)
for
element
in
string
)
elif
isinstance
(
string
,
unicod
e
):
elif
isinstance
(
string
,
six
.
text_typ
e
):
return
string
.
encode
(
'utf-8'
)
return
string
.
encode
(
'utf-8'
)
else
:
else
:
return
string
return
string
...
@@ -175,13 +175,15 @@ class OpenAPIOperation(dict):
...
@@ -175,13 +175,15 @@ class OpenAPIOperation(dict):
# type: (HTTPRequest) -> Optional[dict]
# type: (HTTPRequest) -> Optional[dict]
"""Returns the schema for the request body, or None if no `requestBody` defined
"""Returns the schema for the request body, or None if no `requestBody` defined
"""
"""
request_content_type
=
request
.
getHeader
(
'content-type'
)
exact_request_content_type
=
request
.
getHeader
(
'content-type'
)
# TODO there might be $ref ?
wildcard_request_content_type
=
'%s/*'
%
((
exact_request_content_type
or
''
).
split
(
'/'
)[
0
])
request_body_definition
=
self
.
get
(
for
request_content_type
in
exact_request_content_type
,
wildcard_request_content_type
,
'*/*'
:
'requestBody'
,
{
'content'
:
{}})[
'content'
].
get
(
request_content_type
)
# TODO there might be $ref ?
if
request_body_definition
:
request_body_definition
=
self
.
get
(
return
SchemaWithComponents
(
'requestBody'
,
{
'content'
:
{}})[
'content'
].
get
(
request_content_type
)
self
.
_schema
,
request_body_definition
.
get
(
'schema'
,
{}))
if
request_body_definition
:
return
SchemaWithComponents
(
self
.
_schema
,
request_body_definition
.
get
(
'schema'
,
{}))
class
OpenAPIParameter
(
dict
):
class
OpenAPIParameter
(
dict
):
...
@@ -340,7 +342,10 @@ class OpenAPITypeInformation(ERP5TypeInformation):
...
@@ -340,7 +342,10 @@ class OpenAPITypeInformation(ERP5TypeInformation):
security
.
declareObjectProtected
(
Permissions
.
AccessContentsInformation
)
security
.
declareObjectProtected
(
Permissions
.
AccessContentsInformation
)
def
getSchema
(
self
):
def
getSchema
(
self
):
stream
=
io
.
BytesIO
(
self
.
getTextContent
()
or
b'{}'
)
text_content
=
self
.
getTextContent
()
or
'{}'
if
six
.
PY3
:
text_content
=
text_content
.
encode
()
stream
=
io
.
BytesIO
(
text_content
)
if
self
.
getContentType
()
==
'application/x-yaml'
:
if
self
.
getContentType
()
==
'application/x-yaml'
:
try
:
try
:
import
yaml
# pylint:disable=import-error
import
yaml
# pylint:disable=import-error
...
...
bt5/erp5_open_api/TestTemplateItem/portal_components/test.erp5.testOpenAPIService.py
View file @
86eeb759
...
@@ -25,6 +25,13 @@
...
@@ -25,6 +25,13 @@
#
#
##############################################################################
##############################################################################
import
six
# pylint:disable=no-name-in-module
if
six
.
PY2
:
from
base64
import
encodestring
as
base64_encodebytes
else
:
from
base64
import
encodebytes
as
base64_encodebytes
# pylint:enable=no-name-in-module
import
io
import
io
import
json
import
json
import
unittest
import
unittest
...
@@ -39,7 +46,7 @@ from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
...
@@ -39,7 +46,7 @@ from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
class
OpenAPITestCase
(
ERP5TypeTestCase
):
class
OpenAPITestCase
(
ERP5TypeTestCase
):
_type_id
=
NotImplemented
# type: str
_type_id
=
NotImplemented
# type: str
_open_api_schema
=
NotImplemented
# type:
bytes
_open_api_schema
=
NotImplemented
# type:
str
_open_api_schema_content_type
=
'application/json'
_open_api_schema_content_type
=
'application/json'
_public_api
=
True
_public_api
=
True
...
@@ -375,7 +382,7 @@ class TestOpenAPIServicePetController(OpenAPIPetStoreTestCase):
...
@@ -375,7 +382,7 @@ class TestOpenAPIServicePetController(OpenAPIPetStoreTestCase):
class
TestOpenAPIServiceYaml
(
OpenAPITestCase
):
class
TestOpenAPIServiceYaml
(
OpenAPITestCase
):
_type_id
=
'Test Open API YAML'
_type_id
=
'Test Open API YAML'
_open_api_schema_content_type
=
'application/x-yaml'
_open_api_schema_content_type
=
'application/x-yaml'
_open_api_schema
=
b
'''
_open_api_schema
=
'''
openapi: 3.0.3
openapi: 3.0.3
info:
info:
title: TestOpenAPIServiceYaml
title: TestOpenAPIServiceYaml
...
@@ -456,7 +463,7 @@ class TestPathParameterSerialization(OpenAPITestCase):
...
@@ -456,7 +463,7 @@ class TestPathParameterSerialization(OpenAPITestCase):
}
}
}
}
}
}
})
.
encode
()
})
def
test_primitive_parameter_serialization
(
self
):
def
test_primitive_parameter_serialization
(
self
):
self
.
addPythonScript
(
self
.
addPythonScript
(
...
@@ -532,7 +539,7 @@ class TestQueryParameterSerialization(OpenAPITestCase):
...
@@ -532,7 +539,7 @@ class TestQueryParameterSerialization(OpenAPITestCase):
}
}
}
}
}
}
})
.
encode
()
})
def
test_array_parameter_serialization
(
self
):
def
test_array_parameter_serialization
(
self
):
self
.
addPythonScript
(
self
.
addPythonScript
(
...
@@ -707,7 +714,7 @@ class TestOpenAPINonAsciiParameters(OpenAPIPetStoreTestCase):
...
@@ -707,7 +714,7 @@ class TestOpenAPINonAsciiParameters(OpenAPIPetStoreTestCase):
class
TestOpenAPICommonParameters
(
OpenAPIPetStoreTestCase
):
class
TestOpenAPICommonParameters
(
OpenAPIPetStoreTestCase
):
_type_id
=
'Test Open API Common Parameters'
_type_id
=
'Test Open API Common Parameters'
_open_api_schema
=
(
_open_api_schema
=
(
b
'''
'''
{
{
"openapi": "3.0.3",
"openapi": "3.0.3",
"info": {
"info": {
...
@@ -718,7 +725,7 @@ class TestOpenAPICommonParameters(OpenAPIPetStoreTestCase):
...
@@ -718,7 +725,7 @@ class TestOpenAPICommonParameters(OpenAPIPetStoreTestCase):
'''
'''
# https://swagger.io/docs/specification/describing-parameters/#common-for-path
# https://swagger.io/docs/specification/describing-parameters/#common-for-path
b
'''
'''
"/common-for-path": {
"/common-for-path": {
"parameters": [
"parameters": [
{
{
...
@@ -749,7 +756,7 @@ class TestOpenAPICommonParameters(OpenAPIPetStoreTestCase):
...
@@ -749,7 +756,7 @@ class TestOpenAPICommonParameters(OpenAPIPetStoreTestCase):
},'''
},'''
# https://swagger.io/docs/specification/describing-parameters/#common-for-various-paths
# https://swagger.io/docs/specification/describing-parameters/#common-for-various-paths
b
'''
'''
"/common-for-various-paths": {
"/common-for-various-paths": {
"get": {
"get": {
"operationId": "testGET2",
"operationId": "testGET2",
...
@@ -761,7 +768,7 @@ class TestOpenAPICommonParameters(OpenAPIPetStoreTestCase):
...
@@ -761,7 +768,7 @@ class TestOpenAPICommonParameters(OpenAPIPetStoreTestCase):
'''
'''
# here we also excercice $refs in parameter schemas
# here we also excercice $refs in parameter schemas
b
'''
'''
"$ref": "#/components/schemas/custom-number"
"$ref": "#/components/schemas/custom-number"
}
}
},
},
...
@@ -781,7 +788,7 @@ class TestOpenAPICommonParameters(OpenAPIPetStoreTestCase):
...
@@ -781,7 +788,7 @@ class TestOpenAPICommonParameters(OpenAPIPetStoreTestCase):
# https://spec.openapis.org/oas/v3.1.0#fixed-fields-6
# https://spec.openapis.org/oas/v3.1.0#fixed-fields-6
# $refs: Allows for a referenced definition of this path item.
# $refs: Allows for a referenced definition of this path item.
# The referenced structure MUST be in the form of a Path Item Object.
# The referenced structure MUST be in the form of a Path Item Object.
b
'''
'''
"/alias": {
"/alias": {
"$ref": "#/paths/~1common-for-path"
"$ref": "#/paths/~1common-for-path"
}
}
...
@@ -895,7 +902,7 @@ class TestOpenAPIMissingParameters(OpenAPIPetStoreTestCase):
...
@@ -895,7 +902,7 @@ class TestOpenAPIMissingParameters(OpenAPIPetStoreTestCase):
}
}
}
}
}
}
})
.
encode
()
})
def
test_required_query
(
self
):
def
test_required_query
(
self
):
self
.
addPythonScript
(
self
.
addPythonScript
(
...
@@ -980,7 +987,7 @@ class TestOpenAPIErrorHandling(OpenAPIPetStoreTestCase):
...
@@ -980,7 +987,7 @@ class TestOpenAPIErrorHandling(OpenAPIPetStoreTestCase):
self
.
addPythonScript
(
self
.
addPythonScript
(
'TestPetStoreOpenAPI_findPetsByStatus'
,
'TestPetStoreOpenAPI_findPetsByStatus'
,
'status'
,
'status'
,
'1/0'
,
'1/
/
0'
,
)
)
response
=
self
.
publish
(
response
=
self
.
publish
(
self
.
connector
.
getPath
()
+
'/pet/findByStatus?status=available'
)
self
.
connector
.
getPath
()
+
'/pet/findByStatus?status=available'
)
...
@@ -1097,7 +1104,7 @@ class TestPathParameterAndAcquisition(OpenAPIPetStoreTestCase):
...
@@ -1097,7 +1104,7 @@ class TestPathParameterAndAcquisition(OpenAPIPetStoreTestCase):
"""
"""
def
afterSetUp
(
self
):
def
afterSetUp
(
self
):
super
(
TestPathParameterAndAcquisition
,
self
).
afterSetUp
()
super
(
TestPathParameterAndAcquisition
,
self
).
afterSetUp
()
if
not
'789'
in
self
.
portal
.
portal_web_services
.
objectIds
():
if
'789'
not
in
self
.
portal
.
portal_web_services
.
objectIds
():
self
.
portal
.
portal_web_services
.
newContent
(
self
.
portal
.
portal_web_services
.
newContent
(
id
=
'789'
,
id
=
'789'
,
portal_type
=
self
.
portal
.
portal_web_services
.
allowedContentTypes
()
portal_type
=
self
.
portal
.
portal_web_services
.
allowedContentTypes
()
...
@@ -1242,3 +1249,72 @@ class TestURLPathWithWebSiteAndVirtualHost(OpenAPIPetStoreTestCase):
...
@@ -1242,3 +1249,72 @@ class TestURLPathWithWebSiteAndVirtualHost(OpenAPIPetStoreTestCase):
self
.
connector
.
getRelativeUrl
()
self
.
connector
.
getRelativeUrl
()
))
))
self
.
assertEqual
(
response
.
getBody
(),
b'"ok"'
)
self
.
assertEqual
(
response
.
getBody
(),
b'"ok"'
)
class
TestOpenAPIRequestBody
(
OpenAPITestCase
):
_type_id
=
'Test Open API Request Body'
_open_api_schema
=
json
.
dumps
(
{
'openapi'
:
'3.0.3'
,
'info'
:
{
'title'
:
'TestOpenAPIRequestBody'
,
'version'
:
'0.0.0'
},
'paths'
:
{
'/post'
:
{
'post'
:
{
'operationId'
:
'testPostByContentType'
,
'requestBody'
:
{
'content'
:
{
'image/*'
:
{
'schema'
:
{
'type'
:
'string'
,
'format'
:
'binary'
,
}
},
'application/x-base64'
:
{
'schema'
:
{
'type'
:
'string'
,
'format'
:
'base64'
,
}
}
}
}
}
}
}
})
def
test_request_body_content_encoding
(
self
):
self
.
addPythonScript
(
'TestOpenAPIRequestBody_testPostByContentType'
,
'body=None'
,
'container.REQUEST.RESPONSE.setHeader("Content-Type", "application/octet-stream")
\
n
'
'return body'
,
)
response
=
self
.
publish
(
self
.
connector
.
getPath
()
+
'/post'
,
request_method
=
'POST'
,
stdin
=
io
.
BytesIO
(
b'png file content'
),
env
=
{
"CONTENT_TYPE"
:
'image/png'
})
self
.
assertEqual
(
response
.
getBody
(),
b'png file content'
)
self
.
assertEqual
(
response
.
getStatus
(),
200
)
response
=
self
.
publish
(
self
.
connector
.
getPath
()
+
'/post'
,
request_method
=
'POST'
,
stdin
=
io
.
BytesIO
(
base64_encodebytes
(
b'base64 file content'
)),
env
=
{
"CONTENT_TYPE"
:
'application/x-base64'
})
self
.
assertEqual
(
response
.
getBody
(),
b'base64 file content'
)
self
.
assertEqual
(
response
.
getStatus
(),
200
)
response
=
self
.
publish
(
self
.
connector
.
getPath
()
+
'/post'
,
request_method
=
'POST'
,
stdin
=
io
.
BytesIO
(
b'not base64'
),
env
=
{
"CONTENT_TYPE"
:
'application/x-base64'
})
self
.
assertEqual
(
response
.
getStatus
(),
400
)
body
=
json
.
loads
(
response
.
getBody
())
self
.
assertEqual
(
body
[
'type'
],
'parameter-validation-error'
)
self
.
assertIn
(
'Error validating request body:'
,
body
[
'title'
])
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment