├── .github └── workflows │ └── ci.yml ├── .gitignore ├── MANIFEST.in ├── README.md ├── frappe_er_generator ├── __init__.py ├── config │ ├── __init__.py │ ├── desktop.py │ └── docs.py ├── frappe_er_generator │ ├── __init__.py │ ├── er_generator.py │ └── utility.py ├── hooks.py ├── modules.txt ├── patches.txt ├── public │ └── .gitkeep └── templates │ ├── __init__.py │ └── pages │ └── __init__.py ├── license.txt ├── requirements.txt └── setup.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | - main 8 | pull_request: 9 | 10 | jobs: 11 | linters: 12 | name: Frappe Linter 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Set up Python 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: '3.10' 21 | 22 | - name: Download Semgrep rules 23 | run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules 24 | 25 | - name: Download semgrep 26 | run: pip install semgrep 27 | 28 | - name: Run Semgrep rules 29 | run: semgrep ci --config ./frappe-semgrep-rules/rules 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.egg-info 4 | *.swp 5 | tags 6 | frappe_er_generator/docs/current 7 | node_modules/ -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include requirements.txt 3 | include *.json 4 | include *.md 5 | include *.py 6 | include *.txt 7 | recursive-include frappe_er_generator *.css 8 | recursive-include frappe_er_generator *.csv 9 | recursive-include frappe_er_generator *.html 10 | recursive-include frappe_er_generator *.ico 11 | recursive-include frappe_er_generator *.js 12 | recursive-include frappe_er_generator *.json 13 | recursive-include frappe_er_generator *.md 14 | recursive-include frappe_er_generator *.png 15 | recursive-include frappe_er_generator *.py 16 | recursive-include frappe_er_generator *.svg 17 | recursive-include frappe_er_generator *.txt 18 | recursive-exclude frappe_er_generator *.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Frappe ERD Generator 2 | 3 | ERD generator for frappe doctypes 4 | 5 | #### Download 6 | 7 | ```bash 8 | $ bench get-app https://github.com/The-Commit-Company/frappe_er_generator.git 9 | ``` 10 | 11 | #### Install 12 | 13 | ```bash 14 | $ bench --site site_name install-app frappe_er_generator 15 | ``` 16 | 17 | 1. Call `get_erd` function for generating ERD by passing list of doctypes as argument. 18 | 19 | path = `api/method/frappe_er_generator.frappe_er_generator.er_generator.get_erd?doctypes = ["DocType1", "DocType2"]` 20 | 21 | 2. Call `get_whitelist_methods_in_app` function for fetching all whitelisted methods in app, by passing app name as argument. `app` is argument name. 22 | 23 | #### Note: 24 | 25 | If got error while calling API - "RuntimeError: Make sure the Graphviz executables are on your system's path" after installing Graphviz 2.38, them install graphviz in macos using brew 26 | 27 | ```bash 28 | $ brew install graphviz 29 | ``` 30 | 31 | #### Output: 32 | 33 | 1. ERD in PNG format 34 | 35 | ![erd](https://user-images.githubusercontent.com/59503001/231471200-7717c3d4-75f5-45b2-8c2c-84d07ddd865b.png) 36 | 37 | 38 | 2. Output of `get_whitelist_methods_in_app` 39 | 40 | image 41 | 42 | #### License 43 | 44 | MIT 45 | -------------------------------------------------------------------------------- /frappe_er_generator/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | __version__ = '0.0.1' 3 | 4 | -------------------------------------------------------------------------------- /frappe_er_generator/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/The-Commit-Company/frappe_er_generator/24d5d1daac9c2d0d32e5b8c19070c4c5367311c7/frappe_er_generator/config/__init__.py -------------------------------------------------------------------------------- /frappe_er_generator/config/desktop.py: -------------------------------------------------------------------------------- 1 | from frappe import _ 2 | 3 | def get_data(): 4 | return [ 5 | { 6 | "module_name": "Frappe Er Generator", 7 | "type": "module", 8 | "label": _("Frappe Er Generator") 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /frappe_er_generator/config/docs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration for docs 3 | """ 4 | 5 | # source_link = "https://github.com/[org_name]/frappe_er_generator" 6 | # headline = "App that does everything" 7 | # sub_heading = "Yes, you got that right the first time, everything" 8 | 9 | def get_context(context): 10 | context.brand_html = "Frappe Er Generator" 11 | -------------------------------------------------------------------------------- /frappe_er_generator/frappe_er_generator/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/The-Commit-Company/frappe_er_generator/24d5d1daac9c2d0d32e5b8c19070c4c5367311c7/frappe_er_generator/frappe_er_generator/__init__.py -------------------------------------------------------------------------------- /frappe_er_generator/frappe_er_generator/er_generator.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | from frappe.config import get_modules_from_app, get_modules_from_all_apps 3 | import graphviz 4 | 5 | 6 | def get_apps(): 7 | # get all the apps installed on the bench 8 | return frappe.get_all_apps() 9 | 10 | 11 | @frappe.whitelist() 12 | def get_all_modules_from_all_apps(): 13 | # get all the modules from all the apps installed on the bench 14 | app_module_object = {} 15 | app_module = get_modules_from_all_apps() 16 | for i in app_module: 17 | if i.get('app') in app_module_object.keys(): 18 | app_module_object[i.get('app')].append(i.get('module_name')) 19 | else: 20 | app_module_object[i.get('app')] = [i.get('module_name')] 21 | return app_module_object 22 | 23 | 24 | @frappe.whitelist() 25 | def get_doctype_from_app(app): 26 | doctype_list = [] 27 | module = get_modules_from_app(app) 28 | for i in module: 29 | doctype_list.append(get_doctypes_from_module(i.module_name)) 30 | return doctype_list 31 | 32 | 33 | @frappe.whitelist() 34 | def get_doctypes_from_module(module): 35 | return {'doctype': [doctype['name'] for doctype in frappe.get_list('DocType', filters={'module': module})], 'module': module} 36 | 37 | 38 | @frappe.whitelist() 39 | def get_doctype_json(): 40 | # return frappe.get_doc('DocType', 'Lead').as_dict() 41 | return frappe.get_meta('Lead').as_dict() 42 | 43 | 44 | """ 45 | @api {get} /api/method/frappe_er_generator.er_generator.get_erd Get ERD 46 | @apiName get_erd 47 | @apiQuery {String} doctypes Doctypes 48 | 49 | @apiSuccess {String} message Success {Generate ERD with name erd.png} 50 | """ 51 | 52 | 53 | @frappe.whitelist() 54 | def get_erd(doctypes): 55 | # 1. This is very generic function only have to pass list of doctypes 56 | # 2. This function will generate ERD for all the doctypes passed 57 | 58 | # json_list is list of doctype json data(meta data) 59 | json_list = [] 60 | 61 | # link_list is the list of all `Link` fieldtype fields objects, because we need Link doctype name to generate connection in ERD. 62 | # eg. "fetch_from": "batch_name.project_code" fetch_from look like this. which means batch_name is Link field and project_code is fieldname of that doctype which is linked to batch_name. 63 | # for getting Link doctype name we need all Link fieldtype fields objects. 64 | link_list = [] 65 | 66 | # table_list is list of all the tables in the ERD 67 | table_list = [] 68 | 69 | # connections_string_list is list of all the Link connections in the ERD in string format. 70 | # eg. salutation of lead doctype is link to salutation doctype then connection_string_list will have ['lead:salutation -> salutation:name;'] 71 | connections_string_list = [] 72 | 73 | # fetch_from_string_list is list of all the fetch_from connections in the ERD in string format just like connection_string_list. 74 | fetch_from_string_list = [] 75 | 76 | for doctype in doctypes: 77 | data = frappe.get_meta(doctype).as_dict() 78 | json_list.append(data) 79 | # check if fieldtype is Link then add it to link_list 80 | link_list += [{**x, 'doctype': data.get('name')} 81 | for x in data.get('fields') if x['fieldtype'] == 'Link'] 82 | 83 | for doctype_data in json_list: 84 | # get_table function will return table string, connection_list, fetch_from 85 | table, connection_list, fetch_from = get_table( 86 | doctype_data, link_list, doctypes) 87 | table_list.append(table) 88 | connections_string_list += connection_list 89 | fetch_from_string_list += fetch_from 90 | 91 | # get_graph_string function will return graph string which is used to create graph 92 | graph_string = get_graph_string( 93 | table_list, connections_string_list, fetch_from_string_list) 94 | 95 | # create_graph function will create graph from graph_string 96 | create_graph(graph_string) 97 | 98 | return 'Success' 99 | 100 | 101 | def create_graph(graph_string): 102 | # create graph from graph_string 103 | # format can be png, pdf, etc. 104 | # view=True will open the graph in default browser 105 | # erd is the name of the graph 106 | graph = graphviz.Source(graph_string) 107 | graph.format = 'png' 108 | graph.render('erd', view=True) 109 | 110 | 111 | def get_table(data, link_list, doctypes): 112 | # data is doctype json data (meta data) link_list is list of all Link fieldtype fields objects and doctypes is list of all doctypes 113 | # get_table function will return table string, connection_list, fetch_from 114 | 115 | # table_element_list is row of the table in the ERD in string format. 116 | table_element_list = [] 117 | 118 | # remove_fieldtype is list of fieldtype which we don't want to show in the ERD. 119 | remove_fieldtype = ['Column Break', 'Section Break', 'Tab Break'] 120 | 121 | # connection_list is list of all the Link connections in the ERD in string format. 122 | connection_list = [] 123 | 124 | # fetch_from is list of all the fetch_from connections in the ERD in string format just like connection_list. 125 | fetch_from = [] 126 | for field in data.get("fields"): 127 | if field.get('fieldtype') not in remove_fieldtype: 128 | # add each field as a row in the table 129 | if field.get('is_custom_field'): 130 | table_element_list.append( 131 | f'{field.get("label")}') 132 | else: 133 | table_element_list.append( 134 | f'{field.get("label")}') 135 | if field.get("fieldtype") == "Link": 136 | # get_connection function will return connection string 137 | connection_data = get_connection(field, data.get("name"), doctypes) 138 | if connection_data: 139 | connection_list.append(connection_data) 140 | if field.get("fetch_from") != None: 141 | # get_fetch_from function will return fetch_from string 142 | fetch_data = get_fetch_from(field, data.get( 143 | "name"), link_list, doctypes) 144 | if fetch_data: 145 | fetch_from.append(fetch_data) 146 | 147 | table_elements = "\n".join(table_element_list) 148 | 149 | table = f"""{"".join(c if c.isalnum() else "_" for c in data.get("name")).lower()} [label=< 150 | 151 | 152 | {table_elements} 153 |
{data.get("name")}
>];""" 154 | 155 | return table, connection_list, fetch_from 156 | 157 | 158 | def get_connection(data, doctype_name, doctypes): 159 | # data is Link fieldtype field object, doctype_name is doctype name and doctypes is list of all doctypes 160 | # get_connection function will return connection string 161 | if data.get("options") in doctypes: 162 | connection_string = f"""{"".join(c if c.isalnum() else "_" for c in doctype_name).lower()}:{data.get('fieldname')} -> {"".join(c if c.isalnum() else "_" for c in data.get("options")).lower()}:name;""" 163 | return connection_string 164 | 165 | return None 166 | 167 | 168 | def get_fetch_from(data, doctype_name, link_list, doctypes): 169 | # data is field object of doctype which have fetch_from field, doctype_name is doctype name, link_list is list of all Link fieldtype fields objects and doctypes is list of all doctypes 170 | # get_fetch_from function will return fetch_from string 171 | fetch_link_object = next(x for x in link_list if x.get( 172 | "fieldname") == data.get("fetch_from").split(".")[0]) 173 | if fetch_link_object.get('options') in doctypes: 174 | fetch_string = f"""{"".join(c if c.isalnum() else "_" for c in fetch_link_object.get('doctype')).lower()}:{data.get('fieldname')} -> {"".join(c if c.isalnum() else "_" for c in fetch_link_object.get('options')).lower()}:{data.get("fetch_from").split(".")[1]} [style="dashed"];""" 175 | return fetch_string 176 | 177 | return None 178 | 179 | 180 | def get_graph_string(table_list, connections_string_list, fetch_from_string_list): 181 | # join all the table, connection and fetch_from string to get graph string 182 | table_string = "\n\n".join(table_list) 183 | connections_string = "\n".join(connections_string_list) 184 | fetch_from_string = "\n".join(fetch_from_string_list) 185 | graph_string = f""" 186 | digraph {{ 187 | graph [pad="0.5", nodesep="0.5", ranksep="2",legend="Fetch from\\l\\nNormal Link\\l"]; 188 | node [shape=plain] 189 | rankdir=LR; 190 | 191 | {table_string} 192 | 193 | {connections_string} 194 | 195 | {fetch_from_string} 196 | 197 | subgraph cluster_01 {{ 198 | label = "Legend"; 199 | key [label=< 200 | 201 | 202 | 203 | 205 |
Link
Fetch from
Custom Fields 204 |
>] 206 | key2 [label=< 207 | 208 | 209 |
 
 
>] 210 | key:i1:e -> key2:i1:w 211 | key:i2:e -> key2:i2:w [style=dashed] 212 | }} 213 | }} 214 | """ 215 | return graph_string 216 | -------------------------------------------------------------------------------- /frappe_er_generator/frappe_er_generator/utility.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | import os 3 | 4 | 5 | @frappe.whitelist() 6 | def get_whitelist_methods_in_app(app): 7 | # directory = frappe.get_app_path('emotive_app') 8 | directory = frappe.get_app_path(app) 9 | 10 | whitelisted_functions = [] 11 | 12 | for root, _, files in os.walk(directory): 13 | for file in files: 14 | if file.endswith('.py'): 15 | file_path = os.path.join(root, file) 16 | with open(file_path, 'r') as f: 17 | lines = f.readlines() 18 | for i, line in enumerate(lines): 19 | if "@frappe.whitelist" in line and not is_commented(line): 20 | function_name, params = get_function_name(lines, i) 21 | whitelisted_functions.append( 22 | {'function': function_name, 'params': params, 'file': file_path, 'line': i + 2}) 23 | 24 | return whitelisted_functions 25 | 26 | 27 | def get_function_name(lines, index): 28 | params = [] 29 | for line in lines[index:]: 30 | if 'def' in line: 31 | data = ' '.join(line.split()[1:]) 32 | params = data.split('(')[1].split(')')[0].split(',') 33 | params = [param for param in params if param != ''] 34 | return data.split(':')[0], params 35 | 36 | return None 37 | 38 | 39 | def is_commented(line): 40 | stripped_line = line.strip() 41 | if stripped_line.startswith('#'): 42 | return True 43 | elif '#' in stripped_line: 44 | return stripped_line.index('#') < stripped_line.index('@frappe.whitelist') 45 | else: 46 | return False 47 | -------------------------------------------------------------------------------- /frappe_er_generator/hooks.py: -------------------------------------------------------------------------------- 1 | from . import __version__ as app_version 2 | 3 | app_name = "frappe_er_generator" 4 | app_title = "Frappe Er Generator" 5 | app_publisher = "Sumit Jain" 6 | app_description = "ERD generator for frappe doctypes" 7 | app_email = "jainmit23@gmail.com" 8 | app_license = "MIT" 9 | 10 | # Includes in 11 | # ------------------ 12 | 13 | # include js, css files in header of desk.html 14 | # app_include_css = "/assets/frappe_er_generator/css/frappe_er_generator.css" 15 | # app_include_js = "/assets/frappe_er_generator/js/frappe_er_generator.js" 16 | 17 | # include js, css files in header of web template 18 | # web_include_css = "/assets/frappe_er_generator/css/frappe_er_generator.css" 19 | # web_include_js = "/assets/frappe_er_generator/js/frappe_er_generator.js" 20 | 21 | # include custom scss in every website theme (without file extension ".scss") 22 | # website_theme_scss = "frappe_er_generator/public/scss/website" 23 | 24 | # include js, css files in header of web form 25 | # webform_include_js = {"doctype": "public/js/doctype.js"} 26 | # webform_include_css = {"doctype": "public/css/doctype.css"} 27 | 28 | # include js in page 29 | # page_js = {"page" : "public/js/file.js"} 30 | 31 | # include js in doctype views 32 | # doctype_js = {"doctype" : "public/js/doctype.js"} 33 | # doctype_list_js = {"doctype" : "public/js/doctype_list.js"} 34 | # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} 35 | # doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"} 36 | 37 | # Home Pages 38 | # ---------- 39 | 40 | # application home page (will override Website Settings) 41 | # home_page = "login" 42 | 43 | # website user home page (by Role) 44 | # role_home_page = { 45 | # "Role": "home_page" 46 | # } 47 | 48 | # Generators 49 | # ---------- 50 | 51 | # automatically create page for each record of this doctype 52 | # website_generators = ["Web Page"] 53 | 54 | # Jinja 55 | # ---------- 56 | 57 | # add methods and filters to jinja environment 58 | # jinja = { 59 | # "methods": "frappe_er_generator.utils.jinja_methods", 60 | # "filters": "frappe_er_generator.utils.jinja_filters" 61 | # } 62 | 63 | # Installation 64 | # ------------ 65 | 66 | # before_install = "frappe_er_generator.install.before_install" 67 | # after_install = "frappe_er_generator.install.after_install" 68 | 69 | # Uninstallation 70 | # ------------ 71 | 72 | # before_uninstall = "frappe_er_generator.uninstall.before_uninstall" 73 | # after_uninstall = "frappe_er_generator.uninstall.after_uninstall" 74 | 75 | # Desk Notifications 76 | # ------------------ 77 | # See frappe.core.notifications.get_notification_config 78 | 79 | # notification_config = "frappe_er_generator.notifications.get_notification_config" 80 | 81 | # Permissions 82 | # ----------- 83 | # Permissions evaluated in scripted ways 84 | 85 | # permission_query_conditions = { 86 | # "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions", 87 | # } 88 | # 89 | # has_permission = { 90 | # "Event": "frappe.desk.doctype.event.event.has_permission", 91 | # } 92 | 93 | # DocType Class 94 | # --------------- 95 | # Override standard doctype classes 96 | 97 | # override_doctype_class = { 98 | # "ToDo": "custom_app.overrides.CustomToDo" 99 | # } 100 | 101 | # Document Events 102 | # --------------- 103 | # Hook on document methods and events 104 | 105 | # doc_events = { 106 | # "*": { 107 | # "on_update": "method", 108 | # "on_cancel": "method", 109 | # "on_trash": "method" 110 | # } 111 | # } 112 | 113 | # Scheduled Tasks 114 | # --------------- 115 | 116 | # scheduler_events = { 117 | # "all": [ 118 | # "frappe_er_generator.tasks.all" 119 | # ], 120 | # "daily": [ 121 | # "frappe_er_generator.tasks.daily" 122 | # ], 123 | # "hourly": [ 124 | # "frappe_er_generator.tasks.hourly" 125 | # ], 126 | # "weekly": [ 127 | # "frappe_er_generator.tasks.weekly" 128 | # ], 129 | # "monthly": [ 130 | # "frappe_er_generator.tasks.monthly" 131 | # ], 132 | # } 133 | 134 | # Testing 135 | # ------- 136 | 137 | # before_tests = "frappe_er_generator.install.before_tests" 138 | 139 | # Overriding Methods 140 | # ------------------------------ 141 | # 142 | # override_whitelisted_methods = { 143 | # "frappe.desk.doctype.event.event.get_events": "frappe_er_generator.event.get_events" 144 | # } 145 | # 146 | # each overriding function accepts a `data` argument; 147 | # generated from the base implementation of the doctype dashboard, 148 | # along with any modifications made in other Frappe apps 149 | # override_doctype_dashboards = { 150 | # "Task": "frappe_er_generator.task.get_dashboard_data" 151 | # } 152 | 153 | # exempt linked doctypes from being automatically cancelled 154 | # 155 | # auto_cancel_exempted_doctypes = ["Auto Repeat"] 156 | 157 | # Ignore links to specified DocTypes when deleting documents 158 | # ----------------------------------------------------------- 159 | 160 | # ignore_links_on_delete = ["Communication", "ToDo"] 161 | 162 | # Request Events 163 | # ---------------- 164 | # before_request = ["frappe_er_generator.utils.before_request"] 165 | # after_request = ["frappe_er_generator.utils.after_request"] 166 | 167 | # Job Events 168 | # ---------- 169 | # before_job = ["frappe_er_generator.utils.before_job"] 170 | # after_job = ["frappe_er_generator.utils.after_job"] 171 | 172 | # User Data Protection 173 | # -------------------- 174 | 175 | # user_data_fields = [ 176 | # { 177 | # "doctype": "{doctype_1}", 178 | # "filter_by": "{filter_by}", 179 | # "redact_fields": ["{field_1}", "{field_2}"], 180 | # "partial": 1, 181 | # }, 182 | # { 183 | # "doctype": "{doctype_2}", 184 | # "filter_by": "{filter_by}", 185 | # "partial": 1, 186 | # }, 187 | # { 188 | # "doctype": "{doctype_3}", 189 | # "strict": False, 190 | # }, 191 | # { 192 | # "doctype": "{doctype_4}" 193 | # } 194 | # ] 195 | 196 | # Authentication and authorization 197 | # -------------------------------- 198 | 199 | # auth_hooks = [ 200 | # "frappe_er_generator.auth.validate" 201 | # ] 202 | -------------------------------------------------------------------------------- /frappe_er_generator/modules.txt: -------------------------------------------------------------------------------- 1 | Frappe Er Generator -------------------------------------------------------------------------------- /frappe_er_generator/patches.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/The-Commit-Company/frappe_er_generator/24d5d1daac9c2d0d32e5b8c19070c4c5367311c7/frappe_er_generator/patches.txt -------------------------------------------------------------------------------- /frappe_er_generator/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/The-Commit-Company/frappe_er_generator/24d5d1daac9c2d0d32e5b8c19070c4c5367311c7/frappe_er_generator/public/.gitkeep -------------------------------------------------------------------------------- /frappe_er_generator/templates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/The-Commit-Company/frappe_er_generator/24d5d1daac9c2d0d32e5b8c19070c4c5367311c7/frappe_er_generator/templates/__init__.py -------------------------------------------------------------------------------- /frappe_er_generator/templates/pages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/The-Commit-Company/frappe_er_generator/24d5d1daac9c2d0d32e5b8c19070c4c5367311c7/frappe_er_generator/templates/pages/__init__.py -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | License: MIT -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # frappe -- https://github.com/frappe/frappe is installed via 'bench init' 2 | graphviz -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("requirements.txt") as f: 4 | install_requires = f.read().strip().split("\n") 5 | 6 | # get version from __version__ variable in frappe_er_generator/__init__.py 7 | from frappe_er_generator import __version__ as version 8 | 9 | setup( 10 | name="frappe_er_generator", 11 | version=version, 12 | description="ERD generator for frappe doctypes", 13 | author="Sumit Jain", 14 | author_email="jainmit23@gmail.com", 15 | packages=find_packages(), 16 | zip_safe=False, 17 | include_package_data=True, 18 | install_requires=install_requires 19 | ) 20 | --------------------------------------------------------------------------------