├── .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 | 
36 |
37 |
38 | 2. Output of `get_whitelist_methods_in_app`
39 |
40 |
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 | {data.get("name")} |
152 | {table_elements}
153 |
>];"""
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 | Link |
201 | Fetch from |
202 | Custom Fields |
203 | |
205 |
>]
206 | key2 [label=<>]
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 |
--------------------------------------------------------------------------------