Skip to content

Commit

Permalink
✨Implement a parser with GraphQL like syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
yezyilomo committed Dec 2, 2019
1 parent af294cb commit 52feec2
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 67 deletions.
223 changes: 158 additions & 65 deletions controllers/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,162 @@
import logging
import requests
import datetime
from itertools import chain

from odoo import http
from odoo.http import request

from .parser import Parser


_logger = logging.getLogger(__name__)

try:
import dictfier
except ImportError as err:
_logger.debug(err)


def flat_obj(obj, parent_obj, field_name):
if isinstance(obj, datetime.datetime):
return obj.strftime("%Y-%m-%d-%H-%M")
if isinstance(obj, datetime.date):
return obj.strftime("%Y-%m-%d")
if isinstance(obj, datetime.time):
return obj.strftime("%H-%M-%S")

if hasattr(parent_obj, "fields_get"):
field = parent_obj.fields_get(field_name)[field_name]
field_type = field["type"]
if field_type == "many2one":
return obj.id
if field_type in ["one2many", "many2many"]:
return [rec.id for rec in obj]
if field_type == "binary" and obj:
return obj.decode("utf-8")

return obj

def nested_flat_obj(obj, parent_obj):
return obj

def nested_iter_obj(obj, parent_obj):
return obj

class Serializer(object):
def __init__(self, record, query="{*}", many=False):
self.many = many
self._record = record
self._raw_query = query
super().__init__()

def get_parsed_restql_query(self):
parser = Parser(self._raw_query)
try:
parsed_restql_query = parser.get_parsed()
return parsed_restql_query
except SyntaxError as e:
msg = (
"QueryFormatError: " +
e.msg + " on " +
e.text
)
raise SyntaxError(msg) from None

@property
def data(self):
parsed_restql_query = self.get_parsed_restql_query()
if self.many:
return [
self.serialize(rec, parsed_restql_query)
for rec
in self._record
]
return self.serialize(self._record, parsed_restql_query)

@classmethod
def build_flat_field(cls, rec, field_name):
all_fields = rec.fields_get_keys()
if field_name not in all_fields:
msg = "'%s' field is not found" % field_name
raise LookupError(msg)
field_type = rec.fields_get(field_name).get(field_name).get('type')
if field_type in ['one2many', 'many2many']:
return {
field_name: [record.id for record in rec[field_name]]
}
elif field_type in ['many2one']:
return {field_name: rec[field_name].id}
elif field_type == 'datetime' and rec[field_name]:
return {
field_name: rec[field_name].strftime("%Y-%m-%d-%H-%M")
}
elif field_type == 'date' and rec[field_name]:
return {
field_name: rec[field_name].strftime("%Y-%m-%d")
}
elif field_type == 'time' and rec[field_name]:
return {
field_name: rec[field_name].strftime("%H-%M-%S")
}
elif field_type == "binary" and rec[field_name]:
return {field_name: rec[field_name].decode("utf-8")}
else:
return {field_name: rec[field_name]}

@classmethod
def build_nested_field(cls, rec, field_name, nested_parsed_query):
all_fields = rec.fields_get_keys()
if field_name not in all_fields:
msg = "'%s' field is not found" % field_name
raise LookupError(msg)
field_type = rec.fields_get(field_name).get(field_name).get('type')
if field_type in ['one2many', 'many2many']:
return {
field_name: [
cls.serialize(record, nested_parsed_query)
for record
in rec[field_name]
]
}
elif field_type in ['many2one']:
return {
field_name: cls.serialize(rec[field_name], nested_parsed_query)
}
else:
# Not a neste field
msg = "'%s' is not a nested field" % field_name
raise ValueError(msg)

@classmethod
def serialize(cls, rec, parsed_query):
data = {}

# NOTE: self.parsed_restql_query["include"] not being empty
# is not a guarantee that the exclude operator(-) has not been
# used because the same self.parsed_restql_query["include"]
# is used to store nested fields when the exclude operator(-) is used
if parsed_query["exclude"]:
# Exclude fields from a query
all_fields = rec.fields_get_keys()
for field in parsed_query["include"]:
if field == "*":
continue
for nested_field, nested_parsed_query in field.items():
built_nested_field = cls.build_nested_field(
rec,
nested_field,
nested_parsed_query
)
data.update(built_nested_field)

flat_fields= set(all_fields).symmetric_difference(set(parsed_query['exclude']))
for field in flat_fields:
flat_field = cls.build_flat_field(rec, field)
data.update(flat_field)

elif parsed_query["include"]:
# Here we are sure that self.parsed_restql_query["exclude"]
# is empty which means the exclude operator(-) is not used,
# so self.parsed_restql_query["include"] contains only fields
# to include
all_fields = rec.fields_get_keys()
if "*" in parsed_query['include']:
# Include all fields
parsed_query['include'] = filter(
lambda item: item != "*",
parsed_query['include']
)
fields = chain(parsed_query['include'], all_fields)
parsed_query['include'] = reversed(list(fields))

for field in parsed_query["include"]:
if isinstance(field, dict):
for nested_field, nested_parsed_query in field.items():
built_nested_field = cls.build_nested_field(
rec,
nested_field,
nested_parsed_query
)
data.update(built_nested_field)
else:
flat_field = cls.build_flat_field(rec, field)
data.update(flat_field)
else:
# The query is empty i.e query={}
# return nothing
return {}
return data


class OdooAPI(http.Controller):
@http.route('/auth/',
Expand Down Expand Up @@ -124,16 +243,9 @@ def call_obj_function(self, model, rec_id, function, **post):
def get_model_data(self, model, **params):
records = request.env[model].search([])
if "query" in params:
query = json.loads(params["query"])
query = params["query"]
else:
query = [records.fields_get_keys()]

if "exclude" in params:
exclude = json.loads(params["exclude"])
for field in exclude:
if field in query[0]:
field_to_exclude= query[0].index(field)
query[0].pop(field_to_exclude)
query = "{*}"

if "filter" in params:
filters = json.loads(params["filter"])
Expand Down Expand Up @@ -167,21 +279,15 @@ def get_model_data(self, model, **params):
limit = int(params["limit"])
records = records[0:limit]

data = dictfier.dictfy(
records,
query,
flat_obj=flat_obj,
nested_flat_obj=nested_flat_obj,
nested_iter_obj=nested_iter_obj
)
serializer = Serializer(records, query ,many=True)

res = {
"count": len(records),
"prev": prev_page,
"current": current_page,
"next": next_page,
"total_pages": total_page_number,
"result": data
"result": serializer.data
}
return http.Response(
json.dumps(res),
Expand All @@ -195,27 +301,14 @@ def get_model_data(self, model, **params):
def get_model_rec(self, model, rec_id, **params):
records = request.env[model].search([])
if "query" in params:
query = json.loads(params["query"])
query = params["query"]
else:
query = records.fields_get_keys()

if "exclude" in params:
exclude = json.loads(params["exclude"])
for field in exclude:
if field in query:
field_to_exclude = query.index(field)
query.pop(field_to_exclude)
query = "{*}"

record = records.browse(rec_id).ensure_one()
data = dictfier.dictfy(
record,
query,
flat_obj=flat_obj,
nested_flat_obj=nested_flat_obj,
nested_iter_obj=nested_iter_obj
)
serializer = Serializer(record, query)
return http.Response(
json.dumps(data),
json.dumps(serializer.data),
status=200,
mimetype='application/json'
)
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
dictfier==1.5.1
requests
pypeg2
requests

0 comments on commit 52feec2

Please sign in to comment.