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 +10 -0
- arc_utils/aprx.py +64 -0
- arc_utils/gdb.py +196 -0
- arc_utils/output.py +80 -0
- arc_utils/table.py +502 -0
- arc_utils-1.2.1.dist-info/METADATA +62 -0
- arc_utils-1.2.1.dist-info/RECORD +16 -0
- arc_utils-1.2.1.dist-info/WHEEL +5 -0
- arc_utils-1.2.1.dist-info/licenses/LICENSE.txt +21 -0
- arc_utils-1.2.1.dist-info/top_level.txt +3 -0
- examples/extract-featureclass-details.py +124 -0
- tests/__init__.py +0 -0
- tests/conftest.py +76 -0
- tests/test_aprx.py +23 -0
- tests/test_gdb.py +14 -0
- tests/test_table.py +69 -0
arc_utils/__init__.py
ADDED
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,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,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
|
+
|