Commit 2e607f8d authored by Noé Gaumont's avatar Noé Gaumont 🐙
Browse files

ci: add black

parent bb254572ba40
......@@ -10,6 +10,7 @@ include:
- "templates/no-duplicated-ci-pipelines.yml" # use workflow to avoid duplicated pipelines
- "templates/lint/flake8.yml" # will do the equivalent of 'tox -e flake8'
- "templates/lint/check-manifest.yml" # will do the equivalent of 'tox -e check-manifest'
- "templates/lint/black.yml" # will do the equivalent of 'tox -e black'
- "templates/tests/py3.yml" # will do the equivalent of 'tox -e py3'
- "templates/create-release-on-heptapod.yml" # this will create a release on heptapod
- "templates/upload-to-pypi.yml" # on a new mercurial tag (expected to be done with release-new), will push a release on pypi
......
# pylint: disable=W0622
"""cubicweb-rqlcontroller application packaging information"""
modname = 'rqlcontroller'
distname = 'cubicweb-rqlcontroller'
modname = "rqlcontroller"
distname = "cubicweb-rqlcontroller"
numversion = (0, 7, 2)
version = '.'.join(str(num) for num in numversion)
version = ".".join(str(num) for num in numversion)
license = 'LGPL'
author = 'LOGILAB S.A. (Paris, FRANCE)'
author_email = 'contact@logilab.fr'
description = 'restfull rql edition capabilities'
web = 'http://www.cubicweb.org/project/%s' % distname
license = "LGPL"
author = "LOGILAB S.A. (Paris, FRANCE)"
author_email = "contact@logilab.fr"
description = "restfull rql edition capabilities"
web = "http://www.cubicweb.org/project/%s" % distname
__depends__ = {
'cubicweb': '>= 3.32.2',
'six': None,
"cubicweb": ">= 3.32.2",
"six": None,
}
__recommends__ = {'cubicweb-signedrequest': None}
__recommends__ = {"cubicweb-signedrequest": None}
classifiers = [
'Environment :: Web Environment',
'Framework :: CubicWeb',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: JavaScript',
"Environment :: Web Environment",
"Framework :: CubicWeb",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: JavaScript",
]
......@@ -22,52 +22,55 @@ from cubicweb.utils import json_dumps
class RqlIOSchemaHolder(object):
"""Class used to load the schema and its hash.
Both can be fetched by the client via dedicated views,
so it can decide to update the locally stored schema if
the integrity is not valid.
Both can be fetched by the client via dedicated views,
so it can decide to update the locally stored schema if
the integrity is not valid.
"""
_schema = None
_hash = None
@staticmethod
def format_schema(typeNames,
specializes,
attributeNames,
relationsTo,
relationsFrom):
def format_schema(
typeNames, specializes, attributeNames, relationsTo, relationsFrom
):
return {
"eid": typeNames[0][0],
"name": typeNames[0][1],
"modificationDate": typeNames[0][2].strftime("%Y/%m/%d %H:%M:%S"),
"specializes": specializes[0][0] if specializes else None,
"attributes": [{
"eid": row[0],
"name": row[1],
"type": row[2],
"cardinality": row[3]
} for row in attributeNames],
"relationsTo": [{
"eid": row[0],
"name": row[1],
"from": row[2],
"fromName": typeNames[0][1],
"fromCardinality": row[6],
"to": row[3],
"toName": row[4],
"toCardinality": row[6],
"description": row[5]
} for row in relationsTo],
"relationsFrom": [{
"eid": row[0],
"name": row[1],
"from": row[2],
"fromName": row[3],
"fromCardinality": row[6],
"to": row[4],
"toName": typeNames[0][1],
"toCardinality": row[6],
"description": row[5]
} for row in relationsFrom]
"attributes": [
{"eid": row[0], "name": row[1], "type": row[2], "cardinality": row[3]}
for row in attributeNames
],
"relationsTo": [
{
"eid": row[0],
"name": row[1],
"from": row[2],
"fromName": typeNames[0][1],
"fromCardinality": row[6],
"to": row[3],
"toName": row[4],
"toCardinality": row[6],
"description": row[5],
}
for row in relationsTo
],
"relationsFrom": [
{
"eid": row[0],
"name": row[1],
"from": row[2],
"fromName": row[3],
"fromCardinality": row[6],
"to": row[4],
"toName": typeNames[0][1],
"toCardinality": row[6],
"description": row[5],
}
for row in relationsFrom
],
}
@classmethod
......@@ -76,17 +79,15 @@ class RqlIOSchemaHolder(object):
return cls._schema
entities = []
etypes = cnx.execute('Any T WHERE T is CWEType')
etypes = cnx.execute("Any T WHERE T is CWEType")
for etype in etypes:
eid = etype[0]
rql_requests = [
(
f'Any TYPE,NAME,MD WHERE TYPE eid {eid}, TYPE name NAME, '
'TYPE modification_date MD'
),
(
f'Any PARENT WHERE TYPE eid {eid}, TYPE specializes PARENT'
f"Any TYPE,NAME,MD WHERE TYPE eid {eid}, TYPE name NAME, "
"TYPE modification_date MD"
),
(f"Any PARENT WHERE TYPE eid {eid}, TYPE specializes PARENT"),
(
f"""Any ATTR,NAME,ATTRTYPENAME,CARD ORDERBY ON WHERE TYPE eid {eid},
ATTR from_entity TYPE,
......@@ -123,7 +124,7 @@ class RqlIOSchemaHolder(object):
ATTR is CWRelation,
ATTR cardinality CARD?,
ATTR ordernum ON"""
)
),
]
rsets = [cnx.execute(request) for request in rql_requests]
entities.append(RqlIOSchemaHolder.format_schema(*rsets))
......
......@@ -36,8 +36,8 @@ from cubicweb import Binary
from cubicweb_rqlcontroller.rql_schema_holder import RqlIOSchemaHolder
ARGRE = re.compile(r'__r(?P<ref>\d+)$')
DATARE = re.compile(r'__f(?P<ref>.+)$')
ARGRE = re.compile(r"__r(?P<ref>\d+)$")
DATARE = re.compile(r"__f(?P<ref>.+)$")
def rewrite_args(args, output, form):
......@@ -46,19 +46,20 @@ def rewrite_args(args, output, form):
continue
match = ARGRE.match(v)
if match:
numref = int(match.group('ref'))
numref = int(match.group("ref"))
if 0 <= numref <= len(output):
rset = output[numref]
if not rset:
raise Exception('%s references empty result set %s' %
(v, rset))
raise Exception("%s references empty result set %s" % (v, rset))
if len(rset) > 1:
raise Exception('%s references multi lines result set %s' %
(v, rset))
raise Exception(
"%s references multi lines result set %s" % (v, rset)
)
row = rset.rows[0]
if len(row) > 1:
raise Exception(
'%s references multi column result set %s' % (v, rset))
"%s references multi column result set %s" % (v, rset)
)
args[k] = row[0]
continue
match = DATARE.match(v)
......@@ -68,65 +69,73 @@ def rewrite_args(args, output, form):
class match_request_content_type(ExpectedValuePredicate):
"""check that the request body has the right content type"""
def _get_value(self, cls, req, **kwargs):
header = req.get_header('Content-Type', None)
header = req.get_header("Content-Type", None)
if header is not None:
header = header.split(';', 1)[0].strip()
header = header.split(";", 1)[0].strip()
return header
class RqlIOSchemaController(Controller):
__regid__ = 'rqlio_schema'
__select__ = match_http_method('GET', 'HEAD')
__regid__ = "rqlio_schema"
__select__ = match_http_method("GET", "HEAD")
def publish(self, rset=None):
self._cw.set_content_type('application/json')
self._cw.add_header('Etag', RqlIOSchemaHolder.get_schema_hash(self._cw))
return json.dumps(
RqlIOSchemaHolder.get_schema(self._cw)
).encode(self._cw.encoding)
self._cw.set_content_type("application/json")
self._cw.add_header("Etag", RqlIOSchemaHolder.get_schema_hash(self._cw))
return json.dumps(RqlIOSchemaHolder.get_schema(self._cw)).encode(
self._cw.encoding
)
class RqlIOController(Controller):
"""posted rql queries and arguments use the following pattern:
[('INSERT CWUser U: U login %(login)s, U upassword %(pw)s',
{'login': 'babar', 'pw': 'cubicweb rulez & 42'}),
('INSERT CWGroup G: G name %(name)s',
{'name': 'pachyderms'}),
('SET U in_group G WHERE G eid %(g)s, U eid %(u)s',
{'u': '__r0', 'g': '__r1'}),
('INSERT File F: F data %(content)s, F data_name %(fname)s',
{'content': '__f0', 'fname': 'toto.txt'}),
]
[('INSERT CWUser U: U login %(login)s, U upassword %(pw)s',
{'login': 'babar', 'pw': 'cubicweb rulez & 42'}),
('INSERT CWGroup G: G name %(name)s',
{'name': 'pachyderms'}),
('SET U in_group G WHERE G eid %(g)s, U eid %(u)s',
{'u': '__r0', 'g': '__r1'}),
('INSERT File F: F data %(content)s, F data_name %(fname)s',
{'content': '__f0', 'fname': 'toto.txt'}),
]
The later query is an example of query built to upload binety
data as a file object. It requires to have a multipart query
in which there is a part holding a file named '__f0'. See
cwclientlib for examples of such queries.
The later query is an example of query built to upload binety
data as a file object. It requires to have a multipart query
in which there is a part holding a file named '__f0'. See
cwclientlib for examples of such queries.
Limitations: back references can only work if one entity has been
created.
Limitations: back references can only work if one entity has been
created.
"""
__regid__ = 'rqlio'
__select__ = (match_http_method('POST')
& match_request_content_type(
'application/json', 'multipart/form-data', mode='any')
& match_form_params('version'))
__regid__ = "rqlio"
__select__ = (
match_http_method("POST")
& match_request_content_type(
"application/json", "multipart/form-data", mode="any"
)
& match_form_params("version")
)
require_csrf = False
def json(self):
contenttype = self._cw.get_header('Content-Type', raw=False)
if (contenttype.mediaType, contenttype.mediaSubtype) == ('application', 'json'): # noqa: E501
encoding = contenttype.params.get('charset', 'utf-8')
contenttype = self._cw.get_header("Content-Type", raw=False)
if (contenttype.mediaType, contenttype.mediaSubtype) == (
"application",
"json",
): # noqa: E501
encoding = contenttype.params.get("charset", "utf-8")
content = self._cw.content
else:
# Multipart content is usually built by
# cubicweb.multipart.parse_form_data() which encodes using
# "utf-8" by default.
encoding = 'utf-8'
content = self._cw.form['json'][1]
encoding = "utf-8"
content = self._cw.form["json"][1]
try:
# here we use .read instead of .gevalue because
# on some circumstances content is an instance of BufferedRandom
......@@ -140,11 +149,11 @@ class RqlIOController(Controller):
def publish(self, rset=None):
self._cw.ajax_request = True
self._cw.set_content_type('application/json')
self._cw.set_content_type("application/json")
version = self._cw.form['version']
if version not in ('1.0', '2.0'):
raise RemoteCallFailed('unknown rqlio version %r', version)
version = self._cw.form["version"]
if version not in ("1.0", "2.0"):
raise RemoteCallFailed("unknown rqlio version %r", version)
args = self.json()
try:
......@@ -154,7 +163,7 @@ class RqlIOController(Controller):
except Exception as exc:
raise RemoteCallFailed(exc_message(exc, self._cw.encoding))
if result is None:
return b''
return b""
return json_dumps(result).encode(self._cw.encoding)
def rqlio(self, version, *rql_args):
......@@ -165,8 +174,8 @@ class RqlIOController(Controller):
raise
else:
self._cw.cnx.commit()
if version == '2.0':
return [{'rows': o.rows, 'variables': o.variables} for o in output]
if version == "2.0":
return [{"rows": o.rows, "variables": o.variables} for o in output]
return [o.rows for o in output]
def _rqlio(self, rql_args):
......@@ -181,8 +190,9 @@ class RqlIOController(Controller):
class RQLIORewriter(SchemaBasedRewriter):
rules = [
(re.compile('/rqlio/schema'),
rgx_action(controller='rqlio_schema')),
(re.compile('/rqlio/(?P<version>.+)$'),
rgx_action(controller='rqlio', formgroups=('version',)))
(re.compile("/rqlio/schema"), rgx_action(controller="rqlio_schema")),
(
re.compile("/rqlio/(?P<version>.+)$"),
rgx_action(controller="rqlio", formgroups=("version",)),
),
]
......@@ -30,34 +30,35 @@ here = dirname(__file__)
# load metadata from the __pkginfo__.py file so there is no risk of conflict
# see https://packaging.python.org/en/latest/single_source_version.html
pkginfo = join(here, 'cubicweb_rqlcontroller', '__pkginfo__.py')
pkginfo = join(here, "cubicweb_rqlcontroller", "__pkginfo__.py")
__pkginfo__ = {}
with open(pkginfo) as f:
exec(f.read(), __pkginfo__)
# get required metadatas
distname = __pkginfo__['distname']
version = __pkginfo__['version']
license = __pkginfo__['license']
description = __pkginfo__['description']
web = __pkginfo__['web']
author = __pkginfo__['author']
author_email = __pkginfo__['author_email']
classifiers = __pkginfo__['classifiers']
distname = __pkginfo__["distname"]
version = __pkginfo__["version"]
license = __pkginfo__["license"]
description = __pkginfo__["description"]
web = __pkginfo__["web"]
author = __pkginfo__["author"]
author_email = __pkginfo__["author_email"]
classifiers = __pkginfo__["classifiers"]
with open(join(here, 'README.rst')) as f:
with open(join(here, "README.rst")) as f:
long_description = f.read()
# get optional metadatas
data_files = __pkginfo__.get('data_files', None)
dependency_links = __pkginfo__.get('dependency_links', ())
data_files = __pkginfo__.get("data_files", None)
dependency_links = __pkginfo__.get("dependency_links", ())
requires = {}
for entry in ("__depends__",): # "__recommends__"):
requires.update(__pkginfo__.get(entry, {}))
install_requires = ["{0} {1}".format(d, v and v or "").strip()
for d, v in requires.items()]
install_requires = [
"{0} {1}".format(d, v and v or "").strip() for d, v in requires.items()
]
setup(
......@@ -70,14 +71,13 @@ setup(
author_email=author_email,
url=web,
classifiers=classifiers,
packages=find_packages(exclude=['test']),
packages=find_packages(exclude=["test"]),
install_requires=install_requires,
include_package_data=True,
entry_points={
'cubicweb.cubes': [
'rqlcontroller=cubicweb_rqlcontroller',
"cubicweb.cubes": [
"rqlcontroller=cubicweb_rqlcontroller",
],
},
zip_safe=False,
)
......@@ -22,58 +22,73 @@ from cubicweb.pyramid.test import PyramidCWTest
class RqlIOTC(PyramidCWTest):
settings = {"cubicweb.bwcompat": True}
password = 'gingkow'
password = "gingkow"
def setup_database(self):
with self.admin_access.client_cnx() as cnx:
self.create_user(cnx, u'toto', password=u'toto')
self.create_user(cnx, u"toto", password=u"toto")
cnx.commit()
def assertRQLPostOK(self, queries, code=200, version='1.0'):
return self.webapp.post('/rqlio/%s' % version,
params=json.dumps(queries),
headers={'Content-Type': 'application/json'},
do_not_grab_the_crsf_token=True)
def assertRQLPostOK(self, queries, code=200, version="1.0"):
return self.webapp.post(
"/rqlio/%s" % version,
params=json.dumps(queries),
headers={"Content-Type": "application/json"},
do_not_grab_the_crsf_token=True,
)
def assertRQLPostKO(self, queries, reason, code=500):
res_ko = self.webapp.post('/rqlio/1.0',
params=json.dumps(queries),
headers={'Content-Type': 'application/json'},
status=code,
do_not_grab_the_crsf_token=True)
self.assertIn(reason, res_ko.json[u'reason'])
res_ko = self.webapp.post(
"/rqlio/1.0",
params=json.dumps(queries),
headers={"Content-Type": "application/json"},
status=code,
do_not_grab_the_crsf_token=True,
)
self.assertIn(reason, res_ko.json[u"reason"])
return res_ko
def test_queries(self):
queries = [('INSERT CWUser U: U login %(l)s, U upassword %(p)s',
{'l': 'Babar', 'p': 'cubicweb rulez & 42'}),
('INSERT CWGroup G: G name "pachyderms"', {}),
('SET U in_group G WHERE U eid %(u)s, G eid %(g)s',
{'u': '__r0', 'g': '__r1'})]
queries = [
(
"INSERT CWUser U: U login %(l)s, U upassword %(p)s",
{"l": "Babar", "p": "cubicweb rulez & 42"},
),
('INSERT CWGroup G: G name "pachyderms"', {}),
(
"SET U in_group G WHERE U eid %(u)s, G eid %(g)s",
{"u": "__r0", "g": "__r1"},
),
]
# as an anonymous user
reason = (u'You are not allowed to perform add operation on relation'
' CWUser in_group CWGroup')
reason = (
u"You are not allowed to perform add operation on relation"
" CWUser in_group CWGroup"
)
# should really be 403 if it wasn't for cubicweb brokenness
self.assertRQLPostKO(queries, reason, code=500)
# as a standard user
self.webapp.login(user='toto', password='toto')
reason = (u'You are not allowed to perform add operation on relation'
' CWUser in_group CWGroup')
self.webapp.login(user="toto", password="toto")
reason = (
u"You are not allowed to perform add operation on relation"
" CWUser in_group CWGroup"
)
# should really be 403 if it wasn't for cubicweb brokenness
self.assertRQLPostKO(queries, reason, code=500)
# logout
self.webapp.reset()
# now, as an admin
self.webapp.login(user='admin', password=self.password)
self.webapp.login(user="admin", password=self.password)
res = self.assertRQLPostOK(queries)
with self.admin_access.client_cnx() as cnx:
rset = cnx.execute('String N WHERE U in_group G, U login "Babar", '
'G name N')
self.assertEqual('pachyderms', rset.rows[0][0])
rset = cnx.execute(
'String N WHERE U in_group G, U login "Babar", ' "G name N"
)
self.assertEqual("pachyderms", rset.rows[0][0])
output = [x for x, in res.json]
self.assertEqual(1, len(output[0]))
self.assertEqual(1, len(output[1]))
......@@ -82,68 +97,72 @@ class RqlIOTC(PyramidCWTest):
def test_queries_multipart(self):
queries = [
('INSERT CWUser U: U login %(l)s, U upassword %(p)s',
{'l': 'Babar', 'p': 'cubicweb rulez & 42'}),
(
"INSERT CWUser U: U login %(l)s, U upassword %(p)s",
{"l": "Babar", "p": "cubicweb rulez & 42"},
),
('INSERT CWGroup G: G name "pachyderms"', {}),
('SET U in_group G WHERE U eid %(u)s, G eid %(g)s',
{'u': '__r0', 'g': '__r1'}),
(
"SET U in_group G WHERE U eid %(u)s, G eid %(g)s",
{"u": "__r0", "g": "__r1"},
),
]
self.webapp.login(user='admin', password=self.password)
files = [('json', 'loutre.json', json.dumps(queries).encode('utf-8'))]
self.webapp.post('/rqlio/1.0', upload_files=files,
headers={
'Accept': 'application/json',
},
do_not_grab_the_crsf_token=True
)
self.webapp.login(user="admin", password=self.password)
files = [("json", "loutre.json", json.dumps(queries).encode("utf-8"))]
self.webapp.post(
"/rqlio/1.0",
upload_files=files,
headers={
"Accept": "application/json",
},
do_not_grab_the_crsf_token=True,
)
def test_rewrite_args_errors(self):
rql1 = 'Any U WHERE U login %(l)s'
rql1 = "Any U WHERE U login %(l)s"
rql2 = 'SET U in_group G WHERE G name "managers", U eid %(u)s'
args2 = {'u': '__r0'}
args2 = {"u": "__r0"}
# setup test
self.webapp.login(user='admin', password=self.password)
self.webapp.login(user="admin", password=self.password)
# check ok
queries_ok = [(rql1, {'l': 'toto'}), (rql2, args2)]
queries_ok = [(rql1, {"l": "toto"}), (rql2, args2)]
self.assertRQLPostOK(queries_ok)
# check ko (1)
queries_ko = [(rql1, {'l': 'doesnotexist'}),
(rql2, args2)]
self.assertRQLPostKO(queries_ko,
"__r0 references empty result set")
queries_ko = [(rql1, {"l": "doesnotexist"}), (rql2, args2)]
self.assertRQLPostKO(queries_ko, "__r0 references empty result set")
# check ko (2)