gdbcore 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gdbcore-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.1
2
+ Name: gdbcore
3
+ Version: 0.1.0
4
+ Summary:
5
+ Author: Angel L. P.
6
+ Author-email: 59593766+angelphanth@users.noreply.github.com
7
+ Requires-Python: >=3.11,<3.13
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Requires-Dist: matplotlib (>=3.8.1,<4.0.0)
12
+ Requires-Dist: neo4j (>=5.14.1,<6.0.0)
13
+ Requires-Dist: networkx (>=3.1,<4.0)
14
+ Requires-Dist: obonet (>=1.0.0,<2.0.0)
15
+ Requires-Dist: pandas (>=2.1.1,<3.0.0)
16
+ Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
17
+ Requires-Dist: scipy (>=1.12.0,<2.0.0)
18
+ Description-Content-Type: text/markdown
19
+
20
+ # Work In Progress
21
+
22
+ Overall goal: Create KG dbs (LPGs) from node and edge property lists.
23
+
24
+ Current status:
25
+ - Creates / formats node and edge property lists from CSVs.
26
+ - Produces a Networkx graph and .graphml file
27
+ - Produces a Neo4j Graph database (3 venues)
28
+ - Connector to query Neo4j via python
29
+
30
+
31
+ ## Installation and Getting started
32
+
33
+ 1. download requirements
34
+ `poetry install`
35
+
36
+ 2. activate env
37
+ e.g. `poetry shell`
38
+
39
+
@@ -0,0 +1,19 @@
1
+ # Work In Progress
2
+
3
+ Overall goal: Create KG dbs (LPGs) from node and edge property lists.
4
+
5
+ Current status:
6
+ - Creates / formats node and edge property lists from CSVs.
7
+ - Produces a Networkx graph and .graphml file
8
+ - Produces a Neo4j Graph database (3 venues)
9
+ - Connector to query Neo4j via python
10
+
11
+
12
+ ## Installation and Getting started
13
+
14
+ 1. download requirements
15
+ `poetry install`
16
+
17
+ 2. activate env
18
+ e.g. `poetry shell`
19
+
File without changes
@@ -0,0 +1,186 @@
1
+ """
2
+ - The purpose of this script is to create a Neo4j database (db) from node and edge lists
3
+ in the form of data csv files with an associated header csv file.
4
+ - The process will be:
5
+ - init log
6
+ - load config parameters from config/config.yaml
7
+ - establish neo4j python driver connection
8
+ - move the files to *db import folder* from source folder specified in config.yaml -> db setup -> source folder
9
+ - **to find this folder location you can use helpers.utils_neo4j.Neo4jConnection.abs_import_path()
10
+ - if a file from source folder already exists in the db import folder then it will be skipped NOT OVERWRITTEN
11
+ - TODO
12
+
13
+ NEO4J REQUIREMENT:
14
+ - have a running DBMS instance
15
+
16
+ FILE FORMAT REQUIREMENTS: (will include example file later and script to check/format the file sources)
17
+ - the data files and their matching header file must have the SAME
18
+ filenaming except for the suffices of '-header.csv' and 'data.csv'
19
+ OR '-header.csv' and 'data1.csv', 'data2.csv'
20
+ - if node files have 'node' somehwere in the base file name
21
+ - if edge files have 'edge somewhere in the base file name
22
+ - also see here for column naming requirements: https://neo4j.com/docs/operations-manual/current/tutorial/neo4j-admin-import/
23
+ - for node lists:
24
+ - the primary id column/property should be named 'id:ID'
25
+ - should include a :LABEL column with label
26
+ - only one node label type per files (e.g., if a db is going to have Gene and Protein nodes then
27
+ they should have their own set of data+header files)
28
+ - for edge lists:
29
+ - should hav :START_ID, :END_ID, and :TYPE columns at least
30
+ - only one edge label type per file
31
+
32
+ this whole area is TODO
33
+ """
34
+
35
+ import os
36
+
37
+ # import libraries
38
+ import sys
39
+
40
+ # import modules
41
+ from gdbcore.helpers.utils import (
42
+ config_loader,
43
+ copy_recursively,
44
+ filter_filepaths,
45
+ get_args,
46
+ get_logger,
47
+ normalize_url,
48
+ )
49
+ from gdbcore.helpers.utils_cypher import apoc_load_csv_edges, apoc_load_csv_nodes
50
+ from gdbcore.helpers.utils_neo4j import Neo4jConnection
51
+
52
+ if __name__ == "__main__":
53
+
54
+ ## GET ARGS
55
+ args = get_args(
56
+ prog_name="neo4j-data-loader",
57
+ others=dict(description="uses apoc plugin to load nodes and rels"),
58
+ )
59
+
60
+ ## START LOG FILE
61
+ logger = get_logger()
62
+ logger.info(f"Arguments: {args}")
63
+
64
+ ## LOAD CONFIG PARAMETERS
65
+ # getting the config filepath
66
+ config_filepath = args.config
67
+ # log it
68
+ logger.info(f"Path to config file: {config_filepath}")
69
+ # load config params
70
+ logger.info("Loading config params ... ")
71
+ config = config_loader(config_filepath)["apoc_load_queries"]
72
+ logger.info(f"Configuration: {config}")
73
+ # following db set up
74
+ username = config["neo4j"]["username"]
75
+ password = config["neo4j"]["password"]
76
+ db_name = config["db setup"]["db_name"]
77
+ host = config["neo4j"]["host"]
78
+ bolt_port = config["neo4j"]["bolt port"]
79
+ http_port = config["neo4j"]["http port"]
80
+ sep = config["db setup"]["sep"]
81
+ quotechar = config["db setup"]["quotechar"]
82
+ fill_na = config["db setup"]["fill_na"]
83
+ num_transactions = config["neo4j"]["num_transactions"]
84
+ source_folder = config["db setup"]["source folder"]
85
+ import_path = config['neo4j']['import_path']
86
+ logger.info(f"Configuration: {config}")
87
+
88
+ ## MAIN
89
+ # get and check uris
90
+ uri = normalize_url(host, bolt_port, scheme='bolt')
91
+ http_uri = normalize_url(host, http_port)
92
+
93
+ ## CONNECT TO NEO4J
94
+ connection = Neo4jConnection(uri=uri, user=username, pwd=password)
95
+ # log it
96
+ logger.info(f"Neo4j connection established. neo4j vers: {connection.neo4j_version}")
97
+
98
+ # retrieve import path
99
+ if import_path:
100
+ neo_import_path = import_path
101
+ else:
102
+ neo_import_path = connection.abs_import_path()
103
+
104
+ logger.info(f"DB import filepath: {neo_import_path}")
105
+ ## MOVE FILES FROM source_folder TO DB IMPORT FOLDER
106
+ logger.info(f"Copying files from {source_folder} to {neo_import_path}")
107
+ copy_recursively(src=source_folder, dest=neo_import_path)
108
+
109
+ ## RETRIEVE NODE AND EDGE LISTS (DATA AND HEADER CSVs)
110
+ logger.info("Getting filepaths to node and edge lists")
111
+ # getting header files to iterate through
112
+ node_head_files = filter_filepaths(
113
+ neo_import_path, identifiers=["-header.csv", "node"]
114
+ )
115
+ logger.info(f"Identified NODE header files: {node_head_files}")
116
+ # getting data files
117
+ node_data_files = filter_filepaths(neo_import_path, identifiers=["-data", "node"])
118
+ logger.info(f"Identified NODE data files: {node_data_files}")
119
+ edge_head_files = filter_filepaths(
120
+ neo_import_path, identifiers=["-header.csv", "edge"]
121
+ )
122
+ logger.info(f"Identified EDGE header files: {edge_head_files}")
123
+ # getting data files
124
+ edge_data_files = filter_filepaths(neo_import_path, identifiers=["-data", "edge"])
125
+ logger.info(f"Identified EDGE data files: {edge_data_files}")
126
+
127
+ ## GENERATING NODE CYPHER LOADING QUERIES
128
+ node_queries = apoc_load_csv_nodes(
129
+ header_files=node_head_files,
130
+ data_files=node_data_files,
131
+ num_transactions=num_transactions,
132
+ sep=sep,
133
+ quotechar=quotechar,
134
+ fill_na=fill_na,
135
+ )
136
+
137
+ ## GENERATING EDGE CYPHER LOADING QUERIES
138
+ edge_queries = apoc_load_csv_edges(
139
+ header_files=edge_head_files,
140
+ data_files=edge_data_files,
141
+ num_transactions=num_transactions,
142
+ sep=sep,
143
+ quotechar=quotechar,
144
+ fill_na=fill_na,
145
+ )
146
+
147
+ if import_path:
148
+ local_node_queries = [x.replace(import_path, "file://") for x in node_queries]
149
+ local_edge_queries = [x.replace(import_path, "file://") for x in edge_queries]
150
+ else:
151
+ # change filename to file:///
152
+ local_node_queries = connection.to_local_prefix(node_queries)
153
+ # change filenames to file:///
154
+ local_edge_queries = connection.to_local_prefix(edge_queries)
155
+
156
+ # log it
157
+ logger.info("NODE QUERIES:")
158
+ for q in local_node_queries:
159
+ logger.info(q)
160
+ # log it
161
+ logger.info("EDGE QUERIES:")
162
+ for q in local_edge_queries:
163
+ logger.info(q)
164
+
165
+ ## CREATE NODES IN NEO4J DB
166
+ logger.info(f"Creating nodes in {db_name}")
167
+ for q in local_node_queries:
168
+ # run query in neo4j
169
+ stdout = connection.query(query=q, db=db_name)
170
+ # logit
171
+ logger.info(stdout)
172
+
173
+ ## CREATE EDGES IN NEO4J DB
174
+ logger.info(f"Creating edges in {db_name}")
175
+ for q in local_edge_queries:
176
+ # run query in neo4j
177
+ stdout = connection.query(query=q, db=db_name)
178
+ # logit
179
+ logger.info(stdout)
180
+
181
+ # logit
182
+ connection.close()
183
+ logger.info("Run Complete")
184
+
185
+ else:
186
+ print("File imported instead of executed. Not completed.")
@@ -0,0 +1,292 @@
1
+ """
2
+ The purpose of this script is to take node and edge lists (separate files) and create initial node and edge lists as csvs
3
+
4
+ Options to rename mandatory columns and remove some columns.
5
+
6
+ Any additional cleaning of the node and edge lists can be done any way you prefer outside of this script.
7
+
8
+ Requirements:
9
+ - node and edge lists should be separated csvs
10
+ - don't have excess files in the src foldr
11
+ - filenaming:
12
+ - if source files have a separate header the corresponding header should have '-header' in the filename
13
+
14
+ """
15
+
16
+ # imports
17
+ import os
18
+ import pandas as pd
19
+
20
+ # import modules
21
+ from gdbcore.helpers.utils import (
22
+ get_args,
23
+ get_logger,
24
+ config_loader,
25
+ filter_filepaths,
26
+ get_line_count,
27
+ warn_folder,
28
+ assert_nonempty_keys,
29
+ assert_nonempty_vals,
30
+ group_files,
31
+ find_matches,
32
+ get_header,
33
+ )
34
+ from gdbcore.helpers.utils_nelist import IntSorter
35
+ from gdbcore.helpers.utils_cypher import cypher_upper, cypher_camelcase
36
+
37
+
38
+ if __name__ == "__main__":
39
+
40
+ ## GET ARGS
41
+ args = get_args(
42
+ prog_name="csv-formatter",
43
+ others=dict(description="not sure anymore"),
44
+ )
45
+
46
+ ## START LOG FILE
47
+ logger = get_logger()
48
+ logger.info(f"Arguments: {args}")
49
+
50
+ ## LOAD CONFIG PARAMETERS
51
+ # getting the config filepath
52
+ config_filepath = args.config
53
+ # log it
54
+ logger.info(f"Path to config file: {config_filepath}")
55
+ # load config params
56
+ logger.info("Loading config params ... ")
57
+ config = config_loader(config_filepath)
58
+ assert_nonempty_keys(config)
59
+ assert_nonempty_vals(config)
60
+ logger.info(f"Configuration: {config}")
61
+
62
+ # assign to variables for use in script
63
+ chunksize = config["chunksize"]
64
+ # incoming file conditions
65
+ src_fpath = config["source"]["folder path"]
66
+ src_sep_header = config["source"]["separate header"]
67
+ src_multi_csv = config["source"]["multi_csv"]
68
+ src_sep = config["source"]["sep"]
69
+ src_quotechar = config["source"]["quotechar"]
70
+ # column names corresponding to
71
+ assert_nonempty_keys(config["source"]["col_names"])
72
+ assert_nonempty_vals(config["source"]["col_names"])
73
+ src_node_ids = config["source"]["col_names"]["node_ids"]
74
+ src_node_labels = config["source"]["col_names"]["node_labels"]
75
+ src_edge_labels = config["source"]["col_names"]["edge_labels"]
76
+ src_start_ids = config["source"]["col_names"]["start_ids"]
77
+ src_end_ids = config["source"]["col_names"]["end_ids"]
78
+ src_exclude_cols = config["source"]["col_names"]["exclude list"]
79
+ # outcoming file settings
80
+ out_fpath = config["output"]["folder path"]
81
+ out_prefix = config["output"]["prefix"]
82
+ out_sep_header = config["output"]["separate header"]
83
+ out_multi_csv = config["output"]["multi_csv"]
84
+ out_sep = config["output"]["sep"]
85
+ out_quotechar = config["output"]["quotechar"]
86
+ # column names corresponding to
87
+ assert_nonempty_keys(config["output"]["col_names"])
88
+ assert_nonempty_vals(config["output"]["col_names"])
89
+ out_node_ids = config["output"]["col_names"]["node_ids"]
90
+ out_node_labels = config["output"]["col_names"]["node_labels"]
91
+ out_edge_labels = config["output"]["col_names"]["edge_labels"]
92
+ out_start_ids = config["output"]["col_names"]["start_ids"]
93
+ out_end_ids = config["output"]["col_names"]["end_ids"]
94
+ out_max_rows = config["output"]["max_rows"]
95
+ # additional db specific formating
96
+ out_cypher = config["output"]["cypher_formatting"]
97
+
98
+ # warning
99
+ message = f"""
100
+ Warning: There are existing files in the output path {out_fpath}.
101
+ There is a risk of duplicating nodes and edges being appended to these files.
102
+ """
103
+ logger.info(warn_folder(folderpath=out_fpath, warning_message=message))
104
+
105
+ # getting filepaths
106
+ datafiles = filter_filepaths(
107
+ src_fpath, identifiers=[".csv"], exclude=["-header", ".graphml"]
108
+ )
109
+ headfiles = filter_filepaths(
110
+ src_fpath, identifiers=["-header", ".csv"], exclude=[".graphml"]
111
+ )
112
+
113
+ # grouping them to accomodate multi_csv (this may not be necessary anymore)
114
+
115
+ grouped_files = group_files(
116
+ source_folder=datafiles,
117
+ sub=("\d*.csv", ""),
118
+ )
119
+ logger.info(group_files)
120
+
121
+ # node files first
122
+ for group in grouped_files:
123
+
124
+ # get the group of files
125
+ filelist = grouped_files[group]
126
+ logger.info(filelist)
127
+
128
+ # get the original base filename
129
+ og_fname = os.path.splitext(os.path.basename(group))[0]
130
+
131
+ # preparing the new filenames
132
+ filename_start = f"{out_prefix}_{og_fname}"
133
+ logger.info(filename_start)
134
+
135
+ # preparing header and data filenames
136
+ header_fname = f"{filename_start}-header.csv"
137
+ data_fname_prefix = f"{filename_start}-data"
138
+ # preparing header and data file output paths
139
+ header_fpath = os.path.join(out_fpath, header_fname)
140
+ data_fpath_start = os.path.join(out_fpath, data_fname_prefix)
141
+
142
+ for file in filelist:
143
+
144
+ logger.info(f"Searching {file} ...")
145
+
146
+ # if the source files have separate header files
147
+ if src_sep_header:
148
+ # get the matching header
149
+ header_file = find_matches(file, area=headfiles)
150
+ header = get_header(
151
+ header_file[0], sep=src_sep, quotechar=src_quotechar
152
+ )
153
+
154
+ reader = pd.read_csv(
155
+ file,
156
+ names=[x for x in header if x not in src_exclude_cols],
157
+ chunksize=chunksize,
158
+ sep=src_sep,
159
+ usecols=lambda x: x not in src_exclude_cols,
160
+ )
161
+ # otherwise don't attach header
162
+ else:
163
+ reader = pd.read_csv(
164
+ file,
165
+ header=0,
166
+ chunksize=chunksize,
167
+ sep=src_sep,
168
+ usecols=lambda x: x not in src_exclude_cols,
169
+ )
170
+
171
+ for chunk in reader:
172
+
173
+ if out_multi_csv:
174
+ # getting the latest data file
175
+ # first get all data files for that node/edge type
176
+ data_list = filter_filepaths(
177
+ fpath=out_fpath, identifiers=[data_fname_prefix]
178
+ )
179
+
180
+ if len(data_list) > 0:
181
+ # sort by digit suffix using utils.IntSorter
182
+ dl_sorter = IntSorter(
183
+ regex=f"{data_fpath_start}(\d+).csv", default_val=0
184
+ )
185
+ # getting the latest file (biggest digit suffix)
186
+ sorted_dl = dl_sorter.sort(
187
+ a_list=data_list,
188
+ num_reverse=True,
189
+ str_reverse=False,
190
+ str_lower=False,
191
+ )
192
+ latest_fname = sorted_dl[0]
193
+ # what's the latest file number?
194
+ latest_fnum = dl_sorter.get_int(latest_fname)
195
+
196
+ # create a new file if the latest file reaches max_rows
197
+ if get_line_count(latest_fname) >= out_max_rows:
198
+ current_data_fname = (
199
+ f"{data_fpath_start}{latest_fnum + 1}.csv"
200
+ )
201
+ print(f"Creating {current_data_fname}...")
202
+ else:
203
+ current_data_fname = latest_fname
204
+ else:
205
+ # if no data files create the first one
206
+ current_data_fname = f"{data_fpath_start}1.csv"
207
+ print(f"Creating {current_data_fname}...")
208
+ else:
209
+ # if not writing node/edges to multiple csvs, then use suffix 1 always
210
+ current_data_fname = f"{data_fpath_start}1.csv"
211
+
212
+ # prep rename dict
213
+ src_v_out = {
214
+ src_node_ids: out_node_ids,
215
+ src_node_labels: out_node_labels,
216
+ src_edge_labels: out_edge_labels,
217
+ src_start_ids: out_start_ids,
218
+ src_end_ids: out_end_ids,
219
+ }
220
+ # rename columns?
221
+ chunk = chunk.rename(columns=src_v_out)
222
+
223
+ # cypher formatting?
224
+ if out_cypher:
225
+ # for nodes
226
+ try:
227
+ # CamelCase node labels
228
+ chunk[out_node_labels] = chunk[out_node_labels].apply(
229
+ cypher_camelcase,
230
+ lower_first=False,
231
+ )
232
+ except Exception:
233
+ logger.info(
234
+ f"CamelCase of {out_node_labels} not completed. May be due to labels not in this csv - column names: {chunk.columns}"
235
+ )
236
+ # for edges
237
+ try:
238
+ # UPPER_CASE edge labels
239
+ chunk[out_edge_labels] = chunk[out_edge_labels].apply(
240
+ cypher_upper
241
+ )
242
+ except Exception:
243
+ logger.info(
244
+ f"UPPER_CASE of {out_edge_labels} not completed. May be due to labels not in this csv - column names: {chunk.columns}"
245
+ )
246
+ # camelCase property names
247
+ chunk = chunk.rename(
248
+ columns=lambda x: (
249
+ cypher_camelcase(x, lower_first=True)
250
+ if x not in src_v_out.values()
251
+ else x
252
+ )
253
+ )
254
+
255
+ # append to outfiles
256
+ if out_sep_header:
257
+ chunk.to_csv(
258
+ current_data_fname,
259
+ mode="a",
260
+ header=False,
261
+ index=False,
262
+ sep=out_sep,
263
+ )
264
+ # WRITING HEADER FILE
265
+ # checking if header already exists
266
+ if (os.path.exists(header_fpath) == False) or get_line_count(
267
+ header_fpath
268
+ ) < 1:
269
+ # if doesn't exist then create it
270
+ logger.info(f"Creating {header_fpath}...")
271
+ # write header to header file
272
+ chunk.head(0).to_csv(
273
+ header_fpath,
274
+ mode="w",
275
+ header=True,
276
+ index=False,
277
+ sep=out_sep,
278
+ )
279
+ else:
280
+ chunk.to_csv(
281
+ current_data_fname,
282
+ mode="a",
283
+ header=True,
284
+ index=False,
285
+ sep=out_sep,
286
+ )
287
+
288
+ # done statement
289
+ logger.info("Run Complete.")
290
+
291
+ else:
292
+ print("File imported instead of executed. Not completed.")