matplotlib-map-utils 2.0.2__py3-none-any.whl → 3.0.0__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.
- matplotlib_map_utils/__init__.py +16 -1
- matplotlib_map_utils/core/__init__.py +5 -1
- matplotlib_map_utils/core/inset_map.py +952 -0
- matplotlib_map_utils/core/north_arrow.py +0 -4
- matplotlib_map_utils/core/scale_bar.py +0 -6
- matplotlib_map_utils/defaults/inset_map.py +67 -0
- matplotlib_map_utils/utils/__init__.py +3 -0
- matplotlib_map_utils/utils/usa.json +1038 -0
- matplotlib_map_utils/utils/usa.py +336 -0
- matplotlib_map_utils/validation/functions.py +51 -15
- matplotlib_map_utils/validation/inset_map.py +88 -0
- matplotlib_map_utils/validation/north_arrow.py +1 -1
- matplotlib_map_utils/validation/scale_bar.py +3 -5
- {matplotlib_map_utils-2.0.2.dist-info → matplotlib_map_utils-3.0.0.dist-info}/METADATA +163 -19
- matplotlib_map_utils-3.0.0.dist-info/RECORD +24 -0
- {matplotlib_map_utils-2.0.2.dist-info → matplotlib_map_utils-3.0.0.dist-info}/WHEEL +1 -1
- matplotlib_map_utils-2.0.2.dist-info/RECORD +0 -18
- {matplotlib_map_utils-2.0.2.dist-info → matplotlib_map_utils-3.0.0.dist-info/licenses}/LICENSE +0 -0
- {matplotlib_map_utils-2.0.2.dist-info → matplotlib_map_utils-3.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,336 @@
|
|
1
|
+
import re
|
2
|
+
import json
|
3
|
+
import warnings
|
4
|
+
from importlib import resources
|
5
|
+
from typing import List, Literal, Union
|
6
|
+
|
7
|
+
# Literal lists, for intellisense
|
8
|
+
regions = Literal["Midwest", "Northeast", "South", "West",
|
9
|
+
"Inhabited Territory", "Uninhabited Territory", "Sovereign State"]
|
10
|
+
|
11
|
+
divisions = Literal["East North Central", "East South Central", "Mid-Atlantic", "Mountain",
|
12
|
+
"New England", "Pacific", "South Atlantic", "West North Central", "West South Central",
|
13
|
+
"Commonwealth", "Compact of Free Association", "Incorporated and Unorganized",
|
14
|
+
"Unincorporated and Unorganized", "Unincorporated and Organized"]
|
15
|
+
|
16
|
+
ombs = Literal["Region I", "Region II", "Region III", "Region IV", "Region IX", "Region V",
|
17
|
+
"Region VI", "Region VII", "Region VIII", "Region X",
|
18
|
+
"Inhabited Territory", "Uninhabited Territory", "Sovereign State"]
|
19
|
+
|
20
|
+
beas = Literal["Far West", "Great Lakes", "Mideast", "New England", "Plains",
|
21
|
+
"Rocky Mountain", "Southeast", "Southwest",
|
22
|
+
"Inhabited Territory", "Uninhabited Territory", "Sovereign State"]
|
23
|
+
|
24
|
+
returns = Literal["fips","name","abbr","object","dict"]
|
25
|
+
|
26
|
+
class USA:
|
27
|
+
# No arguments need to pass on initialization really
|
28
|
+
def __init__(self):
|
29
|
+
self._jurisdictions = self._load_json()
|
30
|
+
|
31
|
+
# This is just for loading the JSON
|
32
|
+
def _load_json(self):
|
33
|
+
with resources.files("matplotlib_map_utils.utils").joinpath("usa.json").open("r") as f:
|
34
|
+
usa_json = json.load(f)
|
35
|
+
return usa_json
|
36
|
+
|
37
|
+
# Getter for all jurisdictions, VALID OR NOT
|
38
|
+
@property
|
39
|
+
def _all(self):
|
40
|
+
return self._jurisdictions
|
41
|
+
|
42
|
+
# Getter for all valid jurisdictions
|
43
|
+
@property
|
44
|
+
def jurisdictions(self):
|
45
|
+
return self.filter_valid(True, self._all, "object")
|
46
|
+
|
47
|
+
# Getter for all valid states
|
48
|
+
@property
|
49
|
+
def states(self):
|
50
|
+
return self.filter_state(True, self.jurisdictions, "object")
|
51
|
+
|
52
|
+
# Getter for all valid territories
|
53
|
+
@property
|
54
|
+
def territories(self):
|
55
|
+
return self.filter_territory(True, self.jurisdictions, "object")
|
56
|
+
|
57
|
+
# Getters to generate distinct values for Region, Division, OMB, and BEA
|
58
|
+
# which are useful if you can't recall which options are valid
|
59
|
+
# First, the function that will get the distinct values
|
60
|
+
def _distinct_options(self, key):
|
61
|
+
# First getting all the available options from the list
|
62
|
+
options = [j[key] for j in self.jurisdictions if j[key] is not None]
|
63
|
+
# Creating the distinct set
|
64
|
+
options_set = set(options)
|
65
|
+
# Returning the set (but as a list)
|
66
|
+
# this will also be alphabetically sorted
|
67
|
+
options = list(options_set)
|
68
|
+
options.sort()
|
69
|
+
return options
|
70
|
+
|
71
|
+
# The getters are now just calls to the properties
|
72
|
+
@property
|
73
|
+
def regions(self):
|
74
|
+
return self._distinct_options("region")
|
75
|
+
|
76
|
+
@property
|
77
|
+
def divisions(self):
|
78
|
+
return self._distinct_options("division")
|
79
|
+
|
80
|
+
@property
|
81
|
+
def omb(self):
|
82
|
+
return self._distinct_options("omb")
|
83
|
+
|
84
|
+
@property
|
85
|
+
def bea(self):
|
86
|
+
return self._distinct_options("bea")
|
87
|
+
|
88
|
+
# Main filter function
|
89
|
+
# Each filter step will follow the same process
|
90
|
+
## Check that there is a non-None filter
|
91
|
+
## Normalize the input to be in a list (if not already)
|
92
|
+
## Perform the filter step
|
93
|
+
# Each step is also available as its own independent function, as needed
|
94
|
+
def filter(self, valid: bool | None=True,
|
95
|
+
fips: str | int | None=None,
|
96
|
+
name: str | None=None,
|
97
|
+
abbr: str | None=None,
|
98
|
+
state: bool | None=None,
|
99
|
+
contiguous: bool | None=None,
|
100
|
+
territory: bool | None=None,
|
101
|
+
region: Union[regions, List[regions]]=None,
|
102
|
+
division: Union[divisions, List[divisions]]=None,
|
103
|
+
omb: Union[ombs, List[ombs]]=None,
|
104
|
+
bea: Union[beas, List[beas]]=None,
|
105
|
+
to_return: Union[returns, List[returns]]="fips"):
|
106
|
+
|
107
|
+
# Getting a copy of our jurisdictions, which will be filtered each time
|
108
|
+
filter_juris = self.jurisdictions.copy()
|
109
|
+
|
110
|
+
# Starting with an initial valid filtering
|
111
|
+
# Which will drop invalid FIPS codes 03, 07, 14, 43, and 52
|
112
|
+
if (valid is not None) and (len(filter_juris) > 0):
|
113
|
+
filter_juris = self.filter_valid(valid, filter_juris, to_return="_ignore")
|
114
|
+
|
115
|
+
# Going through each step
|
116
|
+
if (fips is not None) and (len(filter_juris) > 0):
|
117
|
+
filter_juris = self.filter_fips(fips, filter_juris, to_return="_ignore")
|
118
|
+
|
119
|
+
if (name is not None) and (len(filter_juris) > 0):
|
120
|
+
filter_juris = self.filter_name(name, filter_juris, to_return="_ignore")
|
121
|
+
|
122
|
+
if (abbr is not None) and (len(filter_juris) > 0):
|
123
|
+
filter_juris = self.filter_abbr(abbr, filter_juris, to_return="_ignore")
|
124
|
+
|
125
|
+
if (state is not None) and (len(filter_juris) > 0):
|
126
|
+
filter_juris = self.filter_state(state, filter_juris, to_return="_ignore")
|
127
|
+
|
128
|
+
if (contiguous is not None) and (len(filter_juris) > 0):
|
129
|
+
filter_juris = self.filter_contiguous(contiguous, filter_juris, to_return="_ignore")
|
130
|
+
|
131
|
+
if (territory is not None) and (len(filter_juris) > 0):
|
132
|
+
filter_juris = self.filter_territory(territory, filter_juris, to_return="_ignore")
|
133
|
+
|
134
|
+
if (region is not None) and (len(filter_juris) > 0):
|
135
|
+
filter_juris = self.filter_region(region, filter_juris, to_return="_ignore")
|
136
|
+
|
137
|
+
if (division is not None) and (len(filter_juris) > 0):
|
138
|
+
filter_juris = self.filter_division(division, filter_juris, to_return="_ignore")
|
139
|
+
|
140
|
+
if (omb is not None) and (len(filter_juris) > 0):
|
141
|
+
filter_juris = self.filter_omb(omb, filter_juris, to_return="_ignore")
|
142
|
+
|
143
|
+
if (bea is not None) and (len(filter_juris) > 0):
|
144
|
+
filter_juris = self.filter_bea(bea, filter_juris, to_return="_ignore")
|
145
|
+
|
146
|
+
# Final step is to process the input based on to_return
|
147
|
+
# and then return it!
|
148
|
+
return self._process_return(filter_juris, to_return)
|
149
|
+
|
150
|
+
# Filtering bool values (valid, state, contiguous, territory)
|
151
|
+
# Will accept either true or false
|
152
|
+
def _filter_bool(self, value, key, to_filter=None, to_return="_ignore"):
|
153
|
+
# If nothing is passed to to_filter, getting the jurisdictions list
|
154
|
+
to_filter = self.jurisdictions.copy() if to_filter is None else to_filter
|
155
|
+
if not isinstance(value, bool):
|
156
|
+
warnings.warn(f"Invalid {key} filter: {value}. Only boolean values (True/False) are considered valid, see documentation for details.")
|
157
|
+
else:
|
158
|
+
# Performing the filter
|
159
|
+
filtered = [j for j in to_filter if j[key] == value]
|
160
|
+
# And returning the values
|
161
|
+
return self._process_return(filtered, to_return)
|
162
|
+
|
163
|
+
# Shortcuts for filtering based on valid, state, contiguous, and territory
|
164
|
+
def filter_valid(self, valid: bool, to_filter=None, to_return="fips"):
|
165
|
+
return self._filter_bool(valid, "valid", to_filter, to_return)
|
166
|
+
|
167
|
+
def filter_state(self, state: bool, to_filter=None, to_return="fips"):
|
168
|
+
return self._filter_bool(state, "state", to_filter, to_return)
|
169
|
+
|
170
|
+
def filter_contiguous(self, contiguous: bool, to_filter=None, to_return="fips"):
|
171
|
+
return self._filter_bool(contiguous, "contiguous", to_filter, to_return)
|
172
|
+
|
173
|
+
def filter_territory(self, territory: bool, to_filter=None, to_return="fips"):
|
174
|
+
return self._filter_bool(territory, "territory", to_filter, to_return)
|
175
|
+
|
176
|
+
# Filtering FIPS
|
177
|
+
# Will accept an integer or a two-digit string as an input
|
178
|
+
# If a longer string is inserted, will truncate to only the first two characters
|
179
|
+
def filter_fips(self, fips: str | List[str], to_filter=None, to_return="abbr"):
|
180
|
+
# If nothing is passed to to_filter, getting the jurisdictions list
|
181
|
+
to_filter = self.jurisdictions.copy() if to_filter is None else to_filter
|
182
|
+
# Normalizing the fips value being passed
|
183
|
+
fips = self._normalize_input(fips)
|
184
|
+
# This will store the cleaned-up fips codes
|
185
|
+
fips_clean = []
|
186
|
+
for f in fips:
|
187
|
+
# If the input is an integer, convert it to a two-digit string
|
188
|
+
if isinstance(f, int):
|
189
|
+
fips_clean.append(str(f).zfill(2)[:2])
|
190
|
+
# If the input is already a string, get the first two characters
|
191
|
+
elif isinstance(f, str):
|
192
|
+
fips_clean.append(f.zfill(2)[:2])
|
193
|
+
# Otherwise, throw a *warning*
|
194
|
+
else:
|
195
|
+
warnings.warn(f"Invalid FIPS filter: {f}. Only integers and strings are considered valid, see documentation for details.")
|
196
|
+
# Now can use the clean fips to actually filter
|
197
|
+
filtered = [j for j in to_filter if j["fips"] in fips_clean]
|
198
|
+
# And returning the values
|
199
|
+
return self._process_return(filtered, to_return)
|
200
|
+
|
201
|
+
# Filtering name
|
202
|
+
# Will accept strings
|
203
|
+
# Will normalize the string first (trim, case, special characters), before checking
|
204
|
+
# Some states also have an alias available for checking against (Washington, D.C. and District of Columbia are equivalent)
|
205
|
+
def filter_name(self, name: str | List[str], to_filter=None, to_return="fips"):
|
206
|
+
# If nothing is passed to to_filter, getting the jurisdictions list
|
207
|
+
to_filter = self.jurisdictions.copy() if to_filter is None else to_filter
|
208
|
+
# Normalizing the name input being passed
|
209
|
+
name = self._normalize_input(name)
|
210
|
+
# This will store the cleaned-up name input
|
211
|
+
name_clean = []
|
212
|
+
for n in name:
|
213
|
+
# If the input is a string, clean it
|
214
|
+
if isinstance(n, str):
|
215
|
+
name_clean.append(self._normalize_string(n, case="lower"))
|
216
|
+
else:
|
217
|
+
warnings.warn(f"Invalid name filter: {n}. Only strings are considered valid, see documentation for details.")
|
218
|
+
# Now we can use the clean name to filter
|
219
|
+
# Note that we also normalize the names and aliases in our to_filter list!
|
220
|
+
filtered = [j for j in to_filter if ((self._normalize_string(j["name"], case="lower") in name_clean) or
|
221
|
+
(j["alias"] is not None and self._normalize_string(j["alias"], case="lower") in name_clean))]
|
222
|
+
# And returning the values
|
223
|
+
return self._process_return(filtered, to_return)
|
224
|
+
|
225
|
+
# Filtering abbr
|
226
|
+
# Will accept strings
|
227
|
+
# Will normalize the string first (trim, case, special characters), before checking
|
228
|
+
# If a string longer than two characters is passed, will only look at the first two characters!
|
229
|
+
def filter_abbr(self, abbr: str | List[str], to_filter=None, to_return="fips"):
|
230
|
+
# If nothing is passed to to_filter, getting the jurisdictions list
|
231
|
+
to_filter = self.jurisdictions.copy() if to_filter is None else to_filter
|
232
|
+
# Normalizing the input being passed
|
233
|
+
abbr = self._normalize_input(abbr)
|
234
|
+
# This will store the cleaned-up input
|
235
|
+
abbr_clean = []
|
236
|
+
for a in abbr:
|
237
|
+
# If the input is a string, clean it
|
238
|
+
if isinstance(a, str):
|
239
|
+
abbr_clean.append(self._normalize_string(a, case="lower"))
|
240
|
+
else:
|
241
|
+
warnings.warn(f"Invalid abbr filter: {a}. Only strings are considered valid, see documentation for details.")
|
242
|
+
# Now we can use the clean input to filter
|
243
|
+
filtered = [j for j in to_filter if (j["abbr"] is not None and self._normalize_string(j["abbr"], case="lower")[:2] in abbr_clean)]
|
244
|
+
# And returning the values
|
245
|
+
return self._process_return(filtered, to_return)
|
246
|
+
|
247
|
+
# Filtering for categorical values (region/division/omb/bea)
|
248
|
+
# Will get the list of acceptable values and compare inputs to it
|
249
|
+
# while also warning if an invalid filter is requested
|
250
|
+
def _filter_categorical(self, input, key, to_filter=None, to_return="_ignore"):
|
251
|
+
# If nothing is passed to to_filter, getting the jurisdictions list
|
252
|
+
to_filter = self.jurisdictions.copy() if to_filter is None else to_filter
|
253
|
+
# Normalizing the input being passed
|
254
|
+
input = self._normalize_input(input)
|
255
|
+
# This has the acceptable inputs we want to compare against
|
256
|
+
accepted_inputs = self._distinct_options(key)
|
257
|
+
# This will store the cleaned-up input
|
258
|
+
input_clean = []
|
259
|
+
for i in input:
|
260
|
+
# If the input is not a string, warn
|
261
|
+
if not isinstance(i, str):
|
262
|
+
warnings.warn(f"Invalid {key} filter: {i}. Only strings are considered valid, see documentation for details.")
|
263
|
+
# If the input is not in our list, warn the user
|
264
|
+
elif i not in accepted_inputs:
|
265
|
+
warnings.warn(f"Invalid {key} filter: {i}. Only the following inputs are considered valid: {accepted_inputs}.")
|
266
|
+
# Otherwise, add it to our list
|
267
|
+
else:
|
268
|
+
input_clean.append(i)
|
269
|
+
# Now we can use the clean input to filter
|
270
|
+
filtered = [j for j in to_filter if j[key] in input_clean]
|
271
|
+
# And returning the values
|
272
|
+
return self._process_return(filtered, to_return)
|
273
|
+
|
274
|
+
# Iterations for each categorical filter based on their respective inputs
|
275
|
+
def filter_region(self, region: Union[regions, List[regions]], to_filter=None, to_return="fips"):
|
276
|
+
return self._filter_categorical(region, "region", to_filter, to_return)
|
277
|
+
|
278
|
+
def filter_division(self, division: Union[divisions, List[divisions]], to_filter=None, to_return="fips"):
|
279
|
+
return self._filter_categorical(division, "division", to_filter, to_return)
|
280
|
+
|
281
|
+
def filter_omb(self, omb: Union[ombs, List[ombs]], to_filter=None, to_return="fips"):
|
282
|
+
return self._filter_categorical(omb, "omb", to_filter, to_return)
|
283
|
+
|
284
|
+
def filter_bea(self, bea: Union[beas, List[beas]], to_filter=None, to_return="fips"):
|
285
|
+
return self._filter_categorical(bea, "bea", to_filter, to_return)
|
286
|
+
|
287
|
+
# Function that processes the returning of a filtered jurisdiction
|
288
|
+
def _process_return(self, filter_juris, to_return):
|
289
|
+
# If the length is zero, warn!
|
290
|
+
if filter_juris is None or len(filter_juris) == 0:
|
291
|
+
warnings.warn(f"No matching entities found. Please refer to the documentation and double-check your filters.")
|
292
|
+
return None
|
293
|
+
if to_return is None:
|
294
|
+
to_return == "_ignore"
|
295
|
+
# Available options for to_return include fips, name, and abbr
|
296
|
+
elif to_return.lower() == "fips":
|
297
|
+
juris_return = [j["fips"] for j in filter_juris]
|
298
|
+
elif to_return.lower() == "name":
|
299
|
+
juris_return = [j["name"] for j in filter_juris]
|
300
|
+
elif to_return.lower() == "abbr":
|
301
|
+
juris_return = [j["abbr"] for j in filter_juris]
|
302
|
+
# Can also request that the entire object be returned, in which case nothing is done
|
303
|
+
# This will also happen if an invalid return object is passed
|
304
|
+
elif to_return.lower() not in ["object","dict","_ignore"]:
|
305
|
+
warnings.warn(f"Invalid to_return request: {to_return}. The entire object will be returned.")
|
306
|
+
juris_return = filter_juris.copy()
|
307
|
+
else:
|
308
|
+
juris_return = filter_juris.copy()
|
309
|
+
|
310
|
+
# Now, also processing the return request based on the length of the returned list
|
311
|
+
# If the length is zero, warn!
|
312
|
+
if len(juris_return) == 0:
|
313
|
+
warnings.warn(f"No matching entities found. Please refer to the documentation and double-check your filters.")
|
314
|
+
return None
|
315
|
+
# If only one element is returned, return the element itself, not a list
|
316
|
+
elif len(juris_return) == 1 and to_return != "_ignore":
|
317
|
+
return juris_return[0]
|
318
|
+
# Otherwise return the whole thing
|
319
|
+
else:
|
320
|
+
return juris_return
|
321
|
+
|
322
|
+
# Utility function to normalize a string that is passed to it
|
323
|
+
def _normalize_string(self, string, case="keep", nan="", spaces="_"):
|
324
|
+
string = string.strip()
|
325
|
+
if case == "lower":
|
326
|
+
string = string.lower()
|
327
|
+
string = re.sub(r"\W\S",nan,string)
|
328
|
+
string = re.sub(r"\s",spaces,string)
|
329
|
+
return string
|
330
|
+
|
331
|
+
# Utility function to convert a relevant non-list input to a list
|
332
|
+
def _normalize_input(self, input):
|
333
|
+
if not isinstance(input, (list, tuple)):
|
334
|
+
return [input]
|
335
|
+
else:
|
336
|
+
return input
|
@@ -20,7 +20,7 @@ def _validate_list(prop, val, list, none_ok=False):
|
|
20
20
|
raise ValueError(f"'{val}' is not a valid value for {prop}, please provide a value in this list: {list}")
|
21
21
|
return val
|
22
22
|
|
23
|
-
def _validate_range(prop, val, min, max, none_ok=False):
|
23
|
+
def _validate_range(prop, val, min, max=None, none_ok=False):
|
24
24
|
if none_ok==False and val is None:
|
25
25
|
raise ValueError(f"None is not a valid value for {prop}, please provide a value between {min} and {max}")
|
26
26
|
elif none_ok==True and val is None:
|
@@ -69,7 +69,7 @@ def _validate_tuple(prop, val, length, types, none_ok=False):
|
|
69
69
|
raise ValueError(f"None is not a valid value for {prop}, please provide a tuple of length {length} instead")
|
70
70
|
elif none_ok==True and val is None:
|
71
71
|
return val
|
72
|
-
elif
|
72
|
+
elif not isinstance(val, (tuple, list)):
|
73
73
|
raise ValueError(f"{val} is not a valid value for {prop}, please provide a tuple of length {length} instead")
|
74
74
|
elif len(val)!=length:
|
75
75
|
raise ValueError(f"{val} is not a valid value for {prop}, please provide a tuple of length {length} instead")
|
@@ -117,7 +117,7 @@ def _validate_crs(prop, val, rotation_dict, none_ok=False):
|
|
117
117
|
try:
|
118
118
|
val = pyproj.CRS.from_user_input(val)
|
119
119
|
except:
|
120
|
-
raise Exception(f"Invalid CRS supplied ({val}), please provide a valid CRS input
|
120
|
+
raise Exception(f"Invalid CRS supplied ({val}), please provide a valid CRS input that PyProj can use instead")
|
121
121
|
return val
|
122
122
|
|
123
123
|
# A simpler validation function for CRSs
|
@@ -128,14 +128,14 @@ def _validate_projection(prop, val, none_ok=False):
|
|
128
128
|
try:
|
129
129
|
val = pyproj.CRS.from_user_input(val)
|
130
130
|
except:
|
131
|
-
raise Exception(f"Invalid CRS supplied ({val}), please provide a valid CRS input
|
131
|
+
raise Exception(f"Invalid CRS supplied ({val}) for {prop}, please provide a valid CRS input that PyProj can use instead")
|
132
132
|
return val
|
133
133
|
|
134
|
-
#
|
134
|
+
# This is specifically to apply another validation function to the items in a list
|
135
135
|
# Ex. if we want to validate a LIST of colors instead of a single color
|
136
136
|
def _validate_iterable(prop, val, func, kwargs=None):
|
137
137
|
# Making sure we wrap everything in a list
|
138
|
-
if
|
138
|
+
if not isinstance(val, (tuple, list)):
|
139
139
|
val = [val]
|
140
140
|
# Then, we apply our validation func with optional kwargs to each item in said list, relying on it to return an error value
|
141
141
|
if kwargs is not None:
|
@@ -148,21 +148,57 @@ def _validate_iterable(prop, val, func, kwargs=None):
|
|
148
148
|
v = func(v)
|
149
149
|
return val
|
150
150
|
|
151
|
-
# This is
|
151
|
+
# This is to check for the structure of a dictionary-like object
|
152
|
+
def _validate_keys(prop, val, keys, none_ok=False):
|
153
|
+
if none_ok==False and val is None:
|
154
|
+
raise ValueError(f"None is not a valid value for {prop}, please provide a dictionary with keys {keys} instead")
|
155
|
+
elif none_ok==True and val is None:
|
156
|
+
return val
|
157
|
+
elif not isinstance(val, (dict)):
|
158
|
+
raise ValueError(f"{val} is not a valid value for {prop}, please provide a dictionary with keys {keys} instead")
|
159
|
+
else:
|
160
|
+
for k in val.keys():
|
161
|
+
if k not in keys:
|
162
|
+
raise ValueError(f"{k} is not a valid key for the items in {prop}, please provide a dictionary with keys {keys} instead")
|
163
|
+
return val
|
164
|
+
|
165
|
+
# This is to apply multiple validation functions to a value, if needed - only one needs to pass
|
152
166
|
# Ex. If an item can be a string OR a list of strings, we can use this to validate it
|
153
|
-
|
154
|
-
|
167
|
+
def _validate_or(prop, val, funcs, kwargs):
|
168
|
+
success = False
|
155
169
|
# Simply iterate through each func and kwarg
|
156
170
|
for f,k in zip(funcs,kwargs):
|
157
171
|
# We wrap the attempts in a try block to suppress the errors
|
158
172
|
try:
|
159
|
-
|
173
|
+
val = f(prop=prop, val=val, **k)
|
160
174
|
# If we pass, we can stop here and return the value
|
161
|
-
|
175
|
+
success = True
|
176
|
+
break
|
162
177
|
except:
|
163
|
-
|
164
|
-
|
165
|
-
|
178
|
+
pass
|
179
|
+
if success == False:
|
180
|
+
# If we didn't return a value and exit the loop yet, then the passed value is incorrect, as we raise an error
|
181
|
+
raise ValueError(f"{val} is not a valid value for {prop}, please check the documentation")
|
182
|
+
else:
|
183
|
+
return val
|
184
|
+
|
185
|
+
# This is the same, but ALL need to pass
|
186
|
+
def _validate_and(prop, val, funcs, kwargs):
|
187
|
+
success = True
|
188
|
+
# Simply iterate through each func and kwarg
|
189
|
+
for f,k in zip(funcs,kwargs):
|
190
|
+
# We wrap the attempts in a try block to suppress the errors
|
191
|
+
try:
|
192
|
+
val = f(prop=prop, val=val, **k)
|
193
|
+
except:
|
194
|
+
# If we fail, we can stop here and return the value
|
195
|
+
success = False
|
196
|
+
break
|
197
|
+
if success == False:
|
198
|
+
# If we didn't return a value and exit the loop yet, then the passed value is incorrect, as we raise an error
|
199
|
+
raise ValueError(f"{val} is not a valid value for {prop}, please check the documentation")
|
200
|
+
else:
|
201
|
+
return val
|
166
202
|
|
167
203
|
# This final one is used for keys that are not validated
|
168
204
|
def _skip_validation(val, none_ok=False):
|
@@ -221,7 +257,7 @@ def _validate_dict(input_dict, default_dict, functions, to_validate=None, return
|
|
221
257
|
def _validate(validate_dict, prop, val, return_val=True, kwargs={}):
|
222
258
|
fd = validate_dict[prop]
|
223
259
|
func = fd["func"]
|
224
|
-
#
|
260
|
+
# Most of our custom functions always have this dictionary key in them, so we know what form they take
|
225
261
|
if "kwargs" in fd:
|
226
262
|
val = func(prop=prop, val=val, **(fd["kwargs"] | kwargs))
|
227
263
|
# The matplotlib built-in functions DON'T have that, and only ever take the one value
|
@@ -0,0 +1,88 @@
|
|
1
|
+
############################################################
|
2
|
+
# validation/inset_map.py contains all the main objects
|
3
|
+
# for checking inputs passed to class definitions
|
4
|
+
############################################################
|
5
|
+
|
6
|
+
### IMPORTING PACKAGES ###
|
7
|
+
|
8
|
+
# Geo packages
|
9
|
+
import matplotlib.axes
|
10
|
+
import pyproj
|
11
|
+
# Graphical packages
|
12
|
+
import matplotlib
|
13
|
+
# matplotlib's useful validation functions
|
14
|
+
import matplotlib.rcsetup
|
15
|
+
# The types we use in this script
|
16
|
+
from typing import TypedDict, Literal
|
17
|
+
# Finally, the validation functions
|
18
|
+
from . import functions as vf
|
19
|
+
|
20
|
+
### ALL ###
|
21
|
+
# This code tells other packages what to import if not explicitly stated
|
22
|
+
__all__ = [
|
23
|
+
"_TYPE_INSET", "_VALIDATE_INSET",
|
24
|
+
"_TYPE_EXTENT", "_VALIDATE_EXTENT",
|
25
|
+
"_TYPE_DETAIL", "_VALIDATE_DETAIL",
|
26
|
+
]
|
27
|
+
|
28
|
+
### TYPE HINTS ###
|
29
|
+
# This section of the code is for defining structured dictionaries and lists
|
30
|
+
# for the inputs necessary for object creation we've created (such as the style dictionaries)
|
31
|
+
# so that intellisense can help with autocompletion
|
32
|
+
|
33
|
+
class _TYPE_INSET(TypedDict, total=False):
|
34
|
+
size: int | float | tuple[int | float, int | float] | list[int | float, int | float] # each int or float should be between 0 and inf
|
35
|
+
pad: int | float | tuple[int | float, int | float] | list[int | float, int | float] # each int or float should be between 0 and inf
|
36
|
+
coords: tuple[int | float, int | float] | list[int | float, int | float] # each int or float should be between -inf and inf
|
37
|
+
|
38
|
+
class _TYPE_EXTENT(TypedDict, total=False):
|
39
|
+
pax: matplotlib.axes.Axes # any Matplotlib Axes
|
40
|
+
bax: matplotlib.axes.Axes # any Matplotlib Axes
|
41
|
+
pcrs: str | int | pyproj.CRS # should be a valid cartopy or pyproj crs, or a string or int that can be converted to that
|
42
|
+
bcrs: str | int | pyproj.CRS # should be a valid cartopy or pyproj crs, or a string or int that can be converted to that
|
43
|
+
straighten: bool # either true or false
|
44
|
+
pad: float | int # any positive float or integer
|
45
|
+
plot: bool # either true or false
|
46
|
+
to_return: Literal["shape","patch","fig","ax"] | None # any item in the list, or None if nothing should be returned
|
47
|
+
facecolor: str # a color to use for the face of the box
|
48
|
+
linecolor: str # a color to use for the edge of the box
|
49
|
+
alpha: float | int # any positive float or integer
|
50
|
+
linewidth: float | int # any positive float or integer
|
51
|
+
|
52
|
+
class _TYPE_DETAIL(TypedDict, total=False):
|
53
|
+
to_return: Literal["connectors", "lines"] | None # any item in the list, or None if nothing should be returned
|
54
|
+
connector_color: str # a color to use for the face of the box
|
55
|
+
connector_width: float | int # any positive float or integer
|
56
|
+
|
57
|
+
### VALIDITY DICTS ###
|
58
|
+
# These compile the functions in validation/functions, as well as matplotlib's built-in validity functions
|
59
|
+
# into dictionaries that can be used to validate all the inputs to a dictionary at once
|
60
|
+
|
61
|
+
_VALIDATE_INSET = {
|
62
|
+
"location":{"func":vf._validate_list, "kwargs":{"list":["upper right", "upper left", "lower left", "lower right", "center left", "center right", "lower center", "upper center", "center"]}},
|
63
|
+
"size":{"func":vf._validate_or, "kwargs":{"funcs":[vf._validate_range, vf._validate_and], "kwargs":[{"min":0, "none_ok":True}, {"funcs":[vf._validate_tuple, vf._validate_iterable], "kwargs":[{"length":2, "types":[float, int]}, {"func":vf._validate_range, "kwargs":{"min":0}}]}]}}, # between 0 and inf, or a two-tuple of (x,y) size, each between 0 and inf
|
64
|
+
"pad":{"func":vf._validate_or, "kwargs":{"funcs":[vf._validate_range, vf._validate_and], "kwargs":[{"min":0, "none_ok":True}, {"funcs":[vf._validate_tuple, vf._validate_iterable], "kwargs":[{"length":2, "types":[float, int]}, {"func":vf._validate_range, "kwargs":{"min":0}}]}]}}, # between 0 and inf, or a two-tuple of (x,y) size, each between 0 and inf
|
65
|
+
"coords":{"func":vf._validate_tuple, "kwargs":{"length":2, "types":[float, int], "none_ok":True}}, # a two-tuple of coordinates where you want to place the inset map
|
66
|
+
"to_plot":{"func":vf._validate_iterable, "kwargs":{"func":vf._validate_keys, "kwargs":{"keys":["data","kwargs"], "none_ok":True}}}, # a list of dictionaries, where each contains "data" and "kwargs" keys
|
67
|
+
}
|
68
|
+
|
69
|
+
_VALIDATE_EXTENT = {
|
70
|
+
"pax":{"func":vf._validate_type, "kwargs":{"match":matplotlib.axes.Axes}}, # any Matplotlib Axes
|
71
|
+
"bax":{"func":vf._validate_type, "kwargs":{"match":matplotlib.axes.Axes}}, # any Matplotlib Axes
|
72
|
+
"pcrs":{"func":vf._validate_projection, "kwargs":{"none_ok":False}}, # any valid projection input for PyProj
|
73
|
+
"bcrs":{"func":vf._validate_projection, "kwargs":{"none_ok":False}}, # any valid projection input for PyProj
|
74
|
+
"straighten":{"func":vf._validate_type, "kwargs":{"match":bool}}, # true or false
|
75
|
+
"pad":{"func":vf._validate_range, "kwargs":{"min":0}}, # any positive number
|
76
|
+
"plot":{"func":vf._validate_type, "kwargs":{"match":bool}}, # true or false
|
77
|
+
"facecolor":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
|
78
|
+
"linecolor":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
|
79
|
+
"alpha":{"func":vf._validate_range, "kwargs":{"min":0}}, # any positive number
|
80
|
+
"linewidth":{"func":vf._validate_range, "kwargs":{"min":0}}, # any positive number
|
81
|
+
"to_return":{"func":vf._validate_list, "kwargs":{"list":["shape", "patch", "fig", "ax"], "none_ok":True}}, # any value in this list
|
82
|
+
}
|
83
|
+
|
84
|
+
_VALIDATE_DETAIL = {
|
85
|
+
"to_return":{"func":vf._validate_list, "kwargs":{"list":["connectors", "lines"], "none_ok":True}}, # any value in this list
|
86
|
+
"connector_color":{"func":matplotlib.rcsetup.validate_color}, # any color value for matplotlib
|
87
|
+
"connector_width":{"func":vf._validate_range, "kwargs":{"min":0}}, # any positive number
|
88
|
+
}
|
@@ -90,7 +90,7 @@ class _TYPE_ROTATION(TypedDict, total=False):
|
|
90
90
|
coords: Tuple[float | int, float | int] # only required if degrees is None: should be a tuple of coordinates in the relevant reference window
|
91
91
|
|
92
92
|
### VALIDITY DICTS ###
|
93
|
-
# These compile the functions
|
93
|
+
# These compile the functions in validation/functions, as well as matplotlib's built-in validity functions
|
94
94
|
# into dictionaries that can be used to validate all the inputs to a dictionary at once
|
95
95
|
|
96
96
|
_VALIDATE_PRIMARY = {
|
@@ -7,12 +7,10 @@
|
|
7
7
|
|
8
8
|
# Geo packages
|
9
9
|
import pyproj
|
10
|
-
# Graphical packages
|
11
|
-
import matplotlib
|
12
10
|
# matplotlib's useful validation functions
|
13
11
|
import matplotlib.rcsetup
|
14
12
|
# The types we use in this script
|
15
|
-
from typing import
|
13
|
+
from typing import TypedDict, Literal, get_args
|
16
14
|
# Finally, the validation functions
|
17
15
|
from . import functions as vf
|
18
16
|
|
@@ -161,7 +159,7 @@ class _TYPE_AOB(TypedDict, total=False):
|
|
161
159
|
# bbox_transform: None # NOTE: currently unvalidated, use at your own risk!
|
162
160
|
|
163
161
|
### VALIDITY DICTS ###
|
164
|
-
# These compile the functions
|
162
|
+
# These compile the functions in validation/functions, as well as matplotlib's built-in validity functions
|
165
163
|
# into dictionaries that can be used to validate all the inputs to a dictionary at once
|
166
164
|
|
167
165
|
_VALIDATE_PRIMARY = {
|
@@ -173,7 +171,7 @@ _VALID_BAR_TICK_LOC = get_args(_TYPE_BAR.__annotations__["tick_loc"])
|
|
173
171
|
_VALID_BAR_MINOR_TYPE = get_args(_TYPE_BAR.__annotations__["minor_type"])
|
174
172
|
|
175
173
|
_VALIDATE_BAR = {
|
176
|
-
"projection":{"func":vf._validate_projection}, # must be a valid CRS
|
174
|
+
"projection":{"func":vf._validate_projection, "kwargs":{"none_ok":False}}, # must be a valid CRS
|
177
175
|
"unit":{"func":vf._validate_list, "kwargs":{"list":list(units_standard.keys()), "none_ok":True}}, # any of the listed unit values are accepted
|
178
176
|
"rotation":{"func":vf._validate_range, "kwargs":{"min":-360, "max":360, "none_ok":True}}, # between -360 and 360 degrees
|
179
177
|
"max":{"func":vf._validate_range, "kwargs":{"min":0, "max":None, "none_ok":True}}, # between 0 and inf
|