From e67957af36dfd1faf480017c282137a96afce889 Mon Sep 17 00:00:00 2001 From: dhilipsiva Date: Sun, 22 Jul 2018 02:13:17 +0530 Subject: [PATCH] Inintial Garuda code --- Pipfile | 18 ++ Pipfile.lock | 229 +++++++++++++ garuda/__init__.py | 0 garuda/management/__init__.py | 0 garuda/management/commands/__init__.py | 0 garuda/management/commands/garuda.py | 371 ++++++++++++++++++++++ garuda/settings.py | 122 +++++++ manage.py | 15 + proto.sh | 8 + sample/manage.py | 15 + sample/sample/__init__.py | 0 sample/sample/core/__init__.py | 0 sample/sample/core/admin.py | 3 + sample/sample/core/apps.py | 5 + sample/sample/core/choices.py | 9 + sample/sample/core/migrations/__init__.py | 0 sample/sample/core/models.py | 11 + sample/sample/core/tests.py | 3 + sample/sample/core/views.py | 3 + sample/sample/settings.py | 131 ++++++++ sample/sample/urls.py | 21 ++ sample/sample/wsgi.py | 16 + setup.py | 45 +++ 23 files changed, 1025 insertions(+) create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 garuda/__init__.py create mode 100644 garuda/management/__init__.py create mode 100644 garuda/management/commands/__init__.py create mode 100644 garuda/management/commands/garuda.py create mode 100644 garuda/settings.py create mode 100755 manage.py create mode 100644 proto.sh create mode 100755 sample/manage.py create mode 100644 sample/sample/__init__.py create mode 100644 sample/sample/core/__init__.py create mode 100644 sample/sample/core/admin.py create mode 100644 sample/sample/core/apps.py create mode 100644 sample/sample/core/choices.py create mode 100644 sample/sample/core/migrations/__init__.py create mode 100644 sample/sample/core/models.py create mode 100644 sample/sample/core/tests.py create mode 100644 sample/sample/core/views.py create mode 100644 sample/sample/settings.py create mode 100644 sample/sample/urls.py create mode 100644 sample/sample/wsgi.py create mode 100644 setup.py diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..05a5df1 --- /dev/null +++ b/Pipfile @@ -0,0 +1,18 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +django = "*" +garuda = {editable = true, path = "."} +orm-choices = "*" +inflect = "*" + +[dev-packages] +neovim = "*" +django-extensions = "*" +ipdb = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..1e70509 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,229 @@ +{ + "_meta": { + "hash": { + "sha256": "cff1a8e0fb555d81b23e4ebb544e316cd0f220644c259045044d4a84506182a9" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "django": { + "hashes": [ + "sha256:97886b8a13bbc33bfeba2ff133035d3eca014e2309dff2b6da0bdfc0b8656613", + "sha256:e900b73beee8977c7b887d90c6c57d68af10066b9dac898e1eaf0f82313de334" + ], + "index": "pypi", + "version": "==2.0.7" + }, + "garuda": { + "editable": true, + "path": "." + }, + "inflect": { + "hashes": [ + "sha256:51d3d0fe0db77fa9315c45ce5933a64d9043a36d42e8b1a082d3379dc39754cf", + "sha256:7a71eed8a666c0c2b0463bb850a9a5c51603699836bf251521374ceffeb9c322" + ], + "index": "pypi", + "version": "==0.3.1" + }, + "orm-choices": { + "hashes": [ + "sha256:b906b04e4b25c3c804fe296f0755b6ce718469e1951ebc38870b21bb665a3a47", + "sha256:f1ae4a375a7f885585bde673ae4c6d07b0f20f6775b31c0d6ac735db6c0cd0de" + ], + "index": "pypi", + "version": "==1.0.0" + }, + "pytz": { + "hashes": [ + "sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053", + "sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277" + ], + "version": "==2018.5" + } + }, + "develop": { + "backcall": { + "hashes": [ + "sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4", + "sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2" + ], + "version": "==0.1.0" + }, + "decorator": { + "hashes": [ + "sha256:2c51dff8ef3c447388fe5e4453d24a2bf128d3a4c32af3fabef1f01c6851ab82", + "sha256:c39efa13fbdeb4506c476c9b3babf6a718da943dab7811c206005a4a956c080c" + ], + "version": "==4.3.0" + }, + "django-extensions": { + "hashes": [ + "sha256:c3c8524056ab1174c6fc769b0599e60f7e7797ce34f38db2effc48885a0a982c", + "sha256:e279344c6d825adb27694755aa924702d3cc401f8fad5ad9ef2d577948778a5f" + ], + "index": "pypi", + "version": "==2.1.0" + }, + "greenlet": { + "hashes": [ + "sha256:0411b5bf0de5ec11060925fd811ad49073fa19f995bcf408839eb619b59bb9f7", + "sha256:131f4ed14f0fd28d2a9fa50f79a57d5ed1c8f742d3ccac3d773fee09ef6fe217", + "sha256:13510d32f8db72a0b3e1720dbf8cba5c4eecdf07abc4cb631982f51256c453d1", + "sha256:31dc4d77ef04ab0460d024786f51466dbbc274fda7c8aad0885a6df5ff8d642e", + "sha256:35021d9fecea53b21e4defec0ff3ad69a8e2b75aca1ceddd444a5ba71216547e", + "sha256:426a8ef9e3b97c27e841648241c2862442c13c91ec4a48c4a72b262ccf30add9", + "sha256:58217698193fb94f3e6ff57eed0ae20381a8d06c2bc10151f76c06bb449a3a19", + "sha256:5f45adbbb69281845981bb4e0a4efb8a405f10f3cd6c349cb4a5db3357c6bf93", + "sha256:5fdb524767288f7ad161d2182f7ed6cafc0a283363728dcd04b9485f6411547c", + "sha256:71fbee1f7ef3fb42efa3761a8faefc796e7e425f528de536cfb4c9de03bde885", + "sha256:80bd314157851d06f7db7ca527082dbb0ee97afefb529cdcd59f7a5950927ba0", + "sha256:b843c9ef6aed54a2649887f55959da0031595ccfaf7e7a0ba7aa681ffeaa0aa1", + "sha256:c6a05ef8125503d2d282ccf1448e3599b8a6bd805c3cdee79760fa3da0ea090e", + "sha256:deeda2769a52db840efe5bf7bdf7cefa0ae17b43a844a3259d39fb9465c8b008", + "sha256:e66f8b09eec1afdcab947d3a1d65b87b25fde39e9172ae1bec562488335633b4", + "sha256:e8db93045414980dbada8908d49dbbc0aa134277da3ff613b3e548cb275bdd37", + "sha256:f1cc268a15ade58d9a0c04569fe6613e19b8b0345b64453064e2c3c6d79051af", + "sha256:fe3001b6a4f3f3582a865b9e5081cc548b973ec20320f297f5e2d46860e9c703", + "sha256:fe85bf7adb26eb47ad53a1bae5d35a28df16b2b93b89042a3a28746617a4738d" + ], + "version": "==0.4.14" + }, + "ipdb": { + "hashes": [ + "sha256:7081c65ed7bfe7737f83fa4213ca8afd9617b42ff6b3f1daf9a3419839a2a00a" + ], + "index": "pypi", + "version": "==0.11" + }, + "ipython": { + "hashes": [ + "sha256:a0c96853549b246991046f32d19db7140f5b1a644cc31f0dc1edc86713b7676f", + "sha256:eca537aa61592aca2fef4adea12af8e42f5c335004dfa80c78caf80e8b525e5c" + ], + "version": "==6.4.0" + }, + "ipython-genutils": { + "hashes": [ + "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8", + "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8" + ], + "version": "==0.2.0" + }, + "jedi": { + "hashes": [ + "sha256:b409ed0f6913a701ed474a614a3bb46e6953639033e31f769ca7581da5bd1ec1", + "sha256:c254b135fb39ad76e78d4d8f92765ebc9bf92cbc76f49e97ade1d5f5121e1f6f" + ], + "version": "==0.12.1" + }, + "msgpack": { + "hashes": [ + "sha256:0b3b1773d2693c70598585a34ca2715873ba899565f0a7c9a1545baef7e7fbdc", + "sha256:0bae5d1538c5c6a75642f75a1781f3ac2275d744a92af1a453c150da3446138b", + "sha256:0ee8c8c85aa651be3aa0cd005b5931769eaa658c948ce79428766f1bd46ae2c3", + "sha256:1369f9edba9500c7a6489b70fdfac773e925342f4531f1e3d4c20ac3173b1ae0", + "sha256:22d9c929d1d539f37da3d1b0e16270fa9d46107beab8c0d4d2bddffffe895cee", + "sha256:2ff43e3247a1e11d544017bb26f580a68306cec7a6257d8818893c1fda665f42", + "sha256:31a98047355d34d047fcdb55b09cb19f633cf214c705a765bd745456c142130c", + "sha256:8767eb0032732c3a0da92cbec5ac186ef89a3258c6edca09161472ca0206c45f", + "sha256:8acc8910218555044e23826980b950e96685dc48124a290c86f6f41a296ea172", + "sha256:ab189a6365be1860a5ecf8159c248f12d33f79ea799ae9695fa6a29896dcf1d4", + "sha256:cfd6535feb0f1cf1c7cdb25773e965cc9f92928244a8c3ef6f8f8a8e1f7ae5c4", + "sha256:e274cd4480d8c76ec467a85a9c6635bbf2258f0649040560382ab58cabb44bcf", + "sha256:f86642d60dca13e93260187d56c2bef2487aa4d574a669e8ceefcf9f4c26fd00", + "sha256:f8a57cbda46a94ed0db55b73e6ab0c15e78b4ede8690fa491a0e55128d552bb0", + "sha256:fcea97a352416afcbccd7af9625159d80704a25c519c251c734527329bb20d0e" + ], + "version": "==0.5.6" + }, + "neovim": { + "hashes": [ + "sha256:6ce58a742e0427491c0e1c8108556ee72ba33844209bd9e226b8da9538299276" + ], + "index": "pypi", + "version": "==0.2.6" + }, + "parso": { + "hashes": [ + "sha256:35704a43a3c113cce4de228ddb39aab374b8004f4f2407d070b6a2ca784ce8a2", + "sha256:895c63e93b94ac1e1690f5fdd40b65f07c8171e3e53cbd7793b5b96c0e0a7f24" + ], + "version": "==0.3.1" + }, + "pexpect": { + "hashes": [ + "sha256:2a8e88259839571d1251d278476f3eec5db26deb73a70be5ed5dc5435e418aba", + "sha256:3fbd41d4caf27fa4a377bfd16fef87271099463e6fa73e92a52f92dfee5d425b" + ], + "markers": "sys_platform != 'win32'", + "version": "==4.6.0" + }, + "pickleshare": { + "hashes": [ + "sha256:84a9257227dfdd6fe1b4be1319096c20eb85ff1e82c7932f36efccfe1b09737b", + "sha256:c9a2541f25aeabc070f12f452e1f2a8eae2abd51e1cd19e8430402bdf4c1d8b5" + ], + "version": "==0.7.4" + }, + "prompt-toolkit": { + "hashes": [ + "sha256:1df952620eccb399c53ebb359cc7d9a8d3a9538cb34c5a1344bdbeb29fbcc381", + "sha256:3f473ae040ddaa52b52f97f6b4a493cfa9f5920c255a12dc56a7d34397a398a4", + "sha256:858588f1983ca497f1cf4ffde01d978a3ea02b01c8a26a8bbc5cd2e66d816917" + ], + "version": "==1.0.15" + }, + "ptyprocess": { + "hashes": [ + "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", + "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f" + ], + "version": "==0.6.0" + }, + "pygments": { + "hashes": [ + "sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d", + "sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc" + ], + "version": "==2.2.0" + }, + "simplegeneric": { + "hashes": [ + "sha256:dc972e06094b9af5b855b3df4a646395e43d1c9d0d39ed345b7393560d0b9173" + ], + "version": "==0.8.1" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "version": "==1.11.0" + }, + "traitlets": { + "hashes": [ + "sha256:9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835", + "sha256:c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9" + ], + "version": "==4.3.2" + }, + "wcwidth": { + "hashes": [ + "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", + "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" + ], + "version": "==0.1.7" + } + } +} diff --git a/garuda/__init__.py b/garuda/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/garuda/management/__init__.py b/garuda/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/garuda/management/commands/__init__.py b/garuda/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/garuda/management/commands/garuda.py b/garuda/management/commands/garuda.py new file mode 100644 index 0000000..2a6f4c0 --- /dev/null +++ b/garuda/management/commands/garuda.py @@ -0,0 +1,371 @@ +from ast import parse +from inspect import getsource + +from inflect import engine +from orm_choices.core import user_attributes + +from django.core.management.base import BaseCommand +from django.apps import apps + +plural = engine().plural + +GARUDA_SUFFIX = 'Garuda' + +# Dict to translate Django fields into Protobuf fields +FIELDS_DICT = dict( + CharField='string', + DateTimeField='string', + BooleanField='bool', + EmailField='string', + UUIDField='string', + ManyToManyField='repeated string', + TextField='string', + PositiveSmallIntegerField='int32', + IntegerField='int64', + ForeignKey='string', +) + +# `KW_INFO` basically says how to extract data from a given ast `Assign` object +KW_INFO = dict( + Num=lambda kw: kw.value.n, + Str=lambda kw: kw.value.s, + Name=lambda kw: kw.value.id, + NameConstant=lambda kw: kw.value.value, + Attribute=lambda kw: '%s.%s' % (kw.value.value.id, kw.value.attr), +) + +# Fields to ifnore while dictifying +IGNORE_FIELDS = ['created_on', 'updated_on', 'id'] + +CRUD_CONTENT = ''' +from vandal.%(app_name)s.models import %(model_name)s # NOQA +IGNORE_FIELDS = %(ignore_fields)s # NOQA +def read_%(model_name_lower)s(*args, **kwargs): + try: + return %(model_name)s.objects.get(*args, **kwargs) + except %(model_name)s.DoesNotExist: + return None +def read_%(model_name_lower_plural)s_filter(*args, **kwargs): + return %(model_name)s.objects.filter(*args, **kwargs) +def create_%(model_name_lower)s(*args, **kwargs): + for ignore_field in IGNORE_FIELDS: + if ignore_field in kwargs: + del kwargs[ignore_field] + for key in list(kwargs): + if kwargs[key] in [None, 'None', '']: + del kwargs[key] + return %(model_name)s.objects.create(*args, **kwargs) +def update_%(model_name_lower)s(id, *args, **kwargs): + for ignore_field in IGNORE_FIELDS: + if ignore_field in kwargs: + del kwargs[ignore_field] + for key in list(kwargs): + if kwargs[key] in [None, 'None', '']: + del kwargs[key] + return %(model_name)s.objects.filter(id=id).update(*args, **kwargs) +def delete_%(model_name_lower)s(id): + return %(model_name)s.objects.get(id=id).delete() +''' + +RPC_CONTENT = ''' +from vandal.proto.vandal_pb2 import %(model_name)s, Void # NOQA +from vandal.%(app_name)s.auto_crud import ( # NOQA + read_%(model_name_lower)s, + delete_%(model_name_lower)s, + create_%(model_name_lower)s, + update_%(model_name_lower)s, + read_%(model_name_lower_plural)s_filter, +) +def %(model_name_lower)s_to_dict(obj): + # Cycle through fields directly + d = { } + if obj is None: + return d + is_dj_obj = obj.__module__.endswith('models') + foriegn_keys = %(foriegn_keys)s + for field in %(fields)s: # NOQA + value = getattr(obj, field, None) + if field in [None, 'None']: + continue + d[field] = value + if is_dj_obj and (field == 'id' or field in foriegn_keys): + d[field] = str(value) + elif is_dj_obj and field in ['created_on', 'updated_on']: + d[field] = value.isoformat() + return d +class %(rpc_name)s: + def Read%(model_name_plural)sFilter(self, void, context): + objs = read_%(model_name_lower_plural)s_filter() + return [%(model_name)s( + **%(model_name_lower)s_to_dict(obj)) for obj in objs] + def Read%(model_name)s(self, id, context): + obj = read_%(model_name_lower)s(id=id.id) + return %(model_name)s(**%(model_name_lower)s_to_dict(obj)) + def Create%(model_name)s(self, obj, context): + obj = create_%(model_name_lower)s(**%(model_name_lower)s_to_dict(obj)) + return %(model_name)s(**%(model_name_lower)s_to_dict(obj)) + def Update%(model_name)s(self, obj, context): + obj_dict = %(model_name_lower)s_to_dict(obj) + del obj_dict['id'] + obj = update_%(model_name_lower)s(obj.id, **obj_dict) + return Void() + def Delete%(model_name)s(self, id, context): + delete_%(model_name_lower)s(id.id) + return Void() +''' + +GARUDA_RPC_CONTENT = ''' + rpc Delete%(model_name)s(ID) returns (Void); + rpc Update%(model_name)s(%(model_name)s) returns (Void); + rpc Read%(model_name)s(ID) returns (%(model_name)s); + rpc Create%(model_name)s(%(model_name)s) returns (%(model_name)s); + rpc Read%(model_name_plural)sFilter(Tracker) returns (stream %(model_name)s); +''' + + +def extract(ast, attrib): + d = {} + if ast.__class__.__name__ != "Assign": + return d + __import__('ipdb').set_trace() + for kw in ast.value.keywords: + if kw.arg != attrib: + continue + # If we ever encounter a new Type, uncomment the lines below to debug + # if kw.value.__class__.__name__ == 'Name': + # __import__('pdb').set_trace() + klass = kw.value.__class__.__name__ + d[ast.targets[0].id] = KW_INFO[klass](kw) + return d + + +def process(model, attrib): + d = {} + ast = parse(getsource(model)) + ast = ast.body[0] + print(getsource(model)) + for sub_ast in ast.body: + d.update(extract(sub_ast, attrib)) + return d + + +def get_inflections(model_name): + return dict( + model_name=model_name, + model_name_lower=model_name.lower(), + model_name_plural=plural(model_name), + model_name_lower_plural=plural(model_name.lower()), + rpc_name=model_name + GARUDA_SUFFIX, + ) + + +def hacky_m2one_name(field): + ''' + This is a Hacky, Hacky method to get name. + field.related_model.__class__.__name__ somehow returns `BaseModel` + which is not somethig\ng we want + ''' + return str(field.related_model).split(".")[-1].split("'")[0] + + +def process_model(model): + fields = {} + many_fields = {} + foriegn_keys = [] + choices = process(model, 'choices') + defaults = process(model, 'default') + inflections = get_inflections(model.__name__) + for field in model._meta.get_fields(): + f_name = field.__class__.__name__ + if f_name == 'ManyToOneRel': + many_fields[field.name] = hacky_m2one_name(field) + elif f_name == 'ForeignKey': + name = f'{field.name}_id' + fields[name] = f_name + foriegn_keys.append(name) + else: + fields[field.name] = f_name + return dict( + choices=choices, fields=fields, defaults=defaults, + inflections=inflections, foriegn_keys=foriegn_keys, + many_fields=many_fields) + + +def process_app(app): + models = {} + for model in app.get_models(): + models[model.__name__] = process_model(model) + return models + + +def process_apps(): + app_models = {} + for app in apps.get_app_configs(): + app_models[app.name] = process_app(app) + return app_models + + +def get_rpc_type(field, fields, choices, many_fields): + if field in many_fields: + return 'repeated string' # ManyToOneRel fields + if field in choices: + return choices[field].split(".")[0] + dj_field_type = fields[field] + return FIELDS_DICT[dj_field_type] + + +def generate_model(model_name, model): + fields = model['fields'] + choices = model['choices'] + many_fields = model['many_fields'] + # Every model will have an ID + fields_declaration = ' string id = 1;' + field_names = list(fields.keys()) + list(many_fields.keys()) + + # remove id which is already in declaration + for field in ['id', 'deleted']: + # and we do not need these fields as well + field_names.remove(field) + for idx, field in enumerate(sorted(field_names)): + field_type = get_rpc_type(field, fields, choices, many_fields) + fields_declaration += f'\n {field_type} {field} = {idx + 2};' + defnition = '\nmessage %s {\n%s\n}\n' % ( + model_name, fields_declaration) + return defnition + + +def generate_app(app, models): + app_defnition = '' + for model_name, model in models.items(): + app_defnition += generate_model(model_name, model) + return app_defnition + + +def generate_model_proto(app_models): + model_protos = '' + for app, models in app_models.items(): + model_protos += generate_app(app, models) + return model_protos + + +def generate_choices_proto(): + choices_proto = '' + for attr in dir(CHOICES): + choices = getattr(CHOICES, attr) + if not hasattr(choices, 'CHOICES'): + continue + choices_declaration = f'\n {choices.__name__}UNKNOWN = 0;' + attrs = user_attributes(choices.Meta) + attrs.remove('UNKNOWN') + c_dict = {} + for attr in attrs: + value = getattr(choices.Meta, attr)[0] + c_dict.update({attr: value}) + c_list = sorted(c_dict.items(), key=lambda x: x[1]) + for key, value in c_list: + choices_declaration += f'\n {key} = {value};' + choices_proto += '\nenum %s {%s\n}\n' % ( + choices.__name__, choices_declaration) + return choices_proto + + +def write_to_file(app, kind, content, extention='py'): + comment_prefix = '//' + if extention in ['py']: + comment_prefix = '#' + with open(f"vandal/{app}/auto_{kind}.{extention}", "w") as f: + print(f'writing to {f.name}...') + f.write(f'{comment_prefix} DO NOT EDIT THIS FILE MANUALLY\n') + f.write(f'{comment_prefix} THIS FILE IS AUTO-GENERATED\n') + f.write(f'{comment_prefix} MANUAL CHANGES WILL BE DISCARDED\n') + f.write(content.strip()) + + +def codify_model(app_name, model_name, model_defnition): + fields = sorted(model_defnition['fields'].keys()) + foriegn_keys = sorted(model_defnition['foriegn_keys']) + fields.remove('deleted') # Not requred + ctx = dict( + app_name=app_name, fields=fields, foriegn_keys=foriegn_keys, + ignore_fields=IGNORE_FIELDS) + ctx.update(model_defnition['inflections']) + return CRUD_CONTENT % ctx, RPC_CONTENT % ctx + + +def codify_app(app, models): + crud_contents = '' + rpc_contents = '' + for model in models.keys(): + crud_content, rpc_content = codify_model(app, model, models[model]) + crud_contents += crud_content + rpc_contents += rpc_content + write_to_file(app, 'rpc', rpc_contents) + write_to_file(app, 'crud', crud_contents) + + +def generate_rpc_code(app_models): + for app in app_models.keys(): + codify_app(app, app_models[app]) + + +def generate_vandal_rpc(app_models): + content = '' + for app in app_models: + models = app_models[app] + for model in models: + content += GARUDA_RPC_CONTENT % models[model]['inflections'] + v = __import__(f'vandal.{app}.rpc') + rpc_module = getattr(v, app).rpc + rpc_class_names = list( + filter(lambda x: x.endswith('RPC'), dir(rpc_module))) + for rpc_class_name in rpc_class_names: + rpc_class = getattr(rpc_module, rpc_class_name) + for attribute in user_attributes(rpc_class): + content += ' %s\n' % getattr(rpc_class, attribute).__doc__ + return content + + +def generate_auto_vandal(app_models): + content = '' + models = [] + for app in app_models: + _models = [ + f'%s%s' % (model, GARUDA_SUFFIX) + for model in app_models[app].keys()] + models += _models + _models = ", ".join(_models) + content += f'\nfrom vandal.{app}.auto_rpc import {_models} # NOQA' + models = ", ".join(models) + content += f'\n\nclass AutoVandal({models}): # NOQA\n pass' + with open("vandal/rpc/management/commands/auto_vandal.py", "w") as f: + print(f'writing to {f.name}') + f.write(content) + + +class Command(BaseCommand): + help = 'Generate proto files' + + def handle(self, *args, **options): + app_models = process_apps() + generate_rpc_code(app_models) + generate_auto_vandal(app_models) + CHOICES_PROTO = generate_choices_proto() + MODELS_PROTO = generate_model_proto(app_models) + PROTO_HEADER = open('vandal/proto/messages.proto', 'r').read() + PROTO_FOOTER = open('vandal/proto/services.proto', 'r').read() + + try: + GARUDA_RPC = "service Vandal{%s}" % generate_vandal_rpc(app_models) + except ImportError: + GARUDA_RPC = '' + print('ERROR: PLEASE RUN THIS COMMAND AGAIN... ') + print('BECAUSE A NEW MODEL HAS BEEN ADDED!!!') + with open('vandal/proto/vandal.proto', 'w') as f: + print(f'writing to {f.name}...') + f.write(f''' +{PROTO_HEADER} +{CHOICES_PROTO} +{MODELS_PROTO} +{GARUDA_RPC} +{PROTO_FOOTER} + '''.strip()) diff --git a/garuda/settings.py b/garuda/settings.py new file mode 100644 index 0000000..9f26a16 --- /dev/null +++ b/garuda/settings.py @@ -0,0 +1,122 @@ +""" +Django settings for garuda project. + +Generated by 'django-admin startproject' using Django 2.0.7. + +For more information on this file, see +https://docs.djangoproject.com/en/2.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/2.0/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '%-&t*nzub^ptx@f&qu1o1oagf-nl%xod6wabexk=_fmtwv3ppl' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + + 'garuda', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'garuda.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'garuda.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/2.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/2.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/2.0/howto/static-files/ + +STATIC_URL = '/static/' diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..9d2e49c --- /dev/null +++ b/manage.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "garuda.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) diff --git a/proto.sh b/proto.sh new file mode 100644 index 0000000..b125636 --- /dev/null +++ b/proto.sh @@ -0,0 +1,8 @@ +# protoc --proto_path=. --java_out=java --grpc_java_out=java vandal.proto +PROTO_COMMAND_OUTPUT=$(./manage.py proto) +echo $PROTO_COMMAND_OUTPUT +python -m grpc_tools.protoc -I vandal/proto --python_out=vandal/proto --grpc_python_out=vandal/proto vandal.proto +protoc -I vandal/proto vandal/proto/vandal.proto --go_out=plugins=grpc:vandal/proto/ +grpc_tools_node_protoc -I vandal/proto/ --js_out=import_style=commonjs,binary:vandal/proto/ --grpc_out=vandal/proto/ --plugin=protoc-gen-grpc=`which grpc_tools_node_protoc_plugin` vandal/proto/vandal.proto +sed -i 's/import vandal_pb2 as vandal__pb2/import vandal.proto.vandal_pb2 as vandal__pb2/g' vandal/proto/vandal_pb2.py vandal/proto/vandal_pb2_grpc.py +grep -q -F 'exports.grpc = grpc;' vandal/proto/vandal_grpc_pb.js || echo 'exports.grpc = grpc;' >> vandal/proto/vandal_grpc_pb.js diff --git a/sample/manage.py b/sample/manage.py new file mode 100755 index 0000000..90784aa --- /dev/null +++ b/sample/manage.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sample.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) diff --git a/sample/sample/__init__.py b/sample/sample/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sample/sample/core/__init__.py b/sample/sample/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sample/sample/core/admin.py b/sample/sample/core/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/sample/sample/core/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/sample/sample/core/apps.py b/sample/sample/core/apps.py new file mode 100644 index 0000000..26f78a8 --- /dev/null +++ b/sample/sample/core/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + name = 'core' diff --git a/sample/sample/core/choices.py b/sample/sample/core/choices.py new file mode 100644 index 0000000..7264268 --- /dev/null +++ b/sample/sample/core/choices.py @@ -0,0 +1,9 @@ +from orm_choices import choices + +@choices +class ArticleStatus: + class Meta: + UNPUBLISHED = [0, "Not published yet"] + PUBLISHED = [1, "Published"] + REVIEW_REQUIRED = [2, "To be reviewed"] + DELETED = [3, "Deleted"] diff --git a/sample/sample/core/migrations/__init__.py b/sample/sample/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sample/sample/core/models.py b/sample/sample/core/models.py new file mode 100644 index 0000000..960c111 --- /dev/null +++ b/sample/sample/core/models.py @@ -0,0 +1,11 @@ +from sample.core.choices import ArticleStatus + +from django.db.models import Model, PositiveSmallIntegerField, TextField, \ + CharField + + +class Article(Model): + title = CharField(max_length=280) + content = TextField() + status = PositiveSmallIntegerField( + default=ArticleStatus.UNPUBLISHED, choices=ArticleStatus.CHOICES) diff --git a/sample/sample/core/tests.py b/sample/sample/core/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/sample/sample/core/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/sample/sample/core/views.py b/sample/sample/core/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/sample/sample/core/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/sample/sample/settings.py b/sample/sample/settings.py new file mode 100644 index 0000000..f6bfae5 --- /dev/null +++ b/sample/sample/settings.py @@ -0,0 +1,131 @@ +""" +Django settings for sample project. + +Generated by 'django-admin startproject' using Django 2.0.7. + +For more information on this file, see +https://docs.djangoproject.com/en/2.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/2.0/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '%-&t*nzub^ptx@f&qu1o1oagf-nl%xod6wabexk=_fmtwv3ppl' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + + # Garuda + 'garuda', + + # Private Apps + 'sample.core', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'sample.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'sample.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/2.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/2.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/2.0/howto/static-files/ + +STATIC_URL = '/static/' + + +# Garuda Configs + +GARUDA_CHOICES = 'sample.core.choices' diff --git a/sample/sample/urls.py b/sample/sample/urls.py new file mode 100644 index 0000000..6d38610 --- /dev/null +++ b/sample/sample/urls.py @@ -0,0 +1,21 @@ +"""sample URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/2.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path('admin/', admin.site.urls), +] diff --git a/sample/sample/wsgi.py b/sample/sample/wsgi.py new file mode 100644 index 0000000..94f36e1 --- /dev/null +++ b/sample/sample/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for sample project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sample.settings") + +application = get_wsgi_application() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..8aa980c --- /dev/null +++ b/setup.py @@ -0,0 +1,45 @@ +from setuptools import setup, find_packages + +__VERSION__ = '0.0.1' + +setup( + name='garuda', + version=__VERSION__, + description=( + 'Automagically Exposing Djagno ORM over gRPC for microservices' + ' written in any other languages'), + long_description=''' +Microservices are fun. But what would make them even more fun to work with, + is if we can avoid duplicating the data layer across your micro-services. + Django ORM is amazing. Let's share the joy of Django ORM with other languages. + I have written a tool to automatically expose Django ORM to other languages + and which can also generate respective client libraries in other languages. + I heavily rely on Protobuf and gRPC and a lot of AST parsing. + ''', + url='https://github.com/dhilipsiva/garuda', + author='dhilipsiva', + author_email='dhilipsiva@gmail.com', + license='MIT', + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Topic :: Database', + 'Topic :: Database :: Database Engines/Servers', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + ], + + keywords='django orm grpc protobuf microservice database rpc garuda', + packages=['garuda'], + entry_points='', + install_requires=[ + "django>=2.0", + "inflect==0.3.1", + "orm-choices==1.0.0", + ], +)