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.
- r5py/__init__.py +27 -0
- r5py/__main__.py +3 -0
- r5py/r5/__init__.py +39 -0
- r5py/r5/access_leg.py +12 -0
- r5py/r5/base_travel_time_matrix.py +255 -0
- r5py/r5/detailed_itineraries.py +226 -0
- r5py/r5/direct_leg.py +38 -0
- r5py/r5/egress_leg.py +12 -0
- r5py/r5/elevation_cost_function.py +50 -0
- r5py/r5/elevation_model.py +89 -0
- r5py/r5/file_storage.py +82 -0
- r5py/r5/isochrones.py +345 -0
- r5py/r5/regional_task.py +600 -0
- r5py/r5/scenario.py +36 -0
- r5py/r5/street_layer.py +90 -0
- r5py/r5/street_segment.py +39 -0
- r5py/r5/transfer_leg.py +12 -0
- r5py/r5/transit_layer.py +87 -0
- r5py/r5/transit_leg.py +12 -0
- r5py/r5/transport_mode.py +148 -0
- r5py/r5/transport_network.py +299 -0
- r5py/r5/travel_time_matrix.py +186 -0
- r5py/r5/trip.py +97 -0
- r5py/r5/trip_leg.py +204 -0
- r5py/r5/trip_planner.py +576 -0
- r5py/util/__init__.py +31 -0
- r5py/util/camel_to_snake_case.py +25 -0
- r5py/util/classpath.py +95 -0
- r5py/util/config.py +176 -0
- r5py/util/contains_gtfs_data.py +46 -0
- r5py/util/data_validation.py +28 -0
- r5py/util/environment.py +32 -0
- r5py/util/exceptions.py +43 -0
- r5py/util/file_digest.py +40 -0
- r5py/util/good_enough_equidistant_crs.py +73 -0
- r5py/util/jvm.py +138 -0
- r5py/util/memory_footprint.py +178 -0
- r5py/util/parse_int_date.py +24 -0
- r5py/util/sample_data_set.py +76 -0
- r5py/util/snake_to_camel_case.py +16 -0
- r5py/util/spatially_clustered_geodataframe.py +66 -0
- r5py/util/validating_requests_session.py +58 -0
- r5py/util/warnings.py +7 -0
- r5py/util/working_copy.py +42 -0
- r5py-1.1.0.dist-info/METADATA +176 -0
- r5py-1.1.0.dist-info/RECORD +49 -0
- r5py-1.1.0.dist-info/WHEEL +5 -0
- r5py-1.1.0.dist-info/licenses/LICENSE +3 -0
- r5py-1.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""Determine a reasonable memory footprint for the Java virtual machine."""
|
|
4
|
+
|
|
5
|
+
import psutil
|
|
6
|
+
import re
|
|
7
|
+
import warnings
|
|
8
|
+
|
|
9
|
+
from .config import Config
|
|
10
|
+
from .warnings import R5pyWarning
|
|
11
|
+
|
|
12
|
+
__all__ = ["MAX_JVM_MEMORY"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
ABSOLUTE_MINIMUM_MEMORY = 200 * 1024**2 # never grant less than 200 MiB to JVM
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
config = Config()
|
|
19
|
+
config.argparser.add(
|
|
20
|
+
"-m",
|
|
21
|
+
"--max-memory",
|
|
22
|
+
help="""
|
|
23
|
+
Memory limit for the JVM running R5.
|
|
24
|
+
|
|
25
|
+
Use % as a suffix to specify a share of total RAM;
|
|
26
|
+
K, M, G, T to specify KiB, MiB, GiB, or TiB, respectively.
|
|
27
|
+
Values without suffix are interpreted as bytes.
|
|
28
|
+
""",
|
|
29
|
+
default="80%",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _share_of_ram(share=0.8, leave_at_least=(2 * 1024**3)):
|
|
34
|
+
"""
|
|
35
|
+
Calculate a share of total RAM.
|
|
36
|
+
|
|
37
|
+
Arguments
|
|
38
|
+
---------
|
|
39
|
+
share : float
|
|
40
|
+
Which portion of total RAM to return.
|
|
41
|
+
Default: 0.8
|
|
42
|
+
leave_at_least : float
|
|
43
|
+
How much RAM (in bytes) to leave (for other applications and system) in
|
|
44
|
+
any case. If `total RAM - (total RAM ⨉ share)` is smaller than
|
|
45
|
+
`leave_at_least`, return `total RAM - leave_at_least`, instead.
|
|
46
|
+
Default: 2GiB
|
|
47
|
+
|
|
48
|
+
Returns
|
|
49
|
+
-------
|
|
50
|
+
int
|
|
51
|
+
A value in bytes that is close to `share` portion of total RAM.
|
|
52
|
+
"""
|
|
53
|
+
total_ram = psutil.virtual_memory().total
|
|
54
|
+
if total_ram * (1.0 - share) > leave_at_least:
|
|
55
|
+
share_of_ram = round(share * total_ram)
|
|
56
|
+
else:
|
|
57
|
+
share_of_ram = round(total_ram - leave_at_least)
|
|
58
|
+
return share_of_ram
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _parse_value_and_unit(value_and_unit, max_unit_length=1):
|
|
62
|
+
"""
|
|
63
|
+
Extract value and unit from a string.
|
|
64
|
+
|
|
65
|
+
The string is allowed to contain a (non-numeric) unit suffix.
|
|
66
|
+
For instance, input values of `'1M'` or `3.732G` would yield return
|
|
67
|
+
values `(1, 'M')` or `(3.732, 'G')`, respectively.
|
|
68
|
+
|
|
69
|
+
Arguments
|
|
70
|
+
---------
|
|
71
|
+
value_and_unit : str
|
|
72
|
+
Input string (typically passed-through from config
|
|
73
|
+
parameter `--max-memory`).
|
|
74
|
+
max_unit_length : int
|
|
75
|
+
Maximum length in characters of the unit suffix.
|
|
76
|
+
Default: 1
|
|
77
|
+
|
|
78
|
+
Returns
|
|
79
|
+
-------
|
|
80
|
+
tuple: a tuple containing
|
|
81
|
+
- value (float): The value extracted from `value_and_unit`.
|
|
82
|
+
- unit (str): The unit extracted from `value_and_unit`.
|
|
83
|
+
"""
|
|
84
|
+
matches = re.match(
|
|
85
|
+
(
|
|
86
|
+
"^(?P<value>[0-9]+(\\.[0-9]+)?)" # value
|
|
87
|
+
f"(?P<unit>[^0-9]){{0,{max_unit_length}}}$" # unit
|
|
88
|
+
),
|
|
89
|
+
value_and_unit,
|
|
90
|
+
)
|
|
91
|
+
value = float(matches["value"])
|
|
92
|
+
unit = matches["unit"]
|
|
93
|
+
|
|
94
|
+
return value, unit
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _interpret_power_of_two_units(value, unit):
|
|
98
|
+
"""
|
|
99
|
+
Convert a value given as value and power-of-two unit into a bytes value.
|
|
100
|
+
|
|
101
|
+
Arguments
|
|
102
|
+
---------
|
|
103
|
+
value : float
|
|
104
|
+
Input value.
|
|
105
|
+
unit : str
|
|
106
|
+
Unit suffix, as specified in IEC 80000-13, e.g., 'K' for kibibyte.
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
int:
|
|
111
|
+
interpreted value in bytes
|
|
112
|
+
"""
|
|
113
|
+
# the position of each suffix in this string is the unit’s exponent
|
|
114
|
+
# over 1024.
|
|
115
|
+
# Compare https://en.wikipedia.org/wiki/
|
|
116
|
+
# ISO%2FIEC_80000#Part_13:_Information_science_and_technology
|
|
117
|
+
SUFFIXES = " KMGTPEZY"
|
|
118
|
+
|
|
119
|
+
if unit is None:
|
|
120
|
+
unit = " "
|
|
121
|
+
|
|
122
|
+
if unit not in SUFFIXES:
|
|
123
|
+
raise ValueError(
|
|
124
|
+
f"Could not interpret unit '{unit}'. "
|
|
125
|
+
"Allowed suffixes are 'K', 'M', 'G', 'T', 'P', 'E', 'Z', and 'Y'."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
exponent = SUFFIXES.find(unit)
|
|
129
|
+
value *= 1024**exponent
|
|
130
|
+
|
|
131
|
+
return value
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _get_max_memory(max_memory):
|
|
135
|
+
"""
|
|
136
|
+
Interpret the config parameter --max-memory.
|
|
137
|
+
|
|
138
|
+
Arguments
|
|
139
|
+
---------
|
|
140
|
+
|
|
141
|
+
max_memory : str
|
|
142
|
+
Memory limit for the JVM running R5.
|
|
143
|
+
|
|
144
|
+
Use % as a suffix to specify a share of total RAM;
|
|
145
|
+
K, M, G, T suffix specify KiB, MiB, GiB, or TiB, respectively.
|
|
146
|
+
Values without suffix are interpreted as bytes.
|
|
147
|
+
|
|
148
|
+
Returns
|
|
149
|
+
-------
|
|
150
|
+
int
|
|
151
|
+
Maximum amount of memory allocated for R5 in bytes.
|
|
152
|
+
"""
|
|
153
|
+
try:
|
|
154
|
+
value, unit = _parse_value_and_unit(max_memory)
|
|
155
|
+
except TypeError:
|
|
156
|
+
raise ValueError(f"Could not interpret `--max-memory` ('{max_memory}').")
|
|
157
|
+
|
|
158
|
+
if unit == "%":
|
|
159
|
+
value = _share_of_ram(share=(value / 100.0))
|
|
160
|
+
else:
|
|
161
|
+
# convert to bytes
|
|
162
|
+
value = _interpret_power_of_two_units(value, unit)
|
|
163
|
+
|
|
164
|
+
max_memory = round(value)
|
|
165
|
+
|
|
166
|
+
if max_memory < ABSOLUTE_MINIMUM_MEMORY:
|
|
167
|
+
max_memory = ABSOLUTE_MINIMUM_MEMORY
|
|
168
|
+
warnings.warn(
|
|
169
|
+
f"Requested maximum JVM heap size is too low for R5, "
|
|
170
|
+
f"setting to minimum value {ABSOLUTE_MINIMUM_MEMORY:d} bytes.",
|
|
171
|
+
R5pyWarning,
|
|
172
|
+
stacklevel=1,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
return max_memory
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
MAX_JVM_MEMORY = _get_max_memory(config.arguments.max_memory)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""Parse an integer date value."""
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
|
|
7
|
+
__all__ = ["parse_int_date"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parse_int_date(int_date):
|
|
11
|
+
"""
|
|
12
|
+
Parse an integer date value.
|
|
13
|
+
|
|
14
|
+
com.conveyal.gtfs.GtfsFeed.services.start_date and .end_date are integer
|
|
15
|
+
values in the format YYYYMMDD, so, e.g., 20220222 for 2 February 2022.
|
|
16
|
+
This function converts such an integer value into the respective
|
|
17
|
+
datetime.datetime (at 0:00 o’clock).
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
year = round(int_date / 10000)
|
|
21
|
+
month = round((int_date % 10000) / 100)
|
|
22
|
+
day = int_date % 100
|
|
23
|
+
|
|
24
|
+
return datetime.datetime(year, month, day)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
"""A remote data set that is downloaded on demand."""
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import pathlib
|
|
8
|
+
import warnings
|
|
9
|
+
|
|
10
|
+
from .config import Config
|
|
11
|
+
from .validating_requests_session import ValidatingRequestsSession
|
|
12
|
+
|
|
13
|
+
config = Config()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SampleDataSet(pathlib.Path):
|
|
17
|
+
"""Data set that is downloaded and cached as needed."""
|
|
18
|
+
|
|
19
|
+
# decide which kind of pathlib.Path we are (Windows, Unix, ...)
|
|
20
|
+
# cf. https://stackoverflow.com/a/66613346/463864
|
|
21
|
+
try:
|
|
22
|
+
_flavour = type(pathlib.Path())._flavour
|
|
23
|
+
except AttributeError: # Python>=3.13
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
_CACHE_DIR = pathlib.Path(config.CACHE_DIR) / "sampledata"
|
|
27
|
+
|
|
28
|
+
def __new__(cls, remote_url, sha256_checksum):
|
|
29
|
+
"""Define a data set that is downloaded and cached on demand."""
|
|
30
|
+
# pathlib.Path does everything in __new__, rather than __init__
|
|
31
|
+
cached_path = cls._CACHE_DIR / pathlib.Path(remote_url).name
|
|
32
|
+
return super().__new__(cls, cached_path)
|
|
33
|
+
|
|
34
|
+
def __init__(self, remote_url, sha256_checksum, *args, **kwargs):
|
|
35
|
+
"""
|
|
36
|
+
Define a data set that is downloaded and cached on demand.
|
|
37
|
+
|
|
38
|
+
Arguments
|
|
39
|
+
---------
|
|
40
|
+
remote_url : str
|
|
41
|
+
source URL for this data set
|
|
42
|
+
sha256_checksum : str
|
|
43
|
+
checksum for this data set, using an SHA256 algorithm
|
|
44
|
+
"""
|
|
45
|
+
cached_path = self._CACHE_DIR / pathlib.Path(remote_url).name
|
|
46
|
+
|
|
47
|
+
try: # Python>=3.12
|
|
48
|
+
super().__init__(cached_path)
|
|
49
|
+
except TypeError:
|
|
50
|
+
super().__init__()
|
|
51
|
+
|
|
52
|
+
self.remote_url = remote_url
|
|
53
|
+
self.checksum = sha256_checksum
|
|
54
|
+
self.cached_path = cached_path
|
|
55
|
+
self._download_remote_file()
|
|
56
|
+
|
|
57
|
+
def _download_remote_file(self):
|
|
58
|
+
try:
|
|
59
|
+
assert (
|
|
60
|
+
hashlib.sha256(self.cached_path.read_bytes()).hexdigest()
|
|
61
|
+
== self.checksum
|
|
62
|
+
)
|
|
63
|
+
except (AssertionError, FileNotFoundError):
|
|
64
|
+
if config.arguments.verbose:
|
|
65
|
+
warnings.warn(
|
|
66
|
+
f"First access to {pathlib.Path(self.remote_url).name}, "
|
|
67
|
+
"downloading remote file to local cache",
|
|
68
|
+
RuntimeWarning,
|
|
69
|
+
stacklevel=1,
|
|
70
|
+
)
|
|
71
|
+
self.cached_path.parent.mkdir(exist_ok=True)
|
|
72
|
+
with (
|
|
73
|
+
ValidatingRequestsSession() as session,
|
|
74
|
+
session.get(self.remote_url, self.checksum) as response,
|
|
75
|
+
):
|
|
76
|
+
self.cached_path.write_bytes(response.content)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""Convert a snack_case formated string to a camelCase format."""
|
|
4
|
+
|
|
5
|
+
__all__ = ["snake_to_camel_case"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def snake_to_camel_case(snake_case):
|
|
9
|
+
"""Convert `snake_case` to camelCase spelling."""
|
|
10
|
+
if "_" in snake_case:
|
|
11
|
+
words = snake_case.split("_")
|
|
12
|
+
words = [words[0].lower()] + [word.title() for word in words[1:]]
|
|
13
|
+
camel_case = "".join(words)
|
|
14
|
+
else:
|
|
15
|
+
camel_case = snake_case[0].lower() + snake_case[1:]
|
|
16
|
+
return camel_case
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
"""Assign a cluster label column to a point-geometry GeoDataFrame."""
|
|
5
|
+
|
|
6
|
+
import warnings
|
|
7
|
+
|
|
8
|
+
import geopandas
|
|
9
|
+
import numpy
|
|
10
|
+
import shapely
|
|
11
|
+
import sklearn.cluster
|
|
12
|
+
|
|
13
|
+
from .good_enough_equidistant_crs import GoodEnoughEquidistantCrs
|
|
14
|
+
|
|
15
|
+
__all__ = ["SpatiallyClusteredGeoDataFrame"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SpatiallyClusteredGeoDataFrame:
|
|
19
|
+
"""Assign a cluster label column to a point-geometry GeoDataFrame."""
|
|
20
|
+
|
|
21
|
+
def __new__(cls, data, eps=200.0, min_cluster_size=3):
|
|
22
|
+
"""
|
|
23
|
+
Assign a cluster label column to a point-geometry GeoDataFrame.
|
|
24
|
+
|
|
25
|
+
Arguments:
|
|
26
|
+
----------
|
|
27
|
+
data : geopandas.GeoDataFrame
|
|
28
|
+
input data set
|
|
29
|
+
eps : int | float
|
|
30
|
+
EPS parameter to a DBSCAN cluster algorithm, the maximum
|
|
31
|
+
intra-cluster distance between two points
|
|
32
|
+
min_cluster_size : int
|
|
33
|
+
Do not form clusters with less members
|
|
34
|
+
"""
|
|
35
|
+
data = geopandas.GeoDataFrame(data)
|
|
36
|
+
|
|
37
|
+
EQUIDISTANT_CRS = GoodEnoughEquidistantCrs(
|
|
38
|
+
shapely.box(*data.to_crs("EPSG:4326").geometry.total_bounds)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# loosely based on:
|
|
42
|
+
# https://github.com/geopandas/scipy2018-geospatial-data/blob/master/08-clustering.ipynb
|
|
43
|
+
|
|
44
|
+
coordinates = numpy.vstack(
|
|
45
|
+
data.to_crs(EQUIDISTANT_CRS)["geometry"]
|
|
46
|
+
.apply(lambda geometry: numpy.hstack(geometry.xy))
|
|
47
|
+
.values
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
with warnings.catch_warnings():
|
|
51
|
+
warnings.filterwarnings(
|
|
52
|
+
"ignore",
|
|
53
|
+
"Could not find the number of physical cores",
|
|
54
|
+
category=UserWarning,
|
|
55
|
+
)
|
|
56
|
+
data["cluster"] = (
|
|
57
|
+
sklearn.cluster.DBSCAN(
|
|
58
|
+
eps=eps,
|
|
59
|
+
min_samples=min_cluster_size,
|
|
60
|
+
n_jobs=-1,
|
|
61
|
+
)
|
|
62
|
+
.fit(coordinates)
|
|
63
|
+
.labels_
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return data
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
"""Extends requests.Session to enable simple checksum testing."""
|
|
4
|
+
|
|
5
|
+
__all__ = ["ValidatingRequestsSession"]
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from .exceptions import ChecksumFailed
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ValidatingRequestsSession(requests.Session):
|
|
15
|
+
"""Download a file and test whether it matches a checksum."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, *args, checksum_algorithm=hashlib.sha256, **kwargs):
|
|
18
|
+
"""
|
|
19
|
+
Download a file and test whether it matches a checksum.
|
|
20
|
+
|
|
21
|
+
Arguments
|
|
22
|
+
---------
|
|
23
|
+
checksum_algorithm : function
|
|
24
|
+
algorithm to use to create checksum of downloaded file,
|
|
25
|
+
default: hashlib.sha256
|
|
26
|
+
*args, **kwargs
|
|
27
|
+
any argument accepted by `requests.Session`
|
|
28
|
+
"""
|
|
29
|
+
super().__init__(*args, **kwargs)
|
|
30
|
+
self._algorithm = checksum_algorithm
|
|
31
|
+
|
|
32
|
+
def get(self, url, checksum, **kwargs):
|
|
33
|
+
"""Send a GET request, tests checksum."""
|
|
34
|
+
kwargs.setdefault("allow_redirects", True)
|
|
35
|
+
return self.request("GET", url, checksum, **kwargs)
|
|
36
|
+
|
|
37
|
+
def post(self, url, checksum, **kwargs):
|
|
38
|
+
"""Send a POST request, tests checksum."""
|
|
39
|
+
return self.request("POST", url, checksum, **kwargs)
|
|
40
|
+
|
|
41
|
+
# delete, put, head don’t return data,
|
|
42
|
+
# testing checksums does not apply
|
|
43
|
+
|
|
44
|
+
def request(self, method, url, checksum, **kwargs):
|
|
45
|
+
"""
|
|
46
|
+
Retrieve file from cache or proxy requests.request.
|
|
47
|
+
|
|
48
|
+
Raise `ChecksumFailed` if the file’s digest and `checksum` differ.
|
|
49
|
+
"""
|
|
50
|
+
response = super().request(method, url, **kwargs)
|
|
51
|
+
digest = self._algorithm(response.content).hexdigest()
|
|
52
|
+
|
|
53
|
+
if digest != checksum:
|
|
54
|
+
raise ChecksumFailed(
|
|
55
|
+
f"Checksum failed for {url}, expected {checksum}, got {digest}"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return response
|
r5py/util/warnings.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""Create a copy or link of an input file in a cache directory."""
|
|
4
|
+
|
|
5
|
+
import filelock
|
|
6
|
+
import pathlib
|
|
7
|
+
import shutil
|
|
8
|
+
|
|
9
|
+
from .config import Config
|
|
10
|
+
|
|
11
|
+
__all__ = ["WorkingCopy"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class WorkingCopy(pathlib.Path):
|
|
15
|
+
"""Create a copy or link of an input file in a cache directory."""
|
|
16
|
+
|
|
17
|
+
def __new__(cls, path):
|
|
18
|
+
"""
|
|
19
|
+
Create a copy or link of an input file in a cache directory.
|
|
20
|
+
|
|
21
|
+
This exists because R5 creates temporary files in the directory of input
|
|
22
|
+
files. This can not only be annoying clutter, but also create problems
|
|
23
|
+
of concurrency, performance, etc., for instance, when the data comes
|
|
24
|
+
from a shared network drive or a read-only file system.
|
|
25
|
+
|
|
26
|
+
Arguments
|
|
27
|
+
---------
|
|
28
|
+
path : str or pathlib.Path
|
|
29
|
+
The file to create a copy or link of in a cache directory
|
|
30
|
+
"""
|
|
31
|
+
# try to first create a symbolic link, if that fails (e.g., on Windows),
|
|
32
|
+
# copy the file to a cache directory
|
|
33
|
+
path = pathlib.Path(path).absolute()
|
|
34
|
+
destination = pathlib.Path(Config().CACHE_DIR / path.name).absolute()
|
|
35
|
+
|
|
36
|
+
with filelock.FileLock(destination.parent / f"{destination.name}.lock"):
|
|
37
|
+
if not destination.exists():
|
|
38
|
+
try:
|
|
39
|
+
destination.symlink_to(path)
|
|
40
|
+
except OSError:
|
|
41
|
+
shutil.copyfile(f"{path}", f"{destination}")
|
|
42
|
+
return destination
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: r5py
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Python wrapper for the R5 routing analysis engine
|
|
5
|
+
Author: Christoph Fink, Willem Klumpenhouwer, Marcus Sairava, Rafael Pereira, Henrikki Tenkanen
|
|
6
|
+
License: GPL-3.0-or-later or MIT
|
|
7
|
+
Project-URL: Documentation, https://r5py.readthedocs.org/
|
|
8
|
+
Project-URL: Repository, https://github.com/r5py/r5py.git
|
|
9
|
+
Project-URL: Change log, https://github.com/r5py/r5py/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Bug tracker, https://github.com/r5py/r5py/issues
|
|
11
|
+
Keywords: accessibility,transport,routing,research
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: ConfigArgParse
|
|
20
|
+
Requires-Dist: filelock
|
|
21
|
+
Requires-Dist: geohexgrid
|
|
22
|
+
Requires-Dist: geopandas
|
|
23
|
+
Requires-Dist: joblib
|
|
24
|
+
Requires-Dist: jpype1
|
|
25
|
+
Requires-Dist: numpy
|
|
26
|
+
Requires-Dist: pandas
|
|
27
|
+
Requires-Dist: psutil
|
|
28
|
+
Requires-Dist: pyproj
|
|
29
|
+
Requires-Dist: rasterio
|
|
30
|
+
Requires-Dist: requests
|
|
31
|
+
Requires-Dist: scikit-learn
|
|
32
|
+
Requires-Dist: shapely
|
|
33
|
+
Requires-Dist: simplification
|
|
34
|
+
Requires-Dist: typing_extensions; python_version < "3.13"
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: black; extra == "dev"
|
|
37
|
+
Requires-Dist: flake8; extra == "dev"
|
|
38
|
+
Requires-Dist: flake8-bugbear; extra == "dev"
|
|
39
|
+
Requires-Dist: flake8-pyproject; extra == "dev"
|
|
40
|
+
Requires-Dist: pydocstyle; extra == "dev"
|
|
41
|
+
Requires-Dist: pylint; extra == "dev"
|
|
42
|
+
Provides-Extra: docs
|
|
43
|
+
Requires-Dist: contextily; extra == "docs"
|
|
44
|
+
Requires-Dist: folium; extra == "docs"
|
|
45
|
+
Requires-Dist: GitPython; extra == "docs"
|
|
46
|
+
Requires-Dist: h3; extra == "docs"
|
|
47
|
+
Requires-Dist: jupyterlab_myst; extra == "docs"
|
|
48
|
+
Requires-Dist: mapclassify; extra == "docs"
|
|
49
|
+
Requires-Dist: matplotlib; extra == "docs"
|
|
50
|
+
Requires-Dist: myst-nb; extra == "docs"
|
|
51
|
+
Requires-Dist: nbsphinx; extra == "docs"
|
|
52
|
+
Requires-Dist: pybtex-apa7-style; extra == "docs"
|
|
53
|
+
Requires-Dist: r5py.sampledata.helsinki; extra == "docs"
|
|
54
|
+
Requires-Dist: r5py.sampledata.sao_paulo; extra == "docs"
|
|
55
|
+
Requires-Dist: shapely; extra == "docs"
|
|
56
|
+
Requires-Dist: sphinx; extra == "docs"
|
|
57
|
+
Requires-Dist: sphinx-book-theme; extra == "docs"
|
|
58
|
+
Requires-Dist: sphinx-design; extra == "docs"
|
|
59
|
+
Requires-Dist: sphinxcontrib-bibtex; extra == "docs"
|
|
60
|
+
Requires-Dist: sphinxcontrib-images; extra == "docs"
|
|
61
|
+
Provides-Extra: tests
|
|
62
|
+
Requires-Dist: pyarrow; extra == "tests"
|
|
63
|
+
Requires-Dist: pytest; extra == "tests"
|
|
64
|
+
Requires-Dist: pytest-cov; extra == "tests"
|
|
65
|
+
Requires-Dist: pytest-lazy-fixtures; extra == "tests"
|
|
66
|
+
Requires-Dist: r5py.sampledata.helsinki; extra == "tests"
|
|
67
|
+
Requires-Dist: r5py.sampledata.sao_paulo; extra == "tests"
|
|
68
|
+
Requires-Dist: typing-extensions; extra == "tests"
|
|
69
|
+
Dynamic: license-file
|
|
70
|
+
|
|
71
|
+
<img class="r5py_logo" align="right" src="https://github.com/r5py/r5py/raw/main/docs/_static/images/r5py_blue.svg" alt="r5py logo" style="width:180px; max-width:30vW;">
|
|
72
|
+
|
|
73
|
+
# r5py: Rapid Realistic Routing with R5 in Python
|
|
74
|
+
|
|
75
|
+
<!-- badges -->
|
|
76
|
+
[![Try r5py with binder][binder-badge]][binder-link]
|
|
77
|
+
[![DOI][doi-badge]][doi-link]
|
|
78
|
+
<br />
|
|
79
|
+
[![stable version][stable-version-badge]][stable-version-link]
|
|
80
|
+
[![downloads (pypi)][downloads-pypi-badge]][downloads-pypi-link]
|
|
81
|
+
[![downloads (conda-forge)][downloads-conda-forge-badge]][downloads-conda-forge-link]
|
|
82
|
+
<br />
|
|
83
|
+
[![Unit tests][test-status-badge]][test-status-link]
|
|
84
|
+
[![Documentation Status][rtd-status-badge]][rtd-status-link]
|
|
85
|
+
[![Coverage][coverage-badge]][coverage-link]
|
|
86
|
+
<br />
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
**R5py** is a Python library for rapid realistic routing on multimodal transport
|
|
90
|
+
networks (walk, bike, public transport and car). It provides a simple and
|
|
91
|
+
friendly interface to R<sup>5</sup>, the Rapid Realistic Routing on Real-world
|
|
92
|
+
and Reimagined networks, the [routing engine][r5-github] developed by Conveyal.
|
|
93
|
+
**r5py** is inspired by [r5r, a wrapper for R][r5r-vignette], and the library is
|
|
94
|
+
designed to interact with [GeoPandas][geopandas] GeoDataFrames.
|
|
95
|
+
|
|
96
|
+
**R5py** offers a simple way to run R5 locally with Python. It allows users to
|
|
97
|
+
calculate travel time matrices and accessibility by different travel modes. To
|
|
98
|
+
get started, see a detailed demonstration of the **r5py** ‘in action’ from the
|
|
99
|
+
[Usage][rtd-quickstart] section of its documentation. Over time, **r5py** will
|
|
100
|
+
be expanded to incorporate other functionalities from R5.
|
|
101
|
+
|
|
102
|
+
## Installation
|
|
103
|
+
|
|
104
|
+
**R5py** is available from conda-forge and PyPi. You can use `mamba`, `pip` or
|
|
105
|
+
`conda` to install it. To quickstart your use of **r5py**, we also provide an
|
|
106
|
+
[`environment.yml` file ][env-file], using which you can [quickly set up a
|
|
107
|
+
development environment][conda-create-env-from-yml] and are ready to go.
|
|
108
|
+
|
|
109
|
+
For more details and alternative installation options, read the dedicated
|
|
110
|
+
[installation section][rtd-installation] of the r5py documentation.
|
|
111
|
+
|
|
112
|
+
## Usage
|
|
113
|
+
|
|
114
|
+
You can find detailed installation instructions, example code, documentation and
|
|
115
|
+
API reference at [r5py.readthedocs.io][rtd-link].
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
## Acknowledgements
|
|
119
|
+
|
|
120
|
+
The [R<sup>5</sup> routing engine][r5-github] is developed at
|
|
121
|
+
[Conveyal][conveyal] with contributions from several people.
|
|
122
|
+
|
|
123
|
+
R5py draws a lot of inspiration from [r5r][r5r-github], an interface to R5 from
|
|
124
|
+
the R language that is developed at the Institute for Applied Economic Research
|
|
125
|
+
(Ipea), Brazil.
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
## Citation
|
|
129
|
+
|
|
130
|
+
If you use *r5py* for scientific research, please cite it in your publications:
|
|
131
|
+
|
|
132
|
+
Fink, C., Klumpenhouwer, W., Saraiva, M., Pereira, R., & Tenkanen, H., 2022:
|
|
133
|
+
*r5py: Rapid Realistic Routing with R5 in Python*.
|
|
134
|
+
[DOI:10.5281/zenodo.7060437][doi-link]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
This work is dual-licensed under GNU General Public License v3.0 or later and
|
|
140
|
+
MIT License. You can choose between the two depending on which license fits
|
|
141
|
+
your project better.
|
|
142
|
+
|
|
143
|
+
`SPDX-License-Identifier: GPL-3.0-or-later OR MIT`
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
<!-- links used throughout the document -->
|
|
147
|
+
|
|
148
|
+
<!-- (1) badges -->
|
|
149
|
+
[binder-badge]: https://img.shields.io/badge/Try%20r5py%20with-binder-F5A252.svg?logo=data:image/png;base64,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
|
|
150
|
+
[binder-link]: https://mybinder.org/v2/gh/r5py/r5py/stable?urlpath=tree/docs/user-guide/user-manual/quickstart.md
|
|
151
|
+
[coverage-badge]: https://codecov.io/gh/r5py/r5py/branch/main/graph/badge.svg?token=WG8RBMZBK6
|
|
152
|
+
[coverage-link]: https://codecov.io/gh/r5py/r5py
|
|
153
|
+
[doi-badge]: https://zenodo.org/badge/DOI/10.5281/zenodo.7060437.svg
|
|
154
|
+
[doi-link]: https://doi.org/10.5281/zenodo.7060437
|
|
155
|
+
[downloads-conda-forge-badge]: https://img.shields.io/conda/dn/conda-forge/r5py?label=Downloads%20%28conda-forge%29
|
|
156
|
+
[downloads-conda-forge-link]: https://anaconda.org/conda-forge/r5py
|
|
157
|
+
[downloads-pypi-badge]: https://static.pepy.tech/personalized-badge/r5py?period=total&units=international_system&left_color=grey&right_color=orange&left_text=Downloads%20(pypi)
|
|
158
|
+
[downloads-pypi-link]: https://pypi.org/project/r5py/
|
|
159
|
+
[rtd-status-badge]: https://readthedocs.org/projects/r5py/badge/?version=stable
|
|
160
|
+
[rtd-status-link]: https://r5py.readthedocs.io/
|
|
161
|
+
[stable-version-badge]: https://img.shields.io/pypi/v/r5py?label=Stable
|
|
162
|
+
[stable-version-link]: https://github.com/r5py/r5py/releases
|
|
163
|
+
[test-status-badge]: https://github.com/r5py/r5py/actions/workflows/test.yml/badge.svg
|
|
164
|
+
[test-status-link]: https://github.com/r5py/r5py/actions/workflows/test.yml
|
|
165
|
+
|
|
166
|
+
<!-- (2) other links -->
|
|
167
|
+
[conda-create-env-from-yml]: https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#creating-an-environment-from-an-environment-yml-file
|
|
168
|
+
[conveyal]: https://www.conveyal.com/
|
|
169
|
+
[env-file]: https://github.com/r5py/r5py/blob/main/ci/r5py.yaml
|
|
170
|
+
[geopandas]: https://geopandas.org/
|
|
171
|
+
[r5-github]: https://github.com/conveyal/r5/
|
|
172
|
+
[r5r-github]: https://github.com/ipeaGIT/r5r/
|
|
173
|
+
[r5r-vignette]: https://ipeagit.github.io/r5r/
|
|
174
|
+
[rtd-quickstart]: https://r5py.readthedocs.io/stable/user-guide/user-manual/quickstart.html
|
|
175
|
+
[rtd-installation]: https://r5py.readthedocs.io/stable/user-guide/installation/installation.html
|
|
176
|
+
[rtd-link]: https://r5py.readthedocs.io/
|