r5py 1.1.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.
Files changed (49) hide show
  1. r5py/__init__.py +27 -0
  2. r5py/__main__.py +3 -0
  3. r5py/r5/__init__.py +39 -0
  4. r5py/r5/access_leg.py +12 -0
  5. r5py/r5/base_travel_time_matrix.py +255 -0
  6. r5py/r5/detailed_itineraries.py +226 -0
  7. r5py/r5/direct_leg.py +38 -0
  8. r5py/r5/egress_leg.py +12 -0
  9. r5py/r5/elevation_cost_function.py +50 -0
  10. r5py/r5/elevation_model.py +89 -0
  11. r5py/r5/file_storage.py +82 -0
  12. r5py/r5/isochrones.py +345 -0
  13. r5py/r5/regional_task.py +600 -0
  14. r5py/r5/scenario.py +36 -0
  15. r5py/r5/street_layer.py +90 -0
  16. r5py/r5/street_segment.py +39 -0
  17. r5py/r5/transfer_leg.py +12 -0
  18. r5py/r5/transit_layer.py +87 -0
  19. r5py/r5/transit_leg.py +12 -0
  20. r5py/r5/transport_mode.py +148 -0
  21. r5py/r5/transport_network.py +299 -0
  22. r5py/r5/travel_time_matrix.py +186 -0
  23. r5py/r5/trip.py +97 -0
  24. r5py/r5/trip_leg.py +204 -0
  25. r5py/r5/trip_planner.py +576 -0
  26. r5py/util/__init__.py +31 -0
  27. r5py/util/camel_to_snake_case.py +25 -0
  28. r5py/util/classpath.py +95 -0
  29. r5py/util/config.py +176 -0
  30. r5py/util/contains_gtfs_data.py +46 -0
  31. r5py/util/data_validation.py +28 -0
  32. r5py/util/environment.py +32 -0
  33. r5py/util/exceptions.py +43 -0
  34. r5py/util/file_digest.py +40 -0
  35. r5py/util/good_enough_equidistant_crs.py +73 -0
  36. r5py/util/jvm.py +138 -0
  37. r5py/util/memory_footprint.py +178 -0
  38. r5py/util/parse_int_date.py +24 -0
  39. r5py/util/sample_data_set.py +76 -0
  40. r5py/util/snake_to_camel_case.py +16 -0
  41. r5py/util/spatially_clustered_geodataframe.py +66 -0
  42. r5py/util/validating_requests_session.py +58 -0
  43. r5py/util/warnings.py +7 -0
  44. r5py/util/working_copy.py +42 -0
  45. r5py-1.1.0.dist-info/METADATA +176 -0
  46. r5py-1.1.0.dist-info/RECORD +49 -0
  47. r5py-1.1.0.dist-info/WHEEL +5 -0
  48. r5py-1.1.0.dist-info/licenses/LICENSE +3 -0
  49. r5py-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,576 @@
1
+ #!/usr/bin/env python3
2
+
3
+
4
+ """Find detailed routes between two points."""
5
+
6
+ import copy
7
+ import collections
8
+ import datetime
9
+ import functools
10
+ import warnings
11
+
12
+ import jpype
13
+ import pyproj
14
+ import shapely
15
+
16
+ from .access_leg import AccessLeg
17
+ from .direct_leg import DirectLeg
18
+ from .egress_leg import EgressLeg
19
+ from .street_segment import StreetSegment
20
+ from .transfer_leg import TransferLeg
21
+ from .transit_leg import TransitLeg
22
+ from .transport_mode import TransportMode
23
+ from .trip import Trip
24
+ from ..util import GoodEnoughEquidistantCrs, start_jvm
25
+
26
+ import com.conveyal.r5
27
+ import gnu.trove.map
28
+ import java.lang
29
+
30
+ __all__ = ["TripPlanner"]
31
+
32
+
33
+ start_jvm()
34
+
35
+
36
+ COORDINATE_CORRECTION_FACTOR = com.conveyal.r5.streets.VertexStore.FIXED_FACTOR
37
+ R5_CRS = "EPSG:4326"
38
+
39
+ ONE_MINUTE = datetime.timedelta(minutes=1)
40
+ ZERO_SECONDS = datetime.timedelta(seconds=0)
41
+
42
+
43
+ class TripPlanner:
44
+ """Find detailed routes between two points."""
45
+
46
+ MAX_ACCESS_TIME = datetime.timedelta(hours=1)
47
+ MAX_EGRESS_TIME = MAX_ACCESS_TIME
48
+
49
+ def __init__(self, transport_network, request):
50
+ """
51
+ Find detailed routes between two points.
52
+
53
+ Arguments
54
+ =========
55
+ transport_network : r5py.r5.TransportNetwork
56
+ A transport network to route on
57
+ request : r5py.r5.regional_task
58
+ The parameters that should be used when finding a route
59
+ """
60
+ self.transport_network = transport_network
61
+ self.request = request
62
+ self._transfer_paths = {}
63
+
64
+ EQUIDISTANT_CRS = GoodEnoughEquidistantCrs(self.transport_network.extent)
65
+ self._crs_transformer_function = pyproj.Transformer.from_crs(
66
+ R5_CRS,
67
+ EQUIDISTANT_CRS,
68
+ always_xy=True,
69
+ ).transform
70
+
71
+ @property
72
+ def trips(self):
73
+ """
74
+ Detailed routes between two points.
75
+
76
+ Returns
77
+ =======
78
+ list[r5py.r5.Trip]
79
+ Detailed routes that meet the requested parameters
80
+ """
81
+ trips = self.direct_paths + self.transit_paths
82
+ return trips
83
+
84
+ @property
85
+ def direct_paths(self):
86
+ """
87
+ Detailed routes between two points using direct modes.
88
+
89
+ Returns
90
+ =======
91
+ list[r5py.r5.Trip]
92
+ Detailed routes that meet the requested parameters, using direct
93
+ modes (walking, cycling, driving).
94
+ """
95
+ direct_paths = []
96
+ request = copy.copy(self.request)
97
+
98
+ direct_modes = [mode for mode in request.transport_modes if mode.is_street_mode]
99
+
100
+ for transport_mode in direct_modes:
101
+ # short-circuit identical from_id and to_id:
102
+ if (
103
+ request._regional_task.fromLat == request._regional_task.toLat
104
+ and request._regional_task.fromLon == request._regional_task.toLon
105
+ ):
106
+ lat = request._regional_task.fromLat
107
+ lon = request._regional_task.fromLon
108
+ direct_paths.append(
109
+ Trip(
110
+ [
111
+ DirectLeg(
112
+ transport_mode,
113
+ collections.namedtuple(
114
+ "StreetSegment",
115
+ ["distance", "duration", "geometry"],
116
+ )(0.0, 0.0, f"LINESTRING({lon} {lat}, {lon} {lat})"),
117
+ )
118
+ ]
119
+ )
120
+ )
121
+ else:
122
+ street_router = com.conveyal.r5.streets.StreetRouter(
123
+ self.transport_network.street_layer
124
+ )
125
+ street_router.profileRequest = request
126
+ street_router.streetMode = transport_mode
127
+
128
+ street_router.setOrigin(
129
+ request._regional_task.fromLat,
130
+ request._regional_task.fromLon,
131
+ )
132
+ street_router.setDestination(
133
+ request._regional_task.toLat,
134
+ request._regional_task.toLon,
135
+ )
136
+
137
+ street_router.route()
138
+
139
+ try:
140
+ router_state = street_router.getState(
141
+ street_router.getDestinationSplit()
142
+ )
143
+ street_segment = self._street_segment_from_router_state(
144
+ router_state,
145
+ transport_mode,
146
+ )
147
+ direct_paths.append(
148
+ Trip(
149
+ [
150
+ DirectLeg(transport_mode, street_segment),
151
+ ]
152
+ )
153
+ )
154
+ except (
155
+ java.lang.NullPointerException,
156
+ java.util.NoSuchElementException,
157
+ ):
158
+ warnings.warn(
159
+ f"Could not find route between origin "
160
+ f"({self.request._regional_task.fromLon}, "
161
+ f"{self.request._regional_task.fromLat}) "
162
+ f"and destination ({self.request._regional_task.toLon}, "
163
+ f"{self.request._regional_task.toLat})",
164
+ RuntimeWarning,
165
+ stacklevel=1,
166
+ )
167
+ return direct_paths
168
+
169
+ def _street_segment_from_router_state(self, router_state, transport_mode):
170
+ """Retrieve a StreetSegment for a route."""
171
+ street_path = com.conveyal.r5.profile.StreetPath(
172
+ router_state,
173
+ self.transport_network,
174
+ False,
175
+ )
176
+ street_segment = StreetSegment(street_path)
177
+ return street_segment
178
+
179
+ @functools.cached_property
180
+ def transit_paths(self):
181
+ """
182
+ Detailed routes between two points on public transport.
183
+
184
+ Returns
185
+ =======
186
+ list[r5py.r5.Trip]
187
+ Detailed routes that meet the requested parameters, on public
188
+ transport.
189
+ """
190
+ transit_paths = []
191
+
192
+ # if any transit mode requested:
193
+ if [mode for mode in self.request.transport_modes if mode.is_transit_mode]:
194
+ request = copy.copy(self.request)
195
+
196
+ midnight = self.request.departure.replace(
197
+ hour=0, minute=0, second=0, microsecond=0
198
+ )
199
+ suboptimal_minutes = max(self.request._regional_task.suboptimalMinutes, 0)
200
+ transit_layer = self.transport_network.transit_layer
201
+
202
+ if (
203
+ request._regional_task.fromLat == request._regional_task.toLat
204
+ and request._regional_task.fromLon == request._regional_task.toLon
205
+ ):
206
+ lat = request._regional_task.fromLat
207
+ lon = request._regional_task.fromLon
208
+ transit_paths.append(
209
+ Trip(
210
+ [
211
+ TransitLeg(
212
+ transport_mode=TransportMode.TRANSIT,
213
+ departure_time=None,
214
+ distance=0.0,
215
+ travel_time=ZERO_SECONDS,
216
+ wait_time=ZERO_SECONDS,
217
+ geometry=shapely.LineString(((lon, lat), (lon, lat))),
218
+ )
219
+ ]
220
+ )
221
+ )
222
+ else:
223
+ # McRapterSuboptimalPathProfileRouter needs this simple callback,
224
+ # this could, of course, be a lambda function, but this way it’s
225
+ # cleaner
226
+ def list_supplier_callback(departure_time):
227
+ return com.conveyal.r5.profile.SuboptimalDominatingList(
228
+ suboptimal_minutes
229
+ )
230
+
231
+ transit_router = (
232
+ com.conveyal.r5.profile.McRaptorSuboptimalPathProfileRouter(
233
+ self.transport_network,
234
+ request,
235
+ self._transit_access_times,
236
+ self._transit_egress_times,
237
+ list_supplier_callback,
238
+ None,
239
+ True,
240
+ )
241
+ )
242
+ transit_router.route()
243
+
244
+ # `finalStatesByDepartureTime` is a hashmap of lists of router
245
+ # states, indexed by departure times (in seconds since midnight)
246
+ final_states = {
247
+ (midnight + datetime.timedelta(seconds=departure_time)): state
248
+ for departure_time, states in zip(
249
+ transit_router.finalStatesByDepartureTime.keys(),
250
+ transit_router.finalStatesByDepartureTime.values(),
251
+ )
252
+ for state in list(states) # some departure times yield no results
253
+ }
254
+
255
+ # keep another cache layer of shortest access and egress legs
256
+ access_legs_by_stop = {}
257
+ egress_legs_by_stop = {}
258
+
259
+ for departure_time, state in final_states.items():
260
+ trip = Trip()
261
+ while state:
262
+ if state.stop == -1: # EgressLeg
263
+ try:
264
+ leg = egress_legs_by_stop[state.back.stop]
265
+ except KeyError:
266
+ leg = min(
267
+ [
268
+ self._transit_egress_paths[transport_mode][
269
+ state.back.stop
270
+ ]
271
+ for transport_mode in self._transit_egress_paths
272
+ ]
273
+ )
274
+ egress_legs_by_stop[state.back.stop] = leg
275
+ leg.wait_time = ZERO_SECONDS
276
+ leg.departure_time = (
277
+ midnight
278
+ + datetime.timedelta(seconds=state.back.time)
279
+ + ONE_MINUTE
280
+ )
281
+ leg.arrival_time = leg.departure_time + leg.travel_time
282
+
283
+ elif state.back is None: # AccessLeg
284
+ try:
285
+ leg = access_legs_by_stop[state.stop]
286
+ except KeyError:
287
+ leg = min(
288
+ [
289
+ self._transit_access_paths[transport_mode][
290
+ state.stop
291
+ ]
292
+ for transport_mode in self._transit_access_paths
293
+ ]
294
+ )
295
+ access_legs_by_stop[state.stop] = leg
296
+ leg.wait_time = ZERO_SECONDS
297
+ leg.arrival_time = midnight + datetime.timedelta(
298
+ seconds=state.time
299
+ )
300
+ leg.departure_time = leg.arrival_time - leg.travel_time
301
+
302
+ else:
303
+ if state.pattern == -1: # TransferLeg
304
+ departure_stop = state.back.stop
305
+ arrival_stop = state.stop
306
+
307
+ leg = self._transit_transfer_path(
308
+ departure_stop, arrival_stop
309
+ )
310
+
311
+ leg.departure_time = (
312
+ midnight
313
+ + datetime.timedelta(seconds=state.back.time)
314
+ + ONE_MINUTE
315
+ )
316
+ leg.arrival_time = leg.departure_time + leg.travel_time
317
+ leg.wait_time = (
318
+ datetime.timedelta(
319
+ seconds=(state.time - state.back.time)
320
+ )
321
+ - leg.travel_time
322
+ + ONE_MINUTE # the slack added above
323
+ )
324
+
325
+ else: # TransitLeg
326
+ pattern = transit_layer.trip_patterns[state.pattern]
327
+
328
+ # Use the indices to look up the stop ids, which
329
+ # are scoped by the GTFS feed supplied
330
+ start_stop_id = transit_layer.get_stop_id_from_index(
331
+ state.back.stop
332
+ ).split(":")[1]
333
+ end_stop = transit_layer.get_stop_id_from_index(
334
+ state.stop
335
+ )
336
+ end_stop_id = end_stop.split(":")[1]
337
+ feed = end_stop.split(":")[0]
338
+
339
+ route = transit_layer.routes[pattern.routeIndex]
340
+ transport_mode = TransportMode(
341
+ com.conveyal.r5.transit.TransitLayer.getTransitModes( # noqa: E501
342
+ route.route_type
343
+ ).toString()
344
+ )
345
+ departure_time = midnight + datetime.timedelta(
346
+ seconds=state.boardTime
347
+ )
348
+ travel_time = datetime.timedelta(
349
+ seconds=(state.time - state.boardTime)
350
+ )
351
+ wait_time = datetime.timedelta(
352
+ seconds=(state.boardTime - state.back.time)
353
+ )
354
+
355
+ # ‘hops’ in R5 terminology are the LineStrings
356
+ # between each pair of consecutive stops of a route
357
+ hops = list(pattern.getHopGeometries(transit_layer))
358
+
359
+ # select only the ‘hops’ between our stops, and merge
360
+ # them into one LineString
361
+ hops = hops[
362
+ state.boardStopPosition : state.alightStopPosition # noqa: E203, E501
363
+ ]
364
+ geometry = shapely.line_merge(
365
+ shapely.MultiLineString(
366
+ [
367
+ shapely.from_wkt(str(geometry.toText()))
368
+ for geometry in hops
369
+ ]
370
+ )
371
+ )
372
+
373
+ # distance: based on the geometry, which might
374
+ # be inaccurate.
375
+ distance = shapely.ops.transform(
376
+ self._crs_transformer_function,
377
+ geometry,
378
+ ).length
379
+
380
+ leg = TransitLeg(
381
+ transport_mode=transport_mode,
382
+ departure_time=departure_time,
383
+ distance=distance,
384
+ travel_time=travel_time,
385
+ wait_time=wait_time,
386
+ feed=str(feed),
387
+ agency_id=str(route.agency_id),
388
+ route_id=str(route.route_id),
389
+ start_stop_id=str(start_stop_id),
390
+ end_stop_id=str(end_stop_id),
391
+ geometry=geometry,
392
+ )
393
+
394
+ # we traverse in reverse order:
395
+ # add leg to beginning of trip,
396
+ # then fetch previous state (=leg)
397
+ trip = leg + trip
398
+ state = state.back
399
+
400
+ # R5 sometimes reports the same path more than once, skip duplicates
401
+ if trip not in transit_paths:
402
+ transit_paths.append(trip)
403
+
404
+ return transit_paths
405
+
406
+ @functools.cached_property
407
+ def _transit_access_paths(self):
408
+ access_paths = {}
409
+
410
+ request = copy.copy(self.request)
411
+ request._regional_task.reverseSearch = False
412
+
413
+ street_router = com.conveyal.r5.streets.StreetRouter(
414
+ self.transport_network.street_layer
415
+ )
416
+ street_router.profileRequest = request
417
+ street_router.setOrigin(
418
+ self.request._regional_task.fromLat,
419
+ self.request._regional_task.fromLon,
420
+ )
421
+ street_router.transitStopSearch = True
422
+ street_router.timeLimitSeconds = round(self.MAX_ACCESS_TIME.total_seconds())
423
+
424
+ transit_layer = self.transport_network.transit_layer
425
+
426
+ for transport_mode in request.access_modes:
427
+ access_paths[transport_mode] = {}
428
+
429
+ street_router.streetMode = transport_mode
430
+ street_router.route()
431
+ reached_stops = street_router.getReachedStops()
432
+
433
+ for stop in reached_stops.keys():
434
+ router_state = street_router.getStateAtVertex(
435
+ transit_layer.get_street_vertex_for_stop(stop)
436
+ )
437
+ street_segment = self._street_segment_from_router_state(
438
+ router_state,
439
+ transport_mode,
440
+ )
441
+ access_paths[transport_mode][stop] = AccessLeg(
442
+ transport_mode, street_segment
443
+ )
444
+ return access_paths
445
+
446
+ @functools.cached_property
447
+ def _transit_access_times(self):
448
+ """
449
+ Times to reached stops.
450
+
451
+ In the format required by McRaptorSuboptimalPathProfileRouter.
452
+ """
453
+ access_times = jpype.JObject(
454
+ {
455
+ com.conveyal.r5.api.util.LegMode
456
+ @ mode: gnu.trove.map.hash.TIntIntHashMap(
457
+ [stop for stop in reached_stops.keys()],
458
+ [
459
+ round(transfer_leg.travel_time.total_seconds())
460
+ for transfer_leg in reached_stops.values()
461
+ ],
462
+ )
463
+ for mode, reached_stops in self._transit_access_paths.items()
464
+ },
465
+ "java.util.Map<com.conveyal.r5.LegMode, gnu.trove.map.TIntIntMap>",
466
+ )
467
+ return access_times
468
+
469
+ @functools.cached_property
470
+ def _transit_egress_paths(self):
471
+ egress_paths = {}
472
+
473
+ request = copy.copy(self.request)
474
+ request._regional_task.reverseSearch = True
475
+
476
+ street_router = com.conveyal.r5.streets.StreetRouter(
477
+ self.transport_network.street_layer
478
+ )
479
+ street_router.profileRequest = request
480
+ street_router.setOrigin(
481
+ self.request._regional_task.toLat,
482
+ self.request._regional_task.toLon,
483
+ )
484
+ street_router.transitStopSearch = True
485
+ street_router.timeLimitSeconds = round(self.MAX_EGRESS_TIME.total_seconds())
486
+
487
+ transit_layer = self.transport_network.transit_layer
488
+
489
+ for transport_mode in request.egress_modes:
490
+ egress_paths[transport_mode] = {}
491
+
492
+ street_router.streetMode = transport_mode
493
+
494
+ street_router.route()
495
+ reached_stops = street_router.getReachedStops()
496
+
497
+ for stop in reached_stops.keys():
498
+ router_state = street_router.getStateAtVertex(
499
+ transit_layer.get_street_vertex_for_stop(stop)
500
+ )
501
+ street_segment = self._street_segment_from_router_state(
502
+ router_state,
503
+ transport_mode,
504
+ )
505
+ egress_paths[transport_mode][stop] = EgressLeg(
506
+ transport_mode, street_segment
507
+ )
508
+ return egress_paths
509
+
510
+ @functools.cached_property
511
+ def _transit_egress_times(self):
512
+ """
513
+ Times to reached stops.
514
+
515
+ In the format required by McRaptorSuboptimalPathProfileRouter.
516
+ """
517
+ egress_times = jpype.JObject(
518
+ {
519
+ com.conveyal.r5.api.util.LegMode
520
+ @ mode: gnu.trove.map.hash.TIntIntHashMap(
521
+ [stop for stop in reached_stops.keys()],
522
+ [
523
+ round(transfer_leg.travel_time.total_seconds())
524
+ for transfer_leg in reached_stops.values()
525
+ ],
526
+ )
527
+ for mode, reached_stops in self._transit_egress_paths.items()
528
+ },
529
+ "java.util.Map<com.conveyal.r5.LegMode, gnu.trove.map.TIntIntMap>",
530
+ )
531
+ return egress_times
532
+
533
+ def _transit_transfer_path(self, from_stop, to_stop):
534
+ """Find a transfer path between two transit stops."""
535
+ self._transfer_paths = {}
536
+ while True:
537
+ try:
538
+ transfer_path = self._transfer_paths[(from_stop, to_stop)]
539
+ except KeyError:
540
+ request = copy.copy(self.request)
541
+
542
+ street_router = com.conveyal.r5.streets.StreetRouter(
543
+ self.transport_network.street_layer
544
+ )
545
+ street_router.profileRequest = request
546
+ street_router.streetMode = TransportMode.WALK
547
+
548
+ get_coordinates_for_stop = (
549
+ self.transport_network.transit_layer._transit_layer.getCoordinateForStopFixed # noqa: E501
550
+ )
551
+ from_stop_coordinates = get_coordinates_for_stop(from_stop)
552
+ to_stop_coordinates = get_coordinates_for_stop(to_stop)
553
+
554
+ from_lat = from_stop_coordinates.getY() / COORDINATE_CORRECTION_FACTOR
555
+ from_lon = from_stop_coordinates.getX() / COORDINATE_CORRECTION_FACTOR
556
+ to_lat = to_stop_coordinates.getY() / COORDINATE_CORRECTION_FACTOR
557
+ to_lon = to_stop_coordinates.getX() / COORDINATE_CORRECTION_FACTOR
558
+
559
+ street_router.setOrigin(from_lat, from_lon)
560
+ street_router.setDestination(to_lat, to_lon)
561
+
562
+ street_router.route()
563
+
564
+ router_state = street_router.getState(
565
+ street_router.getDestinationSplit()
566
+ )
567
+ street_segment = self._street_segment_from_router_state(
568
+ router_state,
569
+ TransportMode.WALK,
570
+ )
571
+
572
+ transfer_path = self._transfer_paths[(from_stop, to_stop)] = (
573
+ TransferLeg(TransportMode.WALK, street_segment)
574
+ )
575
+
576
+ return transfer_path
r5py/util/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """Utility functions, e.g., starting a JVM, and accessing configuration."""
4
+
5
+ from . import environment # noqa: F401
6
+
7
+ from .camel_to_snake_case import camel_to_snake_case
8
+ from .config import Config
9
+ from .contains_gtfs_data import contains_gtfs_data
10
+ from .data_validation import check_od_data_set
11
+ from .file_digest import FileDigest
12
+ from .good_enough_equidistant_crs import GoodEnoughEquidistantCrs
13
+ from .jvm import start_jvm
14
+ from .parse_int_date import parse_int_date
15
+ from .snake_to_camel_case import snake_to_camel_case
16
+ from .spatially_clustered_geodataframe import SpatiallyClusteredGeoDataFrame
17
+ from .working_copy import WorkingCopy
18
+
19
+ __all__ = [
20
+ "camel_to_snake_case",
21
+ "check_od_data_set",
22
+ "Config",
23
+ "contains_gtfs_data",
24
+ "FileDigest",
25
+ "GoodEnoughEquidistantCrs",
26
+ "parse_int_date",
27
+ "snake_to_camel_case",
28
+ "SpatiallyClusteredGeoDataFrame",
29
+ "start_jvm",
30
+ "WorkingCopy",
31
+ ]
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """Convert a camelCase/CamelCase formated string to a snake_case format."""
4
+
5
+ import re
6
+
7
+ __all__ = ["camel_to_snake_case"]
8
+
9
+
10
+ # https://stackoverflow.com/a/1176023
11
+
12
+
13
+ CAMEL_CASE_TO_SNAKE_CASE_RE1 = re.compile("(.)([A-Z][a-z]+)")
14
+ CAMEL_CASE_TO_SNAKE_CASE_RE2 = re.compile("([a-z0-9])([A-Z])")
15
+ CAMEL_CASE_TO_SNAKE_CASE_SUBSTITUTE = r"\1_\2"
16
+
17
+
18
+ def camel_to_snake_case(camel_case):
19
+ """Convert `camel_case` to snake_case spelling."""
20
+ return CAMEL_CASE_TO_SNAKE_CASE_RE2.sub(
21
+ CAMEL_CASE_TO_SNAKE_CASE_SUBSTITUTE,
22
+ CAMEL_CASE_TO_SNAKE_CASE_RE1.sub(
23
+ CAMEL_CASE_TO_SNAKE_CASE_SUBSTITUTE, camel_case
24
+ ),
25
+ ).lower()