r5py 0.1.1.dev2__py3-none-any.whl → 1.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.

Potentially problematic release.


This version of r5py might be problematic. Click here for more details.

Files changed (38) hide show
  1. r5py/__init__.py +8 -1
  2. r5py/__main__.py +1 -14
  3. r5py/r5/__init__.py +20 -2
  4. r5py/r5/{base_travel_time_matrix_computer.py → base_travel_time_matrix.py} +28 -8
  5. r5py/r5/{detailed_itineraries_computer.py → detailed_itineraries.py} +82 -20
  6. r5py/r5/direct_leg.py +1 -3
  7. r5py/r5/isochrones.py +351 -0
  8. r5py/r5/regional_task.py +12 -9
  9. r5py/r5/street_layer.py +8 -3
  10. r5py/r5/street_segment.py +41 -0
  11. r5py/r5/transfer_leg.py +2 -6
  12. r5py/r5/transit_layer.py +6 -0
  13. r5py/r5/transit_leg.py +1 -5
  14. r5py/r5/transport_mode.py +5 -3
  15. r5py/r5/transport_network.py +60 -138
  16. r5py/r5/travel_time_matrix.py +209 -0
  17. r5py/r5/trip.py +13 -8
  18. r5py/r5/trip_leg.py +76 -15
  19. r5py/r5/trip_planner.py +109 -54
  20. r5py/util/__init__.py +8 -0
  21. r5py/util/classpath.py +9 -5
  22. r5py/util/config.py +32 -7
  23. r5py/util/environment.py +34 -0
  24. r5py/util/file_digest.py +42 -0
  25. r5py/util/good_enough_equidistant_crs.py +8 -4
  26. r5py/util/memory_footprint.py +3 -5
  27. r5py/util/sample_data_set.py +17 -6
  28. r5py/util/spatially_clustered_geodataframe.py +78 -0
  29. r5py/util/validating_requests_session.py +2 -2
  30. r5py/util/working_copy.py +44 -0
  31. {r5py-0.1.1.dev2.dist-info → r5py-1.0.0.dist-info}/METADATA +34 -33
  32. r5py-1.0.0.dist-info/RECORD +47 -0
  33. {r5py-0.1.1.dev2.dist-info → r5py-1.0.0.dist-info}/WHEEL +1 -1
  34. r5py/r5/travel_time_matrix_computer.py +0 -134
  35. r5py/sampledata/_keep/__init__.py +0 -3
  36. r5py-0.1.1.dev2.dist-info/RECORD +0 -42
  37. {r5py-0.1.1.dev2.dist-info → r5py-1.0.0.dist-info}/LICENSE +0 -0
  38. {r5py-0.1.1.dev2.dist-info → r5py-1.0.0.dist-info}/top_level.txt +0 -0
r5py/r5/trip_leg.py CHANGED
@@ -4,6 +4,11 @@
4
4
  """Represent one leg of a trip."""
5
5
 
6
6
 
7
+ import datetime
8
+ import numpy
9
+ import shapely
10
+
11
+
7
12
  __all__ = ["TripLeg"]
8
13
 
9
14
 
@@ -21,39 +26,56 @@ class TripLeg:
21
26
  "distance",
22
27
  "travel_time",
23
28
  "wait_time",
24
- "route",
29
+ "feed",
30
+ "agency_id",
31
+ "route_id",
32
+ "start_stop_id",
33
+ "end_stop_id",
25
34
  "geometry",
26
35
  ]
27
36
 
28
37
  def __init__(
29
38
  self,
30
39
  transport_mode=None,
31
- departure_time=None,
40
+ departure_time=numpy.datetime64("NaT"),
32
41
  distance=None,
33
- travel_time=None,
34
- wait_time=None,
35
- route=None,
36
- geometry=None,
42
+ travel_time=datetime.timedelta(seconds=0),
43
+ wait_time=datetime.timedelta(seconds=0),
44
+ feed=None,
45
+ agency_id=None,
46
+ route_id=None,
47
+ start_stop_id=None,
48
+ end_stop_id=None,
49
+ geometry=shapely.LineString(),
37
50
  ):
38
51
  """
39
52
  Represent one leg of a trip.
40
53
 
41
- This is a base class, use one the specific classes,
42
- e.g., TransitLeg, or DirectLeg
54
+ This is a base class, use one of the more specific classes, e.g.,
55
+ TransitLeg, or DirectLeg
43
56
 
44
57
  Arguments
45
58
  =========
46
59
  transport_mode : r5py.TransportMode
47
60
  mode of transport this trip leg was travelled
48
- departure_time : datetime.datetime,
61
+ departure_time : datetime.datetime
62
+ departure time of this trip leg
49
63
  distance : float
50
64
  distance covered by this trip leg, in metres
51
65
  travel_time : datetime.timedelta
52
66
  time spent travelling on this trip leg
53
67
  wait_time : datetime.timedelta
54
68
  time spent waiting for a connection on this trip leg
55
- route : str
56
- public transport route used for this trip leg
69
+ feed : str
70
+ the GTFS feed identifier used for this trip leg
71
+ agency_id : str
72
+ the GTFS id the agency used for this trip leg
73
+ route_id : str
74
+ the GTFS id of the public transport route used for this trip leg
75
+ start_stop_id : str
76
+ the GTFS stop_id of the boarding stop used for this trip leg
77
+ end_stop_id : str
78
+ the GTFS stop_id of the aligning stop used for this trip leg
57
79
  geometry : shapely.LineString
58
80
  spatial representation of this trip leg
59
81
  """
@@ -62,10 +84,15 @@ class TripLeg:
62
84
  self.distance = distance
63
85
  self.travel_time = travel_time
64
86
  self.wait_time = wait_time
65
- self.route = route
87
+ self.feed = feed
88
+ self.agency_id = agency_id
89
+ self.route_id = route_id
90
+ self.start_stop_id = start_stop_id
91
+ self.end_stop_id = end_stop_id
66
92
  self.geometry = geometry
67
93
 
68
94
  def __add__(self, other):
95
+ """Trip-chain `other` to `self`."""
69
96
  from .trip import Trip
70
97
 
71
98
  if isinstance(other, self.__class__):
@@ -80,6 +107,7 @@ class TripLeg:
80
107
  )
81
108
 
82
109
  def __radd__(self, other):
110
+ """Trip-chain `self` to `other`."""
83
111
  from .trip import Trip
84
112
 
85
113
  if other == 0: # first iteration of sum()
@@ -90,31 +118,43 @@ class TripLeg:
90
118
  else:
91
119
  return self.__add__(other)
92
120
 
121
+ def __eq__(self, other):
122
+ """Check if `other` is an equal `TripLeg`."""
123
+ if isinstance(other, self.__class__):
124
+ return False not in [
125
+ self._are_columns_equal(other, column) for column in self.COLUMNS
126
+ ]
127
+
93
128
  def __gt__(self, other):
129
+ """Check if `other` has a longer travel time."""
94
130
  if isinstance(other, TripLeg):
95
131
  return (self.travel_time + self.wait_time) > (
96
132
  other.travel_time + other.wait_time
97
133
  )
98
134
 
99
135
  def __ge__(self, other):
136
+ """Check if `other` has a longer or equal travel time."""
100
137
  if isinstance(other, TripLeg):
101
138
  return (self.travel_time + self.wait_time) >= (
102
139
  other.travel_time + other.wait_time
103
140
  )
104
141
 
105
142
  def __lt__(self, other):
143
+ """Check if `other` has a shorter travel time."""
106
144
  if isinstance(other, TripLeg):
107
145
  return (self.travel_time + self.wait_time) < (
108
146
  other.travel_time + other.wait_time
109
147
  )
110
148
 
111
149
  def __le__(self, other):
150
+ """Check if `other` has a shorter or equal travel time."""
112
151
  if isinstance(other, TripLeg):
113
152
  return (self.travel_time + self.wait_time) <= (
114
153
  other.travel_time + other.wait_time
115
154
  )
116
155
 
117
156
  def __repr__(self):
157
+ """Return a string representation."""
118
158
  try:
119
159
  first_point = self.geometry.coords[0]
120
160
  last_point = self.geometry.coords[-1]
@@ -127,10 +167,30 @@ class TripLeg:
127
167
  f"{first_point} -> {last_point}"
128
168
  ">"
129
169
  )
130
- except AttributeError:
170
+ except (AttributeError, IndexError):
131
171
  _repr = f"<{self.__class__.__name__}>"
132
172
  return _repr
133
173
 
174
+ def _are_columns_equal(self, other, column):
175
+ """
176
+ Check if a column equals the same column of a different `Trip`.
177
+
178
+ Compare if attribute `column` of self equals attribute `column` of
179
+ other. Also True if both values are None or NaN or NaT.
180
+ """
181
+ self_column = getattr(self, column)
182
+ other_column = getattr(other, column)
183
+
184
+ return (
185
+ self_column == other_column
186
+ or (self_column is None and other_column is None)
187
+ or (self_column == numpy.nan and other_column == numpy.nan)
188
+ or (
189
+ self_column == numpy.datetime64("NaT")
190
+ and other_column == numpy.datetime64("NaT")
191
+ )
192
+ )
193
+
134
194
  def as_table_row(self):
135
195
  """
136
196
  Return a table row (list) of this trip leg’s details.
@@ -138,7 +198,8 @@ class TripLeg:
138
198
  Returns
139
199
  =======
140
200
  list : detailed information about this trip leg: ``transport_mode``,
141
- ``departure_time``, ``distance``, ``travel_time``, ``wait_time``,
142
- ``route``, ``geometry``
201
+ ``departure_time``, ``distance``, ``travel_time``, ``wait_time``,
202
+ ``feed``, ``agency_id`` ``route_id``, ``start_stop_id``,
203
+ ``end_stop_id``, ``geometry``
143
204
  """
144
205
  return [getattr(self, column) for column in self.COLUMNS]
r5py/r5/trip_planner.py CHANGED
@@ -17,6 +17,7 @@ import shapely
17
17
  from .access_leg import AccessLeg
18
18
  from .direct_leg import DirectLeg
19
19
  from .egress_leg import EgressLeg
20
+ from .street_segment import StreetSegment
20
21
  from .transfer_leg import TransferLeg
21
22
  from .transit_leg import TransitLeg
22
23
  from .transport_mode import TransportMode
@@ -43,9 +44,7 @@ ZERO_SECONDS = datetime.timedelta(seconds=0)
43
44
 
44
45
 
45
46
  class TripPlanner:
46
- """
47
- Find detailed routes between two points.
48
- """
47
+ """Find detailed routes between two points."""
49
48
 
50
49
  MAX_ACCESS_TIME = datetime.timedelta(hours=1)
51
50
  MAX_EGRESS_TIME = MAX_ACCESS_TIME
@@ -66,13 +65,15 @@ class TripPlanner:
66
65
 
67
66
  EQUIDISTANT_CRS = GoodEnoughEquidistantCrs(self.transport_network.extent)
68
67
  self._crs_transformer_function = pyproj.Transformer.from_crs(
69
- R5_CRS, EQUIDISTANT_CRS
68
+ R5_CRS,
69
+ EQUIDISTANT_CRS,
70
+ always_xy=True,
70
71
  ).transform
71
72
 
72
73
  @property
73
74
  def trips(self):
74
75
  """
75
- Find detailed routes between two points.
76
+ Detailed routes between two points.
76
77
 
77
78
  Returns
78
79
  =======
@@ -84,6 +85,15 @@ class TripPlanner:
84
85
 
85
86
  @property
86
87
  def direct_paths(self):
88
+ """
89
+ Detailed routes between two points using direct modes.
90
+
91
+ Returns
92
+ =======
93
+ list[r5py.r5.Trip]
94
+ Detailed routes that meet the requested parameters, using direct
95
+ modes (walking, cycling, driving).
96
+ """
87
97
  direct_paths = []
88
98
  request = copy.copy(self.request)
89
99
 
@@ -143,31 +153,41 @@ class TripPlanner:
143
153
  ]
144
154
  )
145
155
  )
146
- except java.lang.NullPointerException:
156
+ except (
157
+ java.lang.NullPointerException,
158
+ java.util.NoSuchElementException,
159
+ ):
147
160
  warnings.warn(
148
161
  f"Could not find route between origin "
149
- f"({self.request.fromLon}, {self.request.fromLat}) "
150
- f"and destination ({self.request.toLon}, {self.request.toLat})",
162
+ f"({self.request._regional_task.fromLon}, "
163
+ f"{self.request._regional_task.fromLat}) "
164
+ f"and destination ({self.request._regional_task.toLon}, "
165
+ f"{self.request._regional_task.toLat})",
151
166
  RuntimeWarning,
152
167
  )
153
168
  return direct_paths
154
169
 
155
170
  def _street_segment_from_router_state(self, router_state, transport_mode):
156
- """Retrieve a com.conveyal.r5.street.StreetSegment for a route."""
171
+ """Retrieve a StreetSegment for a route."""
157
172
  street_path = com.conveyal.r5.profile.StreetPath(
158
173
  router_state,
159
174
  self.transport_network,
160
175
  False,
161
176
  )
162
- street_segment = com.conveyal.r5.api.util.StreetSegment(
163
- street_path,
164
- transport_mode,
165
- self.transport_network.street_layer,
166
- )
177
+ street_segment = StreetSegment(street_path)
167
178
  return street_segment
168
179
 
169
180
  @functools.cached_property
170
181
  def transit_paths(self):
182
+ """
183
+ Detailed routes between two points on public transport.
184
+
185
+ Returns
186
+ =======
187
+ list[r5py.r5.Trip]
188
+ Detailed routes that meet the requested parameters, on public
189
+ transport.
190
+ """
171
191
  transit_paths = []
172
192
 
173
193
  # if any transit mode requested:
@@ -195,7 +215,6 @@ class TripPlanner:
195
215
  distance=0.0,
196
216
  travel_time=ZERO_SECONDS,
197
217
  wait_time=ZERO_SECONDS,
198
- route=None,
199
218
  geometry=shapely.LineString(((lon, lat), (lon, lat))),
200
219
  )
201
220
  ]
@@ -214,8 +233,8 @@ class TripPlanner:
214
233
  com.conveyal.r5.profile.McRaptorSuboptimalPathProfileRouter(
215
234
  self.transport_network,
216
235
  request,
217
- self.transit_access_times,
218
- self.transit_egress_times,
236
+ self._transit_access_times,
237
+ self._transit_egress_times,
219
238
  list_supplier_callback,
220
239
  None,
221
240
  True,
@@ -234,18 +253,26 @@ class TripPlanner:
234
253
  for state in list(states) # some departure times yield no results
235
254
  }
236
255
 
256
+ # keep another cache layer of shortest access and egress legs
257
+ access_legs_by_stop = {}
258
+ egress_legs_by_stop = {}
259
+
237
260
  for departure_time, state in final_states.items():
238
261
  trip = Trip()
239
262
  while state:
240
263
  if state.stop == -1: # EgressLeg
241
- leg = min(
242
- [
243
- self.transit_egress_paths[transport_mode][
244
- state.back.stop
264
+ try:
265
+ leg = egress_legs_by_stop[state.back.stop]
266
+ except KeyError:
267
+ leg = min(
268
+ [
269
+ self._transit_egress_paths[transport_mode][
270
+ state.back.stop
271
+ ]
272
+ for transport_mode in self._transit_egress_paths.keys()
245
273
  ]
246
- for transport_mode in self.transit_egress_paths.keys()
247
- ]
248
- )
274
+ )
275
+ egress_legs_by_stop[state.back.stop] = leg
249
276
  leg.wait_time = ZERO_SECONDS
250
277
  leg.departure_time = (
251
278
  midnight
@@ -255,14 +282,18 @@ class TripPlanner:
255
282
  leg.arrival_time = leg.departure_time + leg.travel_time
256
283
 
257
284
  elif state.back is None: # AccessLeg
258
- leg = min(
259
- [
260
- self.transit_access_paths[transport_mode][
261
- state.stop
285
+ try:
286
+ leg = access_legs_by_stop[state.stop]
287
+ except KeyError:
288
+ leg = min(
289
+ [
290
+ self._transit_access_paths[transport_mode][
291
+ state.stop
292
+ ]
293
+ for transport_mode in self._transit_access_paths.keys()
262
294
  ]
263
- for transport_mode in self.transit_access_paths.keys()
264
- ]
265
- )
295
+ )
296
+ access_legs_by_stop[state.stop] = leg
266
297
  leg.wait_time = ZERO_SECONDS
267
298
  leg.arrival_time = midnight + datetime.timedelta(
268
299
  seconds=state.time
@@ -274,7 +305,7 @@ class TripPlanner:
274
305
  departure_stop = state.back.stop
275
306
  arrival_stop = state.stop
276
307
 
277
- leg = self.transit_transfer_path(
308
+ leg = self._transit_transfer_path(
278
309
  departure_stop, arrival_stop
279
310
  )
280
311
 
@@ -294,6 +325,17 @@ class TripPlanner:
294
325
 
295
326
  else: # TransitLeg
296
327
  pattern = transit_layer.trip_patterns[state.pattern]
328
+
329
+ # Use the indices to look up the stop ids, which 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
+
297
339
  route = transit_layer.routes[pattern.routeIndex]
298
340
  transport_mode = TransportMode(
299
341
  com.conveyal.r5.transit.TransitLayer.getTransitModes(
@@ -346,13 +388,17 @@ class TripPlanner:
346
388
  distance = None
347
389
 
348
390
  leg = TransitLeg(
349
- transport_mode,
350
- departure_time,
351
- distance,
352
- travel_time,
353
- wait_time,
354
- str(route.route_short_name),
355
- geometry,
391
+ transport_mode=transport_mode,
392
+ departure_time=departure_time,
393
+ distance=distance,
394
+ travel_time=travel_time,
395
+ wait_time=wait_time,
396
+ feed=str(feed),
397
+ agency_id=str(route.agency_id),
398
+ route_id=str(route.route_id),
399
+ start_stop_id=str(start_stop_id),
400
+ end_stop_id=str(end_stop_id),
401
+ geometry=geometry,
356
402
  )
357
403
 
358
404
  # we traverse in reverse order:
@@ -361,12 +407,14 @@ class TripPlanner:
361
407
  trip = leg + trip
362
408
  state = state.back
363
409
 
364
- transit_paths.append(trip)
410
+ # R5 sometimes reports the same path more than once, skip duplicates
411
+ if trip not in transit_paths:
412
+ transit_paths.append(trip)
365
413
 
366
414
  return transit_paths
367
415
 
368
416
  @functools.cached_property
369
- def transit_access_paths(self):
417
+ def _transit_access_paths(self):
370
418
  access_paths = {}
371
419
 
372
420
  request = copy.copy(self.request)
@@ -406,9 +454,12 @@ class TripPlanner:
406
454
  return access_paths
407
455
 
408
456
  @functools.cached_property
409
- def transit_access_times(self):
410
- """Times to reached stops in the format required by
411
- McRaptorSuboptimalPathProfileRouter."""
457
+ def _transit_access_times(self):
458
+ """
459
+ Times to reached stops.
460
+
461
+ In the format required by McRaptorSuboptimalPathProfileRouter.
462
+ """
412
463
  access_times = jpype.JObject(
413
464
  {
414
465
  com.conveyal.r5.api.util.LegMode
@@ -419,14 +470,14 @@ class TripPlanner:
419
470
  for transfer_leg in reached_stops.values()
420
471
  ],
421
472
  )
422
- for mode, reached_stops in self.transit_access_paths.items()
473
+ for mode, reached_stops in self._transit_access_paths.items()
423
474
  },
424
475
  "java.util.Map<com.conveyal.r5.LegMode, gnu.trove.map.TIntIntMap>",
425
476
  )
426
477
  return access_times
427
478
 
428
479
  @functools.cached_property
429
- def transit_egress_paths(self):
480
+ def _transit_egress_paths(self):
430
481
  egress_paths = {}
431
482
 
432
483
  request = copy.copy(self.request)
@@ -467,9 +518,12 @@ class TripPlanner:
467
518
  return egress_paths
468
519
 
469
520
  @functools.cached_property
470
- def transit_egress_times(self):
471
- """Times to reached stops in the format required by
472
- McRaptorSuboptimalPathProfileRouter."""
521
+ def _transit_egress_times(self):
522
+ """
523
+ Times to reached stops.
524
+
525
+ In the format required by McRaptorSuboptimalPathProfileRouter.
526
+ """
473
527
  egress_times = jpype.JObject(
474
528
  {
475
529
  com.conveyal.r5.api.util.LegMode
@@ -480,13 +534,14 @@ class TripPlanner:
480
534
  for transfer_leg in reached_stops.values()
481
535
  ],
482
536
  )
483
- for mode, reached_stops in self.transit_egress_paths.items()
537
+ for mode, reached_stops in self._transit_egress_paths.items()
484
538
  },
485
539
  "java.util.Map<com.conveyal.r5.LegMode, gnu.trove.map.TIntIntMap>",
486
540
  )
487
541
  return egress_times
488
542
 
489
- def transit_transfer_path(self, from_stop, to_stop):
543
+ def _transit_transfer_path(self, from_stop, to_stop):
544
+ """Find a transfer path between two transit stops."""
490
545
  self._transfer_paths = {}
491
546
  while True:
492
547
  try:
@@ -524,8 +579,8 @@ class TripPlanner:
524
579
  TransportMode.WALK,
525
580
  )
526
581
 
527
- transfer_path = self._transfer_paths[
528
- (from_stop, to_stop)
529
- ] = TransferLeg(TransportMode.WALK, street_segment)
582
+ transfer_path = self._transfer_paths[(from_stop, to_stop)] = (
583
+ TransferLeg(TransportMode.WALK, street_segment)
584
+ )
530
585
 
531
586
  return transfer_path
r5py/util/__init__.py CHANGED
@@ -2,22 +2,30 @@
2
2
 
3
3
  """Utility functions, e.g., starting a JVM, and accessing configuration."""
4
4
 
5
+ from . import environment # noqa: F401
6
+
5
7
  from .camel_to_snake_case import camel_to_snake_case
6
8
  from .config import Config
7
9
  from .contains_gtfs_data import contains_gtfs_data
8
10
  from .data_validation import check_od_data_set
11
+ from .file_digest import FileDigest
9
12
  from .good_enough_equidistant_crs import GoodEnoughEquidistantCrs
10
13
  from .jvm import start_jvm
11
14
  from .parse_int_date import parse_int_date
12
15
  from .snake_to_camel_case import snake_to_camel_case
16
+ from .spatially_clustered_geodataframe import SpatiallyClusteredGeoDataFrame
17
+ from .working_copy import WorkingCopy
13
18
 
14
19
  __all__ = [
15
20
  "camel_to_snake_case",
16
21
  "check_od_data_set",
17
22
  "Config",
18
23
  "contains_gtfs_data",
24
+ "FileDigest",
19
25
  "GoodEnoughEquidistantCrs",
20
26
  "parse_int_date",
21
27
  "snake_to_camel_case",
28
+ "SpatiallyClusteredGeoDataFrame",
22
29
  "start_jvm",
30
+ "WorkingCopy",
23
31
  ]
r5py/util/classpath.py CHANGED
@@ -17,8 +17,10 @@ from .warnings import R5pyWarning
17
17
 
18
18
 
19
19
  # update these to use a newer R5 version if no R5 available locally
20
- R5_JAR_URL = "https://github.com/conveyal/r5/releases/download/v6.9/r5-v6.9-all.jar"
21
- R5_JAR_SHA256 = "a7e1c5ff8786a9fb9191073b8f31a6933b862f44b9ff85b2c00a68c85491274d"
20
+ R5_JAR_URL = (
21
+ "https://github.com/r5py/r5/releases/download/v7.3-r5py/r5-v7.3-r5py-all.jar"
22
+ )
23
+ R5_JAR_SHA256 = "cb1ccad370757ba229cf17f1bedc9549ff5d77fdbb44b7a3058104fe1f243f53"
22
24
  # ---
23
25
 
24
26
 
@@ -73,9 +75,11 @@ def find_r5_classpath(arguments):
73
75
  "Could not find R5 jar, trying to download it from upstream",
74
76
  R5pyWarning,
75
77
  )
76
- with ValidatingRequestsSession() as session, session.get(
77
- R5_JAR_URL, R5_JAR_SHA256
78
- ) as response, open(r5_classpath, "wb") as jar:
78
+ with (
79
+ ValidatingRequestsSession() as session,
80
+ session.get(R5_JAR_URL, R5_JAR_SHA256) as response,
81
+ open(r5_classpath, "wb") as jar,
82
+ ):
79
83
  jar.write(response.content)
80
84
  if arguments.verbose:
81
85
  warnings.warn(