CORD-762 add modeldefs API
Change-Id: Ieed10ec8d40533906c8956d2b8c2ea62709bd8b4
diff --git a/xos/grpc/grpc_client.py b/xos/grpc/grpc_client.py
index 52f7f90..8defd42 100644
--- a/xos/grpc/grpc_client.py
+++ b/xos/grpc/grpc_client.py
@@ -2,7 +2,7 @@
import grpc
from protos.common_pb2 import *
from protos.xos_pb2 import *
-from protos import xos_pb2_grpc
+from protos import xos_pb2_grpc, modeldefs_pb2_grpc
from google.protobuf.empty_pb2 import Empty
from grpc import metadata_call_credentials, ChannelCredentials, composite_channel_credentials, ssl_channel_credentials
@@ -28,6 +28,7 @@
super(InsecureClient,self).__init__(hostname, port)
self.channel = grpc.insecure_channel("%s:%d" % (self.hostname, self.port))
self.stub = xos_pb2_grpc.xosStub(self.channel)
+ self.modeldefs = modeldefs_pb2_grpc.modeldefsStub(self.channel)
class SecureClient(XOSClient):
def __init__(self, hostname, port=50051, cacert=SERVER_CA, username=None, password=None):
@@ -40,6 +41,7 @@
self.channel = grpc.secure_channel("%s:%d" % (self.hostname, self.port), chan_creds)
self.stub = xos_pb2_grpc.xosStub(self.channel)
+ self.modeldefs = modeldefs_pb2_grpc.modeldefsStub(self.channel)
def main(): # self-test
client = InsecureClient("xos-core.cord.lab")
diff --git a/xos/grpc/grpc_server.py b/xos/grpc/grpc_server.py
index ec7cc9e..d231bfc 100644
--- a/xos/grpc/grpc_server.py
+++ b/xos/grpc/grpc_server.py
@@ -29,8 +29,9 @@
sys.path.append('/opt/xos')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
-from protos import xos_pb2, schema_pb2
+from protos import xos_pb2, schema_pb2, modeldefs_pb2
from xos_grpc_api import XosService
+from xos_modeldefs_api import ModelDefsService
from google.protobuf.empty_pb2 import Empty
from xos.logger import Logger, logging
@@ -116,6 +117,7 @@
for activator_func, service_class in (
(schema_pb2.add_SchemaServiceServicer_to_server, SchemaService),
(xos_pb2.add_xosServicer_to_server, XosService),
+ (modeldefs_pb2.add_modeldefsServicer_to_server, ModelDefsService),
):
service = service_class(self.thread_pool)
self.register(activator_func, service)
diff --git a/xos/grpc/protos/modeldefs.proto b/xos/grpc/protos/modeldefs.proto
new file mode 100644
index 0000000..d738c49
--- /dev/null
+++ b/xos/grpc/protos/modeldefs.proto
@@ -0,0 +1,50 @@
+syntax = "proto3";
+
+package xos;
+
+import "google/protobuf/empty.proto";
+import "google/api/annotations.proto";
+import "common.proto";
+
+// This API is used by the UI to validate fields.
+
+message FieldValidator {
+ string name = 1;
+ oneof val_value {
+ bool bool_value = 2;
+ int32 int_value = 3;
+ string str_value = 4;
+ };
+};
+
+message FieldRelation {
+ string model = 1;
+ string type = 2;
+};
+
+message ModelField {
+ string name = 1;
+ string hint = 2;
+ string type = 3;
+ FieldRelation relation = 4;
+ repeated FieldValidator validators = 5;
+};
+
+message ModelDef {
+ string name = 1;
+ repeated ModelField fields = 2;
+ repeated FieldRelation relations = 3;
+};
+
+message ModelDefs {
+ repeated ModelDef items = 1;
+};
+
+service modeldefs {
+
+ rpc ListModelDefs(google.protobuf.Empty) returns (ModelDefs) {
+ option (google.api.http) = {
+ get: "/api/v1/modeldefs"
+ };
+ }
+};
diff --git a/xos/grpc/xos_grpc_api.py b/xos/grpc/xos_grpc_api.py
index cf98031..a9ed79d 100644
--- a/xos/grpc/xos_grpc_api.py
+++ b/xos/grpc/xos_grpc_api.py
@@ -6,160 +6,15 @@
from django.contrib.auth import authenticate as django_authenticate
from core.models import *
from xos.exceptions import *
+from apihelper import XOSAPIHelperMixin
-class XosService(xos_pb2.xosServicer):
+class XosService(xos_pb2.xosServicer, XOSAPIHelperMixin):
def __init__(self, thread_pool):
self.thread_pool = thread_pool
def stop(self):
pass
- def getProtoClass(self, djangoClass):
- pClass = getattr(xos_pb2, djangoClass.__name__)
- return pClass
-
- def getPluralProtoClass(self, djangoClass):
- pClass = getattr(xos_pb2, djangoClass.__name__ + "s")
- return pClass
-
- def convertFloat(self, x):
- if not x:
- return 0
- else:
- return float(x)
-
- def convertDateTime(self, x):
- if not x:
- return 0
- else:
- return time.mktime(x.timetuple())
-
- def convertForeignKey(self, x):
- if not x:
- return 0
- else:
- return int(x.id)
-
- def objToProto(self, obj):
- p_obj = self.getProtoClass(obj.__class__)()
- for field in obj._meta.fields:
- if getattr(obj, field.name) == None:
- continue
-
- ftype = field.get_internal_type()
- if (ftype == "CharField") or (ftype == "TextField") or (ftype == "SlugField"):
- setattr(p_obj, field.name, str(getattr(obj, field.name)))
- elif (ftype == "BooleanField"):
- setattr(p_obj, field.name, getattr(obj, field.name))
- elif (ftype == "AutoField"):
- setattr(p_obj, field.name, int(getattr(obj, field.name)))
- elif (ftype == "IntegerField") or (ftype == "PositiveIntegerField"):
- setattr(p_obj, field.name, int(getattr(obj, field.name)))
- elif (ftype == "ForeignKey"):
- setattr(p_obj, field.name+"_id", self.convertForeignKey(getattr(obj, field.name)))
- elif (ftype == "DateTimeField"):
- setattr(p_obj, field.name, self.convertDateTime(getattr(obj, field.name)))
- elif (ftype == "FloatField"):
- setattr(p_obj, field.name, float(getattr(obj, field.name)))
- elif (ftype == "GenericIPAddressField"):
- setattr(p_obj, field.name, str(getattr(obj, field.name)))
- return p_obj
-
- def protoToArgs(self, djangoClass, message):
- args={}
- fmap={}
- fset={}
- for field in djangoClass._meta.fields:
- fmap[field.name] = field
- if field.get_internal_type() == "ForeignKey":
- # foreign key can be represented as an id
- fmap[field.name + "_id"] = field
-
- for (fieldDesc, val) in message.ListFields():
- name = fieldDesc.name
- if name in fmap:
- if (name=="id"):
- # don't let anyone set the id
- continue
- ftype = fmap[name].get_internal_type()
- if (ftype == "CharField") or (ftype == "TextField") or (ftype == "SlugField"):
- args[name] = val
- elif (ftype == "BooleanField"):
- args[name] = val
- elif (ftype == "AutoField"):
- args[name] = val
- elif (ftype == "IntegerField") or (ftype == "PositiveIntegerField"):
- args[name] = val
- elif (ftype == "ForeignKey"):
- args[name] = val # field name already has "_id" at the end
- elif (ftype == "DateTimeField"):
- pass # do something special here
- elif (ftype == "FloatField"):
- args[name] = val
- elif (ftype == "GenericIPAddressField"):
- args[name] = val
- fset[name] = True
-
- return args
-
- def querysetToProto(self, djangoClass, queryset):
- objs = queryset
- p_objs = self.getPluralProtoClass(djangoClass)()
-
- for obj in objs:
- new_obj = p_objs.items.add()
- new_obj.CopyFrom(self.objToProto(obj))
-
- return p_objs
-
- def get(self, djangoClass, id):
- obj = djangoClass.objects.get(id=id)
- return self.objToProto(obj)
-
- def create(self, djangoClass, user, request):
- args = self.protoToArgs(djangoClass, request)
- new_obj = djangoClass(**args)
- new_obj.caller = user
- if (not user) or (not new_obj.can_update(user)):
- raise XOSPermissionDenied()
- new_obj.save()
- return self.objToProto(new_obj)
-
- def update(self, djangoClass, user, id, message):
- obj = djangoClass.objects.get(id=id)
- obj.caller = user
- if (not user) or (not obj.can_update(user)):
- raise XOSPermissionDenied()
- args = self.protoToArgs(djangoClass, message)
- for (k,v) in args.iteritems():
- setattr(obj, k, v)
- obj.save()
- return self.objToProto(obj)
-
- def delete(self, djangoClass, user, id):
- obj = djangoClass.objects.get(id=id)
- if (not user) or (not obj.can_update(user)):
- raise XOSPermissionDenied()
- obj.delete()
- return Empty()
-
- def authenticate(self, context):
- for (k, v) in context.invocation_metadata():
- if (k.lower()=="authorization"):
- (method, auth) = v.split(" ",1)
- if (method.lower() == "basic"):
- auth = base64.b64decode(auth)
- (username, password) = auth.split(":")
- user = django_authenticate(username=username, password=password)
- if not user:
- raise Exception("failed to authenticate %s:%s" % (username, password))
- print "authenticated %s:%s as %s" % (username, password, user)
- return user
-
- return None
-
-
-
def ListServiceControllerResource(self, request, context):
user=self.authenticate(context)
return self.querysetToProto(ServiceControllerResource, ServiceControllerResource.objects.all())
diff --git a/xos/grpc/xos_modeldefs_api.py b/xos/grpc/xos_modeldefs_api.py
new file mode 100644
index 0000000..b918683
--- /dev/null
+++ b/xos/grpc/xos_modeldefs_api.py
@@ -0,0 +1,119 @@
+import base64
+import time
+from protos import modeldefs_pb2
+from google.protobuf.empty_pb2 import Empty
+
+from django.contrib.auth import authenticate as django_authenticate
+import django.apps
+from core.models import *
+from xos.exceptions import *
+from apihelper import XOSAPIHelperMixin
+
+class ModelDefsService(modeldefs_pb2.modeldefsServicer, XOSAPIHelperMixin):
+ def __init__(self, thread_pool):
+ self.thread_pool = thread_pool
+
+ def stop(self):
+ pass
+
+ typeMap = {
+ 'BooleanField': 'boolean',
+ 'TextField': 'text',
+ 'CharField': 'string',
+ 'ForeignKey': 'number',
+ 'IntegerField': 'number',
+ 'AutoField': 'number',
+ 'DateTimeField': 'date'
+ }
+
+ validatorMap = {
+ 'EmailValidator': 'email',
+ 'MaxLengthValidator': 'maxlength',
+ 'URLValidator': 'url',
+ 'MinValueValidator': 'min',
+ 'MaxValueValidator': 'max',
+ 'validate_ipv46_address': 'ip'
+ }
+
+ def convertType(self, type):
+ try:
+ jsType = self.typeMap[type]
+ return jsType
+ except Exception:
+ return None
+
+ def convertValidator(self, validator):
+ try:
+ jsValidator = self.validatorMap[validator]
+ return jsValidator
+ except Exception:
+ return None
+
+ def getRelationType(self, field):
+ if (field.many_to_many):
+ return 'many_to_many'
+ if (field.many_to_one):
+ return 'many_to_one'
+ if (field.one_to_many):
+ return 'one_to_many'
+ if (field.one_to_one):
+ return 'one_to_one'
+
+ def ListModelDefs(self, request, context):
+ models = django.apps.apps.get_models()
+
+ modeldefs = modeldefs_pb2.ModelDefs();
+
+ response = []
+
+ for model in models:
+ if 'core' in model.__module__:
+ modeldef = modeldefs.items.add()
+
+ modeldef.name = model.__name__
+
+ for f in model._meta.fields:
+ field = modeldef.fields.add()
+
+ field.name = f.name
+ field.hint = f.help_text
+
+ fieldtype = self.convertType(f.get_internal_type())
+ if fieldtype is not None:
+ field.type = fieldtype
+ else:
+ field.type = 'string'
+
+ if not f.blank and not f.null:
+ val = field.validators.add()
+ val.name = "required"
+ val.bool_value = True
+
+ for v in f.validators:
+ val = field.validators.add()
+ validator_name = v.__class__.__name__
+ if 'function' in validator_name:
+ validator_name = v.__name__
+ validator_name = self.convertValidator(validator_name)
+
+ if not validator_name:
+ continue
+
+ val.name = validator_name
+ if hasattr(v, 'limit_value'):
+ try:
+ val.int_value = v.limit_value
+ except TypeError:
+ val.str_value = str(v.limit_value)
+ else:
+ val.bool_value = True
+
+ if f.is_relation and f.related_model:
+ field.relation.model = f.related_model.__name__
+ field.relation.type = self.getRelationType(f)
+
+ rel = modeldef.relations.add()
+ rel.model = f.related_model.__name__
+ rel.type = self.getRelationType(f)
+ return modeldefs
+