arc-utils 1.2.1__py3-none-any.whl

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.
arc_utils/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """A collection of python utilities for ArcGIS Pro
2
+ """
3
+ __version__ = '1.1'
4
+ __author__ = 'Grant Herbert'
5
+
6
+ __all__ = ['aprx.py', 'gdb.py', 'table.py']
7
+
8
+ from arc_utils import gdb
9
+ from arc_utils import table
10
+ from arc_utils import aprx
arc_utils/aprx.py ADDED
@@ -0,0 +1,64 @@
1
+ # -*- coding: utf-8 -*-
2
+ """ArcGIS Pro aprx related utilities
3
+ """
4
+
5
+ import arcpy
6
+
7
+ class AprxObj(object):
8
+ """ provide methods for working with a ArcGIS Pro aprx file
9
+ all standard arcpy methods are available via .aprx
10
+ Usage: pro_doc = arc_utils.aprx.AprxObj(path)
11
+ :param
12
+ path: a string representing an aprx file,
13
+ or "CURRENT" if used in ArcGIS Pro Python window
14
+ """
15
+ def __init__(self, aprx_path):
16
+ """ :param path to aprx or "CURRENT"
17
+ """
18
+ self.aprx = arcpy.mp.ArcGISProject(aprx_path)
19
+ self.path = aprx_path
20
+ self.maps = self.get_maps()
21
+ self.layouts = self.get_layouts()
22
+
23
+ def get_maps(self):
24
+ """ :return array of map objects"""
25
+ map_array = []
26
+ for map in self.aprx.listMaps():
27
+ map_array.append(map)
28
+ return map_array
29
+
30
+ def get_layouts(self):
31
+ """ :return array of layout objects"""
32
+ layout_array = []
33
+ for layout in self.aprx.listLayouts():
34
+ layout_array.append(layout)
35
+ return layout_array
36
+
37
+ def get_layer_obj_as_array(self, map):
38
+ """ :param: map object
39
+ :return array of layer objects in a specified map object"""
40
+ layer_obj_array = []
41
+ for layer in map.listLayers():
42
+ layer_obj_array.append(layer)
43
+ return layer_obj_array
44
+
45
+ def get_layer_names_as_array(self, map):
46
+ """ :param: map object
47
+ :return array of layer names in a specified map object"""
48
+ lyr_name_array = []
49
+ for layer in map.listLayers():
50
+ lyr_name_array.append(layer.name)
51
+ return lyr_name_array
52
+
53
+ def layer_obj_generator(self, map):
54
+ """ :param: map object
55
+ yields layer objects"""
56
+ for layer in map.listLayers():
57
+ yield layer
58
+
59
+ def layer_names_generator(self, map):
60
+ """ :param: map object
61
+ yields layer names"""
62
+ for layer in map.listLayers():
63
+ yield layer.name
64
+
arc_utils/gdb.py ADDED
@@ -0,0 +1,196 @@
1
+ # -*- coding: utf-8 -*-
2
+ """utilities for working with geodatabases and reporting on geodatabase contents
3
+ """
4
+ from __future__ import print_function, unicode_literals, absolute_import
5
+ import os
6
+ import arcpy
7
+ from .output import get_valid_output_path
8
+ from .output import output_msg
9
+
10
+ class GDBObj(object):
11
+ """ provide properties for working with a GDB
12
+ Usage: gdb = arc_utils.gdb.GDBObj(path)
13
+ :param
14
+ path: a string representing a GDB connection/path
15
+ """
16
+ def __init__(self, gdb_path):
17
+ """ sets up reference to table
18
+ adds properties and methods
19
+ """
20
+ self.path = gdb_path
21
+ self.describe_obj = self._describe_object()
22
+ self.feature_classes = self.get_feature_class_names()
23
+ self.tables = self.get_table_names()
24
+ self.domain_names = self.get_all_domain_names()
25
+
26
+ def _describe_object(self):
27
+ """ returns describe object"""
28
+ return arcpy.Describe(self.path)
29
+
30
+ def get_feature_class_names(self):
31
+ """ get a list of all the featureclass names"""
32
+ fc_list = []
33
+ temp_ws = arcpy.env.workspace
34
+ arcpy.env.workspace = self.path
35
+ datasets = arcpy.ListDatasets(feature_type='feature')
36
+ datasets = [''] + datasets if datasets is not None else []
37
+ for dataset in datasets:
38
+ for fc in arcpy.ListFeatureClasses(feature_dataset=dataset):
39
+ fc_list.append(fc)
40
+ arcpy.env.workspace = temp_ws
41
+ return fc_list
42
+
43
+ def get_table_names(self):
44
+ """ get a list of all the table names"""
45
+ tbl_list = []
46
+ temp_ws = arcpy.env.workspace
47
+ arcpy.env.workspace = self.path
48
+ tbls = arcpy.ListTables()
49
+ for tbl in tbls:
50
+ tbl_list.append(tbl)
51
+ arcpy.env.workspace = temp_ws
52
+ return tbl_list
53
+
54
+ def get_all_domain_names(self):
55
+ domain_names = []
56
+ domains = arcpy.da.ListDomains(self.path)
57
+ for domain in domains:
58
+ domain_names.append(domain.name)
59
+ return domain_names
60
+
61
+ def OID_check(self):
62
+ for fc in self.feature_classes:
63
+ oidfield = ''
64
+ tmp = arcpy.Describe(fc)
65
+ if tmp.hasOID:
66
+ oidfield = tmp.OIDFieldName
67
+ print('featureclass', tmp.name, tmp.hasOID, oidfield)
68
+ for tb in self.tables:
69
+ oidfield = ''
70
+ tmp = arcpy.Describe(tb)
71
+ if tmp.hasOID:
72
+ oidfield = tmp.OIDFieldName
73
+ print('table', tmp.name, tmp.hasOID, oidfield)
74
+
75
+ #TODO split into 3 - get all FC, create formatted string output, write to file. use io
76
+ def report_all_fc_as_text(geodatabase, output_file=None, sep='\t'):
77
+ """Create a text report of all fields in all featureclasses/tables from a geodatabase
78
+ to specified output file.
79
+
80
+ :param geodatabase {String}
81
+ Path or reference to a geodatabase.
82
+
83
+ :param output_file {String}
84
+ Path or reference to a text file. If not supplied defaults to gdb directory.
85
+
86
+ :param sep {String}
87
+ seperator value (eg ',' or r'\t'
88
+ """
89
+ default_env = arcpy.env.workspace
90
+ try:
91
+ desc = arcpy.Describe(geodatabase)
92
+ arcpy.env.workspace = geodatabase
93
+ if not output_file:
94
+ path = get_valid_output_path(desc.Path)
95
+ output_file = os.path.join(path, desc.name.split(".")[0] + ".txt")
96
+
97
+ output_msg("Writing to: {0}".format(output_file))
98
+ # list all datasets
99
+ datasets = arcpy.ListDatasets(feature_type='feature')
100
+ datasets = [''] + datasets if datasets is not None else []
101
+ # write out gdb info, fields etc
102
+ with open(output_file, "w") as logFile:
103
+ header = ["FCDataset","Feature"]
104
+ atts = ['name', 'baseName', 'aliasName', 'type', 'length', 'precision', 'scale',
105
+ 'domain', 'defaultValue', 'editable', 'isNullable', 'required']
106
+ header.extend(atts)
107
+ header_output = sep.join(["{}".format(i) for i in header])
108
+ logFile.write(header_output + "\n")
109
+ for tbl in arcpy.ListTables():
110
+ output_msg("Processing Table: {0}".format(tbl))
111
+ try:
112
+ fields = arcpy.ListFields(tbl)
113
+ str_output = ""
114
+ line_start = "{0}{1}{2}{1}".format("", sep, tbl)
115
+ for field in fields:
116
+ str_output += line_start + sep.join(["{}".format(getattr(field, i)) for i in atts])
117
+ str_output += "\n"
118
+ logFile.write(str_output)
119
+
120
+ except Exception as e:
121
+ output_msg(str(e.args[0]))
122
+ output_msg(arcpy.GetMessages())
123
+ continue
124
+
125
+ for dataset in datasets:
126
+ for fc in arcpy.ListFeatureClasses(feature_dataset=dataset):
127
+ output_msg("Processing Dataset: {0} \ FeatureClass: {1}".format(dataset, fc))
128
+ try:
129
+ fields = arcpy.ListFields(fc)
130
+ str_output = ""
131
+ line_start = "{0}{1}{2}{1}".format(dataset, sep, fc)
132
+ for field in fields:
133
+ str_output += line_start + sep.join(["{}".format(getattr(field, i)) for i in atts])
134
+ str_output += "\n"
135
+ logFile.write(str_output)
136
+
137
+ except Exception as e:
138
+ output_msg(str(e.args[0]))
139
+ output_msg(arcpy.GetMessages())
140
+ continue
141
+
142
+ except Exception as e:
143
+ output_msg(str(e.args[0]))
144
+ output_msg(arcpy.GetMessages())
145
+ finally:
146
+ arcpy.env.workspace = default_env
147
+ output_msg("Completed")
148
+
149
+
150
+ def export_all_domains(geodatabase, workspace=None):
151
+ """Output all the domains in a geodatabase
152
+ to tables in a workspace.
153
+
154
+ :param geodatabase {String}:
155
+ Path or reference to input geodatabase.
156
+
157
+ :param workspace {String}: Optional
158
+ Path to output folder or geodatabase. Defaults to input geodatabase.
159
+ """
160
+ try:
161
+ if not workspace:
162
+ workspace = geodatabase
163
+ domains = arcpy.da.ListDomains(geodatabase)
164
+ for domain in domains:
165
+ dname = arcpy.ValidateTableName(domain.name + '_domain', workspace)
166
+ output = os.path.join(workspace, dname)
167
+ if workspace != geodatabase:
168
+ output += '.txt'
169
+ output_msg('Exporting {0} domain to {1}'.format(domain.name, dname))
170
+ arcpy.DomainToTable_management(geodatabase, domain.name, output, "codedValues", 'description')
171
+
172
+ except Exception as e:
173
+ output_msg(str(e.args[0]))
174
+ output_msg(arcpy.GetMessages())
175
+ finally:
176
+ output_msg("Completed")
177
+
178
+
179
+ def import_tables_as_domains(tables, geodatabase):
180
+ """import tables as domains into geodatabase. Expects fields 'codedValues' and 'description'
181
+
182
+ :param tables {string}
183
+ path or array of paths of input tables containing domain data
184
+
185
+ :param geodatabase
186
+ Path or reference to geodatabase."""
187
+ try:
188
+ for table in tables:
189
+ desc = arcpy.Describe(table)
190
+ dname = desc.name.replace("_domain", "")
191
+ arcpy.TableToDomain_management(table, "codedValues", "description", geodatabase, dname)
192
+ except Exception as e:
193
+ output_msg(str(e.args[0]))
194
+ output_msg(arcpy.GetMessages())
195
+ finally:
196
+ output_msg("Completed")
arc_utils/output.py ADDED
@@ -0,0 +1,80 @@
1
+ # -*- coding: utf-8 -*-
2
+ """output related utilities
3
+ """
4
+ from __future__ import print_function, unicode_literals, absolute_import
5
+ import arcpy
6
+ import os
7
+
8
+ def output_msg(msg, severity=0):
9
+ """Output msg to print and/or to Arc. Useful to include in a tool
10
+ that can be run in Python or in ArcGIS
11
+
12
+ msg{String}:
13
+ message to output.
14
+
15
+ severity(integer):
16
+ severity = 0 (none), 1 (warning), 2 (error)
17
+ usage:
18
+ output_msg("message")
19
+
20
+ function from http://help.arcgis.com/en/arcgisdesktop/10.0/help/index.html#//00150000000p000000.htm
21
+ """
22
+
23
+ # Adds a Message (in case this is run as a tool)
24
+ # and also prints the message to the screen (standard output)
25
+ #
26
+ print(msg)
27
+
28
+ # Split the message on \n first, so that if it's multiple lines,
29
+ # a GPMessage will be added for each line
30
+ try:
31
+ for string in msg.split('\n'):
32
+ # Add appropriate geoprocessing message
33
+ #
34
+ if severity == 0:
35
+ arcpy.AddMessage(string)
36
+ elif severity == 1:
37
+ arcpy.AddWarning(string)
38
+ elif severity == 2:
39
+ arcpy.AddError(string)
40
+ except:
41
+ pass
42
+
43
+
44
+ def get_valid_output_path(path, folder_reqd=True, make_dir=True):
45
+ """ return a valid path for an output, eg a folder path.
46
+ If folder_reqd is True (default) and a gdb is passed as path,
47
+ will drop down to the folder containing gdb.
48
+ If path does not exist and make_dir is True (default)
49
+ folder will be created.
50
+ Otherwise will default to userprofile\documents folder
51
+ path {String}:
52
+ filepath
53
+ folder_reqd {Boolean}:
54
+ returned path must be a folder
55
+ make_dir {Boolean}:
56
+ make the directory if not exists
57
+ :return path
58
+ """
59
+ report_dir = path
60
+ if os.path.isdir(path):
61
+ if folder_reqd:
62
+ if path.lower().endswith(".gdb"):
63
+ report_dir = arcpy.Describe(path).Path
64
+ elif make_dir:
65
+ os.makedirs(path)
66
+ report_dir = path
67
+ else:
68
+ report_dir = os.environ['USERPROFILE']
69
+ if os.path.exists(report_dir + "\\Documents"):
70
+ report_dir = report_dir + "\\Documents"
71
+
72
+ return report_dir
73
+
74
+
75
+ def output_to_file(data, path, filename):
76
+ """send result string to file"""
77
+ path = get_valid_output_path(path)
78
+ output_file = os.path.join(path, filename)
79
+ with open(output_file, "w") as output:
80
+ output_file.write('{}').format(data)
arc_utils/table.py ADDED
@@ -0,0 +1,502 @@
1
+ # -*- coding: utf-8 -*-
2
+ """utilities for working with tables, featureclasses and fields
3
+ """
4
+ from __future__ import print_function, unicode_literals, absolute_import
5
+ import arcpy
6
+ from .output import get_valid_output_path
7
+ from .output import output_msg
8
+
9
+
10
+ class TableObj(object):
11
+ """ provide properties for working with a table/featureclass
12
+ Usage: tbl = arc_utils.table.TableObj(path)
13
+ :param
14
+ path: a string representing an table/featureclass
15
+ """
16
+ def __init__(self, table_path):
17
+ """ sets up reference to table
18
+ adds properties and methods
19
+ """
20
+ self.path = table_path
21
+ self.describe_obj = self._describe_object()
22
+ self.name = self._get_fc_name()
23
+ self.type = self._get_fc_type()
24
+ self.field_dict = self._make_field_dict()
25
+ self.fields = self._list_field_names()
26
+ self.fields2 = self._list_field_names(required=False)
27
+ self.fieldaliases = self._list_field_names(aliases=True)
28
+
29
+
30
+ def _describe_object(self):
31
+ """ returns describe object"""
32
+ return arcpy.Describe(self.path)
33
+
34
+ def _get_fc_name(self):
35
+ return self.describe_obj.baseName
36
+
37
+ def _get_fc_type(self):
38
+ if hasattr(self.describe_obj, 'shapeType'):
39
+ return self.describe_obj.shapeType
40
+ elif hasattr(self.describe_obj, 'dataType'):
41
+ return self.describe_obj.dataType
42
+ else:
43
+ return 'Unknown'
44
+
45
+ def _list_field_names(self, required=True, aliases=False):
46
+ """Array of field names
47
+ """
48
+ if required:
49
+ f_list = [field['name'] for field in self.field_dict.values()]
50
+ else:
51
+ f_list = [field['name'] for field in self.field_dict.values() if not field['required']]
52
+ if aliases:
53
+ # override the output to use aliases instead of field names
54
+ f_list = [self.field_dict[field]['aliasName'] for field in f_list]
55
+ return f_list
56
+
57
+ def _make_field_dict(self):
58
+ """Dictionary of fields containing
59
+ all properties exposed by the arcpy.ListFields tool
60
+ """
61
+ field_dict = dict()
62
+ for field in arcpy.ListFields(self.path):
63
+ field_dict[field.name] = {
64
+ "name": field.name,
65
+ "baseName": field.baseName,
66
+ "aliasName": field.aliasName,
67
+ "type": field.type,
68
+ "length": field.length,
69
+ "required": field.required,
70
+ "domain": field.domain,
71
+ "defaultValue": field.defaultValue,
72
+ "precision": field.precision,
73
+ "scale": field.scale,
74
+ "isNullable": field.isNullable,
75
+ "editable": field.editable
76
+ }
77
+ return field_dict
78
+
79
+ def get_field_info_as_text(self, sep="\t"):
80
+ """ Create a delimeter separated output of a table's fields and their properties
81
+ :param sep {String}
82
+ Separator value to use. eg r"\t" for tab (default), "," for comma
83
+ :return string of values with separator between
84
+ """
85
+ str_output = sep.join("{}\n".format(self.field_dict.keys()))
86
+ for k in self.field_dict.keys():
87
+ str_output += self.field_dict[k] + sep
88
+ str_output += "\n"
89
+ return str_output
90
+
91
+ def get_max_field_value(self, field, lengthcomp=False):
92
+ """Return the largest value (if numeric).
93
+ lexicographic string comparison is used to determine largest value for strings by default.
94
+ :param {String} field:
95
+ name of the field to parse
96
+ :param {Boolean} lengthcomp:
97
+ If True will compare strings for length rather than lexicographically (ascii value of letters)
98
+ """
99
+ field_type = self.field_dict[field]['type']
100
+ if field_type in ["Geometry"]:
101
+ print("Cannot process Geometry field")
102
+ return None
103
+ elif field_type in ["String"]:
104
+ result = ''
105
+ else:
106
+ result = 0
107
+ with arcpy.da.SearchCursor(self.path, field) as values:
108
+ for value in values:
109
+ if value[0] is not None:
110
+ val = value[0]
111
+ if field_type in ["String"] and lengthcomp:
112
+ if len(val) > len(result):
113
+ result = val
114
+ else:
115
+ if val > result:
116
+ result = val
117
+ return result
118
+
119
+ def get_max_field_value_length(self, field):
120
+ """Return the length of the maximum value in the field.
121
+ :param: field {String}:
122
+ name of the field to parse
123
+ """
124
+ length = 0
125
+ with arcpy.da.SearchCursor(self.path, field) as values:
126
+ for value in values:
127
+ if value[0] is not None:
128
+ val = str(value[0])
129
+ if len(val) > length:
130
+ length = len(val)
131
+ return length
132
+
133
+ def get_field_value_set(self, field):
134
+ """Return set of unique field values
135
+ :param field {String}:
136
+ name of the field to parse
137
+ :return set of unique values. Null values are represented as 'NULL' string
138
+ """
139
+ try:
140
+ value_set = set() # set to hold unique values
141
+ with arcpy.da.SearchCursor(self.path, field) as values:
142
+ for value in values:
143
+ if value[0] is None:
144
+ value_set.add("NULL")
145
+ else:
146
+ value_set.add(value[0])
147
+ return value_set
148
+
149
+ except arcpy.ExecuteError:
150
+ output_msg(arcpy.GetMessages(2))
151
+ except Exception as e:
152
+ output_msg(e.args[0])
153
+
154
+ def get_multiple_field_value_set(self, fields, sep=':'):
155
+ """return a set of unique field values for an input table
156
+ and any number of fields (values will be concatenated with sep)
157
+ :param fields {array of String values}:
158
+ single field name or an array of field names (['Field1', 'Field2'])
159
+ :param sep {String}:
160
+ character to use as a separator (default = ':'
161
+ """
162
+ if not isinstance(fields, list):
163
+ fieldslist = [fields]
164
+ else:
165
+ fieldslist = fields
166
+
167
+ result = set()
168
+
169
+ with arcpy.da.SearchCursor(self.path, fieldslist) as cursor:
170
+ for row in cursor:
171
+ parts = [('NULL' if v is None else str(v)) for v in row]
172
+ result.add(sep.join(parts))
173
+
174
+ return set(result)
175
+
176
+ def find_duplicate_field_values(self, field, charset='ascii'):
177
+ """Return set of unique field values
178
+ :param field {String}:
179
+ name of the field to parse
180
+ :param: charset {String}:
181
+ character set to use (default = 'ascii').
182
+ Valid values are those in the Python documentation for string encode.
183
+ :return set of values which are duplicated in the field (ignores Null values).
184
+ """
185
+ try:
186
+ dup_set = set() # set to hold duplicate values
187
+ value_set = set() # set to hold unique values
188
+ with arcpy.da.SearchCursor(self.path, field) as values:
189
+ for value in values:
190
+ if value[0] in value_set:
191
+ dup_set.add(value[0])
192
+ else:
193
+ value_set.add(value[0])
194
+ return dup_set
195
+
196
+ except arcpy.ExecuteError:
197
+ output_msg(arcpy.GetMessages(2))
198
+ except Exception as e:
199
+ output_msg(e.args[0])
200
+
201
+ def find_duplicate_field_values_as_df(path, fields):
202
+ """Return set of unique field values
203
+ :param path {String}:
204
+ path to data
205
+ :param field [{String}]:
206
+ list of name(s) of fields to parse
207
+ :return Pandas DataFrame of values which are duplicated in the field with count > 1 (ignores Null values).
208
+ Use Pandas DataFrame <result>.to_csv() to export to file
209
+ """
210
+ if not isinstance(fields, list):
211
+ fieldslist = [fields]
212
+ else:
213
+ fieldslist = fields
214
+
215
+ import pandas
216
+ data = arcpy.da.TableToNumPyArray(path, fieldslist, null_value='NULL')
217
+ df = pandas.DataFrame(data)
218
+ count = df.groupby(fields).size().reset_index().rename(columns={0:'count'})
219
+ dups = count[count['count'] > 1]
220
+ return dups
221
+
222
+ def export_schema_to_csv(self, path):
223
+ """Create a csv schema report of all fields in a featureclass,
224
+ to the supplied path.
225
+ """
226
+ import datetime
227
+ import os
228
+ import csv
229
+ # TODO convert to use csv
230
+ start_time = datetime.datetime.today()
231
+ start_date_string = start_time.strftime('%Y%m%d')
232
+ default_env = arcpy.env.workspace
233
+ fc = self.path
234
+ delimiter = ','
235
+ # nice to convert reported types to types accepted by add field tool
236
+ type_conversions = {"String": "TEXT", "Float": "FLOAT", "Double": "DOUBLE", "SmallInteger": "SHORT",
237
+ "Integer": "LONG", "Date": "DATE", "Blob": "BLOB", "Raster": "RASTER", "GUID": "GUID",
238
+ "TRUE": "True", "FALSE": "False", "Geometry": "GEOMETRY", "OID": "OID"}
239
+
240
+ output_msg("Processing: {}".format(self.path))
241
+
242
+ try:
243
+ fc_type = self.type
244
+
245
+ report_dir = get_valid_output_path(path)
246
+ out_file_name = self.name + "_Field_Report " + start_date_string + ".csv"
247
+ out_file_path = os.path.join(report_dir, out_file_name)
248
+ output_msg("Report file: {0}".format(out_file_path))
249
+ with open(out_file_path, "w") as logFile:
250
+ #logFile.write("Type,{}\n".format(fc_type))
251
+ logFile.write(
252
+ "FieldName,FieldType,FieldPrecision,FieldScale,FieldLength,FieldAlias,isNullable,Required,"
253
+ "FieldDomain,DefaultValue,Editable,BaseName\n")
254
+
255
+ for field in self.field_dict:
256
+ output_msg("Writing {}".format(field))
257
+ logFile.write("{0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10},{11}\n".format(
258
+ self.field_dict[field]['name'],
259
+ type_conversions[self.field_dict[field]['type']],
260
+ self.field_dict[field]['precision'],
261
+ self.field_dict[field]['scale'],
262
+ self.field_dict[field]['length'],
263
+ self.field_dict[field]['aliasName'],
264
+ self.field_dict[field]['isNullable'],
265
+ self.field_dict[field]['required'],
266
+ self.field_dict[field]['domain'],
267
+ self.field_dict[field]['defaultValue'],
268
+ self.field_dict[field]['editable'],
269
+ self.field_dict[field]['baseName'])
270
+ )
271
+
272
+ return out_file_path
273
+ except Exception as e:
274
+ output_msg("error: " + str(e.args[0]))
275
+ output_msg(arcpy.GetMessages())
276
+
277
+ def compare_field_values_to_domain(self, field, gdb, domain_name):
278
+ """compare field values with domain values
279
+ return a named tuple (matched = values in domain,
280
+ unmatched = values outside of domain
281
+ NULL is skipped
282
+
283
+ :param field {string}
284
+ Field name
285
+ :param gdb {string}
286
+ Geodatabase path
287
+ :param domain_name {string}
288
+ Domain name in gdb
289
+ """
290
+ from collections import namedtuple
291
+ nt = namedtuple('Result', 'match unmatched')
292
+ field_values = self.get_field_value_set(field)
293
+ domain_values = []
294
+ domain_type = None
295
+ field_in_domain = []
296
+ field_out_domain = []
297
+ domains = arcpy.da.ListDomains(gdb)
298
+ for domain in domains:
299
+ if domain.name == domain_name:
300
+ if domain.domainType == 'CodedValue':
301
+ domain_type = 'CodedValue'
302
+ coded_values = domain.codedValues
303
+ for code, descr in coded_values.items():
304
+ domain_values.append(code)
305
+ elif domain.domainType == 'Range':
306
+ domain_type = 'Range'
307
+ domain_values.append(domain.range[0])
308
+ domain_values.append(domain.range[1])
309
+ break
310
+ if domain_type == 'Range':
311
+ # compare upper and lower bounds
312
+ for fv in field_values:
313
+ if fv == 'NULL':
314
+ continue
315
+ elif int(fv) < domain_values[0] or int(fv) > domain_values[1]:
316
+ field_out_domain.append(fv)
317
+ else:
318
+ field_in_domain.append(fv)
319
+ else:
320
+ for fv in field_values:
321
+ if type(fv) == bytes: # Python 3 catch as domain values are strings
322
+ fv = fv.decode()
323
+ if fv in domain_values:
324
+ field_in_domain.append(fv)
325
+ else:
326
+ field_out_domain.append(fv)
327
+
328
+ return nt(field_in_domain, field_out_domain)
329
+
330
+ def pretty_print(self):
331
+ """ pretty print a table's fields and their properties
332
+ """
333
+ def _print(l):
334
+ print("".join(["{:>12}".format(i) for i in l]))
335
+
336
+ atts = ['name', 'aliasName', 'type', 'baseName', 'domain',
337
+ 'editable', 'isNullable', 'length', 'precision',
338
+ 'required', 'scale', ]
339
+ _print(atts)
340
+
341
+ for f in arcpy.ListFields(self.path):
342
+ _print(["{:>12}".format(getattr(f, i)) for i in atts])
343
+
344
+
345
+ def get_max_field_value(input_fc, field, treatasfloat=False):
346
+ """Return either the longest string in the field,
347
+ or the largest number.
348
+ :param input_fc {String}:
349
+ Path or reference to feature class or table.
350
+ :param field {String}:
351
+ name of the field to parse
352
+ :param treatasfloat:
353
+ setting to treat the field as a float value (expects all numeric)
354
+ :return value of largest field entry
355
+ """
356
+ result = None
357
+ with arcpy.da.SearchCursor(input_fc, field) as values:
358
+ for value in values:
359
+ if value[0] is not None:
360
+ if treatasfloat:
361
+ val = float(value[0])
362
+ else:
363
+ val = value[0]
364
+ if isinstance(val, str):
365
+ # return longest string
366
+ if result is None:
367
+ result = ''
368
+ if len(val) > len(result):
369
+ result = val
370
+ else:
371
+ if result is None:
372
+ result = 0
373
+ if val > result:
374
+ result = val
375
+ return result
376
+
377
+
378
+ def export_field_sets(fc_list, out_xlsx, ignore_fields=["objectid","globalid"], use_lyr_alias=True):
379
+ """ Export unique field values for each layer in a list to an Excel file with one sheet per layer
380
+ :param fc_list: list of feature class or table paths
381
+ :param out_xlsx: path to output Excel file
382
+ :param ignore_fields: list of lowercase fields to ignore (e.g. ['objectid', 'shape'])
383
+ :param use_lyr_alias: if True, use layer alias for sheet name;"""
384
+ import os
385
+ from openpyxl import Workbook
386
+
387
+ ignore_fields = [v.lower() for v in ignore_fields]
388
+
389
+ # Create workbook
390
+ wb = Workbook()
391
+ # Remove the default sheet that openpyxl creates
392
+ default_sheet = wb.active
393
+ wb.remove(default_sheet)
394
+
395
+ # Loop through layers and create a sheet per layer
396
+ for lyr in layr_list:
397
+ tbl = TableObj(lyr)
398
+
399
+ print(f'Processing name: {tbl.name}, alias: {str(lyr)}')
400
+
401
+ # Excel sheet name must be <= 31 chars and cannot contain: : \ / ? * [ ]
402
+ if use_lyr_alias:
403
+ raw_name = str(lyr)
404
+ else:
405
+ raw_name = str(tbl.name) if hasattr(tbl, "name") else str(lyr)
406
+ safe_name = "".join(ch for ch in raw_name if ch not in r':\/?*[]')
407
+ safe_name = safe_name[:31] if len(safe_name) > 31 else safe_name
408
+ if not safe_name:
409
+ safe_name = "Sheet"
410
+
411
+ ws = wb.create_sheet(title=safe_name)
412
+
413
+ # Header row
414
+ ws["A1"] = "Field"
415
+ ws["B1"] = "Values"
416
+
417
+ row_idx = 2
418
+
419
+ for fld in tbl.fields2: # skip required fields by default
420
+ if fld.lower() in ignore_fields:
421
+ continue
422
+
423
+ #get field alias if available
424
+ if fld in tbl.field_dict and 'aliasName' in tbl.field_dict[fld] and tbl.field_dict[fld]['aliasName']:
425
+ alias = tbl.field_dict[fld]['aliasName']
426
+ else:
427
+ alias = fld
428
+ # Get unique values for the field
429
+ vals = tbl.get_field_value_set(fld)
430
+
431
+ # Convert to sorted, comma-separated string
432
+ # Make sure everything is a string for consistency
433
+ vals_str = ", ".join(sorted(str(v) for v in vals))
434
+
435
+ # Write to Excel
436
+ ws.cell(row=row_idx, column=1, value=alias) # Column A: field alias/name
437
+ try:
438
+ ws.cell(row=row_idx, column=2, value=vals_str) # Column B: CSV of unique values
439
+ except Exception as e:
440
+ ws.cell(row=row_idx, column=2, value="VALUES ERROR") # Column B: error msg
441
+ print(f"Error writing field '{alias}' values to Excel: {e}")
442
+ row_idx += 1
443
+
444
+ # Save workbook
445
+ wb.save(out_xlsx)
446
+ print(f"\nExcel file written to: {out_xlsx}")
447
+
448
+
449
+ def compare_schema(fc1, fc2):
450
+ """compare the schemas of two tables. Return an array of results.
451
+ :param fc1 {String}:
452
+ Path or reference to feature class or table.
453
+ :param fc2 {String}:
454
+ Path or reference to feature class or table.
455
+ :return array of results (field not found, field same, etc)
456
+ """
457
+ result_arr= []
458
+ fcobj1 = TableObj(fc1)
459
+ fcobj2 = TableObj(fc2)
460
+ field_dict1 = fcobj1.field_dict
461
+ field_dict2 = fcobj2.field_dict
462
+ for ifield in sorted(list(set(field_dict1.keys()+field_dict2.keys()))):
463
+ # check name for missing fields first
464
+ if not (field_dict1.has_key(ifield)):
465
+ the_result = " {0} not found in {1}".format(ifield, fc1)
466
+ output_msg(the_result)
467
+ result_arr.append(the_result)
468
+ elif not (field_dict2.has_key(ifield)):
469
+ the_result = " {0} not found in {1}".format(ifield, fc2)
470
+ output_msg(the_result)
471
+ result_arr.append(the_result)
472
+ else:
473
+ # string comparison of name, type and length
474
+ if field_dict1[ifield] == field_dict2[ifield]:
475
+ the_result = " {0} field same in both".format(ifield)
476
+ output_msg(the_result)
477
+ result_arr.append(the_result)
478
+ else:
479
+ the_result = "Field Mismatch {}: ({}, {}) ".format(
480
+ ifield, fc1, fc2)
481
+ for k in field_dict1[ifield]:
482
+ if field_dict1[ifield][k] != field_dict2[ifield][k]:
483
+ the_result += '{}: ({}, {}) '.format(k, field_dict1[ifield][k], field_dict2[ifield][k])
484
+ output_msg(the_result)
485
+ result_arr.append(the_result)
486
+
487
+ return result_arr
488
+
489
+
490
+ def import_schema_to_fc(csv_file, fc_name):
491
+ """convert csv schema from report_fields_to_csv_schema
492
+ to a featureclass"""
493
+ # assume headers as per output from export_schema_to_csv
494
+ # get fc type from csv file line 1
495
+ # load the csv file from line 2 for field info
496
+ # get field list
497
+ # ignore OBJECTID, SHAPE
498
+ fields_to_ignore =["OBJECTID", "GLOBALID","FID", "SHAPE", "SHAPE_AREA", "SHAPE.AREA", "SHAPE.STAREA()", "SHAPE_LENGTH", "SHAPE.LEN", "SHAPE.STLENGTH()"]
499
+ # create fc from fc_name
500
+ # add fields from field list
501
+ # arcpy.AddField_management(in_table, field_name, field_type, {field_precision}, {field_scale}, {field_length}, {field_alias}, {field_is_nullable}, {field_is_required}, {field_domain})
502
+ return None
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: arc-utils
3
+ Version: 1.2.1
4
+ Summary: Python utilities for use with Esri ArcGIS Pro software
5
+ Author-email: Grant Herbert <gdherbert@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/gdherbert/arc_utils
8
+ Project-URL: Download, https://github.com/gdherbert/arc_utils/tree/1.2
9
+ Keywords: esri,arcpy,arcgis
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Operating System :: Microsoft :: Windows
15
+ Requires-Python: >=3.11
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE.txt
18
+ Dynamic: license-file
19
+
20
+ # README #
21
+
22
+
23
+ ### What is this repository for? ###
24
+ * A collection of (mostly) arcpy related utilities for use within ArcGIS Pro (Python 3).
25
+ Shortcuts for accessing elements of a map document, performing various data discovery tasks on a table or geodatabase
26
+
27
+ ### How do I get set up? ###
28
+
29
+ * To install the utilities:
30
+ * ``pip install arc_utils``
31
+ * Clone the repo to your machine and add the path to the python install https://docs.python.org/2/tutorial/modules.html#the-module-search-path
32
+ * Optionally, you can copy the folder to the site-packages in the ESRI python install directory, or add a .pth file to the site-packages folder pointing to the clone directory.
33
+
34
+ To use the utilities:
35
+
36
+ * in the python window in ArcGIS Pro
37
+
38
+ ``import arc_utils as au``
39
+
40
+ * typing ``au.`` will now reveal the modules and functions available to you if you have autocomplete on
41
+
42
+ * Depends on arcpy (only available with Esri license)
43
+
44
+ ### Example usage ###
45
+
46
+ ``import arc_utils as au``
47
+
48
+ ``tbl = au.table.TableObj(path to featureclass)``
49
+
50
+ # list all fields
51
+
52
+ ``print(tbl.fields)``
53
+
54
+ ``pro_doc = au.aprx.AprxObj(“CURRENT”)``
55
+
56
+ ``pro_doc.maps`` >> [<arcpy._mp.Map object at 0x000001A4AECBF108>, <arcpy._mp.Map object at 0x000001A4AEBB6608>]
57
+
58
+ ``pro_doc.maps[0].name`` >> ‘Map’
59
+
60
+ ### Contribution guidelines ###
61
+
62
+ Contributions welcomed, this is a starting point for various utilities that I think could be useful within ArcGIS Pro.
@@ -0,0 +1,16 @@
1
+ arc_utils/__init__.py,sha256=NqPSgtw3GDpN5dltKc5Dl_x4sirGg4cDHdkd9e1L1ZY,239
2
+ arc_utils/aprx.py,sha256=VahUkYu-X4OR7vdncBbNSY_V96Ouu-txeqCHCF5uJ1k,2043
3
+ arc_utils/gdb.py,sha256=Cy8RmCSyRYaCwHItv477-ECRdKLlSuUA2aA5BkgBhxA,7721
4
+ arc_utils/output.py,sha256=0zq2hrnYQXiE5EABsiG-HTkaPpT4K-r89Mve4s2ctfI,2533
5
+ arc_utils/table.py,sha256=lB8o-hl23gTTLCp0tSbnFleNWR1dflZHQwKo2dBuvlw,20631
6
+ arc_utils-1.2.1.dist-info/licenses/LICENSE.txt,sha256=utsApqHdbH4b8YLRL99aOiW7p1_QXOs1WN1oagYbzVw,1089
7
+ examples/extract-featureclass-details.py,sha256=SFvudfiqR3L8POFqDLgUWr52c5yGn2gy2ipx62Vy6FE,4199
8
+ tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ tests/conftest.py,sha256=2rsbUMkHvXwFF9mKvQqNTAxcTPxPUpS2krKyXHUVYd0,3347
10
+ tests/test_aprx.py,sha256=Or02a0xFdSMiMFjtxjohthKw5kFKw6Qb7zjadVfjpok,774
11
+ tests/test_gdb.py,sha256=N5sAJxvIxbLP0qU6_orcBQG_lsWOp7fzqcdYJhRBObE,541
12
+ tests/test_table.py,sha256=HPPknGOaWB0JJ_rAPshzeW_f7WfjN0vwwCcfwGBgok0,2650
13
+ arc_utils-1.2.1.dist-info/METADATA,sha256=t9iTDfaR8rLUVItfuN13WgPxPO0zOAR6vA1KnYzxTSA,2161
14
+ arc_utils-1.2.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
15
+ arc_utils-1.2.1.dist-info/top_level.txt,sha256=rw_nJqzaer4UzqCOdnxEy6oZ5UqmLF0sI_u05cLF8zc,25
16
+ arc_utils-1.2.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Grant Herbert
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ arc_utils
2
+ examples
3
+ tests
@@ -0,0 +1,124 @@
1
+ import arc_utils as au
2
+ import arcpy
3
+ import os
4
+
5
+
6
+ def write_field_info(fc):
7
+ desc = arcpy.Describe(fc)
8
+ root_path = desc.path
9
+ fc_name = desc.name
10
+ print('getting field info from %s' % fc_name)
11
+ if fc_name.find(".") != -1:
12
+ fc_name = fc_name.split(".")[0]
13
+ a = open(os.path.join(root_path, fc_name + ".tsv"), "w")
14
+ a.write(au.table.get_field_info_as_text(fc)) # use default tab
15
+ a.flush()
16
+ a.close()
17
+
18
+
19
+ def write_field_values(fc):
20
+ import csv
21
+ desc = arcpy.Describe(fc)
22
+ root_path = desc.path
23
+ print(root_path)
24
+ fc_name = desc.name
25
+ print('getting field values from %s' % fc_name)
26
+ if fc_name.find(".") != -1:
27
+ fc_name = fc_name.split(".")[0]
28
+ field_value_csv = os.path.join(root_path, fc_name + "_field_values.csv")
29
+ with open(field_value_csv, 'a+') as a:
30
+ csvw = csv.writer(a, lineterminator='\n')
31
+ fco = au.table.TableObj(fc)
32
+ for field in fco.fields2:
33
+ print('writing field values for %s' % field)
34
+ values = [field]
35
+ values.extend(fco.get_field_value_set(field))
36
+ csvw.writerow(values)
37
+
38
+
39
+ def get_featureclass_info(workspace):
40
+ ws = arcpy.env.workspace
41
+ arcpy.env.workspace = workspace
42
+ desc = arcpy.Describe(workspace)
43
+ root_path = workspace
44
+ if not desc.dataType == 'Folder':
45
+ root_path = desc.catalogPath # path
46
+ for fc in arcpy.ListFeatureClasses():
47
+ fc_path = os.path.join(arcpy.env.workspace, fc)
48
+ # write field info
49
+ write_field_info(fc_path)
50
+ # write field values
51
+ write_field_values(fc_path)
52
+ arcpy.env.workspace = ws
53
+
54
+
55
+ def export_field_sets(layr_list, out_xlsx, ignore_fields=[], use_lyr_alias=True):
56
+ """ Export unique field values for each layer in a list to an Excel file with one sheet per layer
57
+ :param layr_list: list of layer objects or paths to layers
58
+ :param out_xlsx: path to output Excel file
59
+ :param ignore_fields: list of lowercase fields to ignore (e.g. ['objectid', 'shape'])
60
+ :param use_lyr_alias: if True, use layer alias for sheet name;"""
61
+ import os
62
+ from openpyxl import Workbook
63
+
64
+ ignore_fields = [v.lower() for v in ignore_fields]
65
+
66
+ # Create workbook
67
+ wb = Workbook()
68
+ # Remove the default sheet that openpyxl creates
69
+ default_sheet = wb.active
70
+ wb.remove(default_sheet)
71
+
72
+ # Loop through layers and create a sheet per layer
73
+
74
+ for lyr in layr_list:
75
+ tbl = au.table.TableObj(lyr)
76
+
77
+ print(f'Processing name: {tbl.name}, alias: {str(lyr)}')
78
+
79
+ # Excel sheet name must be <= 31 chars and cannot contain: : \ / ? * [ ]
80
+ if use_lyr_alias:
81
+ raw_name = str(lyr)
82
+ else:
83
+ raw_name = str(tbl.name) if hasattr(tbl, "name") else str(lyr)
84
+ safe_name = "".join(ch for ch in raw_name if ch not in r':\/?*[]')
85
+ safe_name = safe_name[:31] if len(safe_name) > 31 else safe_name
86
+ if not safe_name:
87
+ safe_name = "Sheet"
88
+
89
+ ws = wb.create_sheet(title=safe_name)
90
+
91
+ # Header row
92
+ ws["A1"] = "Field"
93
+ ws["B1"] = "Values"
94
+
95
+ row_idx = 2
96
+
97
+ for fld in tbl.fields2:
98
+ if fld.lower() in ignore_fields:
99
+ continue
100
+
101
+ #get field alias if available
102
+ if fld in tbl.field_dict and 'aliasName' in tbl.field_dict[fld] and tbl.field_dict[fld]['aliasName']:
103
+ alias = tbl.field_dict[fld]['aliasName']
104
+ else:
105
+ alias = fld
106
+ # Get unique values for the field
107
+ vals = tbl.get_field_value_set(fld)
108
+
109
+ # Convert to sorted, comma-separated string
110
+ # Make sure everything is a string for consistency
111
+ vals_str = ", ".join(sorted(str(v) for v in vals))
112
+
113
+ # Write to Excel
114
+ ws.cell(row=row_idx, column=1, value=alias) # Column A: field alias/name
115
+ try:
116
+ ws.cell(row=row_idx, column=2, value=vals_str) # Column B: CSV of unique values
117
+ except Exception as e:
118
+ ws.cell(row=row_idx, column=2, value="VALUES ERROR") # Column B: error msg
119
+ print(f"Error writing field '{alias}' values to Excel: {e}")
120
+ row_idx += 1
121
+
122
+ # Save workbook
123
+ wb.save(out_xlsx)
124
+ print(f"\nExcel file written to: {out_xlsx}")
tests/__init__.py ADDED
File without changes
tests/conftest.py ADDED
@@ -0,0 +1,76 @@
1
+ """module wide test fixtures"""
2
+ import pytest
3
+ import arcpy
4
+ import os
5
+ import random
6
+ import collections
7
+
8
+ testfolder = 'tests'
9
+
10
+ @pytest.fixture(scope='module')
11
+ def testgdb(pytestconfig):
12
+ gdb_name = "arc_utils_test_tmp.gdb"
13
+ return str(pytestconfig.rootdir.join(testfolder).join(gdb_name))
14
+
15
+ @pytest.fixture(scope='module')
16
+ def testaprx(pytestconfig):
17
+ aprx_name = "arc_utils_test_pro.aprx"
18
+ return str(pytestconfig.rootdir.join(testfolder).join(aprx_name))
19
+
20
+ @pytest.fixture(scope='module')
21
+ def testdatabase(request, testgdb):
22
+ # create a testing gdb
23
+ print("Creating test geodatabase")
24
+ testgdbpath = testgdb # os.path.join(arcpy.env.scratchFolder, "arc_utils_test.gdb")
25
+ if arcpy.Exists(testgdbpath):
26
+ print("Deleting " + testgdbpath)
27
+ arcpy.Delete_management(testgdbpath)
28
+ arcpy.CreateFileGDB_management(os.path.dirname(testgdbpath), os.path.basename(testgdbpath))
29
+ # add a domain for each field in testfc
30
+ ftext_dom_name = "ftext_coded"
31
+ fint_dom_name = "fint_range"
32
+ arcpy.CreateDomain_management(in_workspace=testgdbpath,
33
+ domain_name=ftext_dom_name,
34
+ domain_description="text domain",
35
+ field_type="TEXT",
36
+ domain_type="CODED")
37
+ arcpy.CreateDomain_management(in_workspace=testgdbpath,
38
+ domain_name=fint_dom_name,
39
+ field_type="SHORT",
40
+ domain_type="RANGE")
41
+ ftextDict = {"val1": "val1", "val2": "val2", "val3": "val3"}
42
+ for code in ftextDict:
43
+ arcpy.AddCodedValueToDomain_management(testgdbpath, ftext_dom_name, code, ftextDict[code])
44
+ arcpy.SetValueForRangeDomain_management(testgdbpath, fint_dom_name, 1, 12)
45
+ print("Creating test featureclasses")
46
+ sr = arcpy.SpatialReference(4326)
47
+ fc_names = ['test_fc', 'test_fc2']
48
+ fc_paths = []
49
+ fc_fields = (('ftext', 'TEXT', None, None, 20, '', 'NULLABLE', 'NON_REQUIRED', ftext_dom_name),
50
+ ('fint', 'SHORT', 0, 0, 0, '', 'NULLABLE', 'NON_REQUIRED', fint_dom_name))
51
+
52
+ for name in fc_names:
53
+ arcpy.CreateFeatureclass_management(testgdbpath, name, "POINT", spatial_reference=sr)
54
+ fc = os.path.join(testgdbpath, name)
55
+ fc_paths.append(fc)
56
+ for fc_field in fc_fields:
57
+ arcpy.AddField_management(fc, *fc_field)
58
+ print("loading data")
59
+ records = (("val1", None),("val1", 4), ("val1", 4), ("val2", 7), ("val02", 7), ("val1", 10), ("val2", 5), ("val1", 10), ("val2", 5), ("val1", None), (None, 5))
60
+ with arcpy.da.InsertCursor(fc, ["ftext", "fint", "SHAPE@XY"]) as cursor:
61
+ for key, val in records:
62
+ lon = -122.3 + float(random.randint(-9, 9)) / 100
63
+ lat = 47.6 + float(random.randint(-9, 9)) / 100
64
+ cursor.insertRow([key, val, (lon, lat)])
65
+
66
+ result = collections.namedtuple('testdata', 'gdb, test_fc1, test_fc2')
67
+ result.gdb = testgdbpath
68
+ result.fc1 = fc_paths[0]
69
+ result.fc2 = fc_paths[1]
70
+
71
+ def testdata_teardown():
72
+ print('Deleting test geodatabase')
73
+ arcpy.Delete_management(testgdbpath)
74
+
75
+ request.addfinalizer(testdata_teardown)
76
+ return result
tests/test_aprx.py ADDED
@@ -0,0 +1,23 @@
1
+ import sys
2
+ if sys.version_info.major == 3:
3
+ from arc_utils import aprx
4
+
5
+ def test_aprxobj_properties(testaprx):
6
+ aprxobj = aprx.AprxObj(testaprx)
7
+ assert aprxobj.path == testaprx
8
+ assert aprxobj.maps[0].name == 'Map'
9
+ assert aprxobj.layouts[0].name == 'MyLayout'
10
+
11
+
12
+ def test_aprxobj_layer_names(testaprx):
13
+ aprxobj = aprx.AprxObj(testaprx)
14
+ assert aprxobj.get_layer_names_as_array(aprxobj.maps[0]) == ['test_fc', 'Topographic']
15
+
16
+
17
+ def test_aprxobj_layer_obj(testaprx):
18
+ aprxobj = aprx.AprxObj(testaprx)
19
+ map_layer_obj = aprxobj.get_layer_obj_as_array(aprxobj.maps[0])
20
+ assert isinstance(map_layer_obj, list)
21
+ assert map_layer_obj[0].name == 'test_fc'
22
+
23
+
tests/test_gdb.py ADDED
@@ -0,0 +1,14 @@
1
+ from arc_utils import gdb
2
+
3
+ def test_gdb_properties(testdatabase):
4
+ gdbpath = testdatabase.gdb
5
+ gdbobj = gdb.GDBObj(gdbpath)
6
+ assert gdbobj.path == gdbpath
7
+ assert isinstance(gdbobj.feature_classes, list)
8
+ assert isinstance(gdbobj.domain_names, list)
9
+
10
+ def test_gdb_content(testdatabase):
11
+ gdbpath = testdatabase.gdb
12
+ gdbobj = gdb.GDBObj(gdbpath)
13
+ assert sorted(gdbobj.get_feature_class_names()) == ['test_fc', 'test_fc2']
14
+ assert sorted(gdbobj.get_all_domain_names()) == ['fint_range', 'ftext_coded']
tests/test_table.py ADDED
@@ -0,0 +1,69 @@
1
+ from arc_utils import table
2
+ import collections
3
+ import os
4
+
5
+ def test_tableobj_properties(testdatabase):
6
+ # test table object properties
7
+ testdata = testdatabase
8
+ tbl = table.TableObj(testdata.fc1)
9
+ assert tbl.name == 'test_fc'
10
+ assert tbl.path == testdata.fc1
11
+ assert tbl.type == u'Point'
12
+ assert tbl.fields == [u'OBJECTID', u'Shape', u'ftext', u'fint']
13
+ assert tbl.fields2 == [u'ftext', u'fint']
14
+ assert tbl.field_dict['Shape']['type'] == u'Geometry'
15
+ assert tbl.field_dict['ftext']['aliasName']== u'ftext'
16
+ assert tbl.field_dict['OBJECTID']['required']== True
17
+ assert tbl.field_dict['fint']['type']== u'Integer'
18
+
19
+
20
+ def test_tableobj_single_text_field_methods(testdatabase):
21
+ # test table object methods
22
+ tbl = table.TableObj(testdatabase.fc1)
23
+ ftext_set = tbl.get_field_value_set('ftext')
24
+ assert isinstance(ftext_set, set)
25
+ assert sorted(ftext_set) == [u'NULL', 'val02', 'val1', 'val2']
26
+ assert tbl.get_max_field_value('ftext') == 'val2'
27
+ assert tbl.get_max_field_value('ftext', True) == 'val02'
28
+ assert tbl.get_max_field_value_length('ftext') == 5
29
+
30
+
31
+ def test_tableobj_single_integer_field_methods(testdatabase):
32
+ # test table object methods
33
+ tbl = table.TableObj(testdatabase.fc1)
34
+ fint_set = tbl.get_field_value_set('fint')
35
+ assert isinstance(fint_set, set)
36
+ assert tbl.get_field_value_set('fint') == set([4, 5, 7, 10, u'NULL'])
37
+ assert tbl.get_max_field_value('fint') == 10
38
+ assert tbl.get_max_field_value_length('fint') == 2
39
+
40
+
41
+ def test_tableobj_multi_field_methods(testdatabase):
42
+ # test table object methods
43
+ tbl = table.TableObj(testdatabase.fc1)
44
+ multi_field = tbl.get_multiple_field_value_set(['ftext', 'fint'])
45
+ assert isinstance(multi_field, set)
46
+ assert sorted(multi_field) == [u'NULL:5', u'val02:7', u'val1:10', u'val1:4', u'val1:NULL', u'val2:5', u'val2:7']
47
+
48
+
49
+
50
+ def test_tableobj_compare_methods(testdatabase):
51
+ # test table object compare methods
52
+ tbl = table.TableObj(testdatabase.fc1)
53
+ result = tbl.compare_field_values_to_domain('fint', testdatabase.gdb, "fint_range")
54
+ result.match.sort()
55
+ assert result.match == [4, 5, 7, 10]
56
+ assert result.unmatched == []
57
+
58
+ result = tbl.compare_field_values_to_domain('ftext', testdatabase.gdb, "ftext_coded")
59
+ result.match.sort()
60
+ result.unmatched.sort()
61
+ assert result.match == ['val1', 'val2']
62
+ assert result.unmatched == ['NULL', 'val02']
63
+
64
+
65
+ def test_table_fc_methods(testdatabase):
66
+ # test non-object table methods
67
+ #table.compare_schema(testdata.fc, testdata.fc2)
68
+ assert True
69
+