Repository: mobidata-bw/ipl-proxy
Pull request #169: converter to set return_constraint
Author: login: ThorstenFroehlinghaus
Created: 2025-05-02T14:16:15Z | Merged: 2025-05-06T05:06:58Z

transitous.org asked us to set return_constraint=any_station für RegioRad Stuttgart because this attribute is required for their intermodeal routing. the ipl-proxy has been developed such, that return_constraint can be set also for other operators.


Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 749f40d..31edea0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,9 @@
 
 The changelog lists relevant feature changes between each release. Search GitHub issues and pull requests for smaller issues.
 
+## 2025-05-05:
+- add converter for all feeds: set return_constraint if not defined
+
 ## 2025-04-09:
 - add converter for all feeds: override ttl=0
 
diff --git a/app/converters/gbfs_https_to_http.py b/app/converters/gbfs_https_to_http.py
index de28a5f..c78a66f 100644
--- a/app/converters/gbfs_https_to_http.py
+++ b/app/converters/gbfs_https_to_http.py
@@ -19,6 +19,8 @@ class GbfsHttpsToHttpConverter(BaseConverter):
         'gbfs.prod.sharedmobility.ch',
         'api.voiapp.io',
         'gbfs.api.ridedott.com',
+        'zeus.city',
+        'yoio.rideatom.com',
     ]
 
     def convert(self, data: Union[dict, list], path: str) -> Union[dict, list]:
diff --git a/app/converters/gbfs_set_return_constraint.py b/app/converters/gbfs_set_return_constraint.py
new file mode 100644
index 0000000..bf0cbd8
--- /dev/null
+++ b/app/converters/gbfs_set_return_constraint.py
@@ -0,0 +1,46 @@
+from typing import Union
+
+from app.base_converter import BaseConverter
+
+
+class GbfsSetReturnConstraintConverter(BaseConverter):
+    hostnames = [
+        'gbfs.nextbike.net',
+        'apis.deutschebahn.com',
+        'stables.donkey.bike',
+        'data.lime.bike',
+        'mds.bird.co',
+        'gbfs.prod.sharedmobility.ch',
+        'api.voiapp.io',
+        'gbfs.api.ridedott.com',
+        'zeus.city',
+        'yoio.rideatom.com',
+    ]
+
+    # ensure that return_constraint is always set
+    # this attribute is necessary for intermodal routing
+    def convert(self, data: Union[dict, list], path: str) -> Union[dict, list]:
+        if not isinstance(data, dict):
+            return data
+
+        if path.endswith(('/vehicle_types', '/vehicle_types.json')):
+            fields = data.get('data')
+            if not isinstance(fields, dict):
+                return data
+            vehicle_types = fields.get('vehicle_types')
+            if not isinstance(vehicle_types, list):
+                return data
+            for vehicle_type in vehicle_types:
+                if 'return_constraint' in vehicle_type:
+                    continue
+                if vehicle_type.get('form_factor', '') == 'bicycle':
+                    vehicle_type['return_constraint'] = 'any_station'  # valid for most providers
+                if vehicle_type.get('form_factor', '') == 'scooter':
+                    vehicle_type['return_constraint'] = 'free_floating'  # valid for most providers
+                if path.startswith('/maps/gbfs/v2/nextbike_fg'):
+                    vehicle_type['return_constraint'] = 'hybrid'
+                if path.startswith('/public/v2/'):  # dott feed
+                    vehicle_type['return_constraint'] = 'hybrid'
+            return data
+
+        return data
diff --git a/config_dist_dev.yaml b/config_dist_dev.yaml
index aaaadb8..cc091bd 100644
--- a/config_dist_dev.yaml
+++ b/config_dist_dev.yaml
@@ -8,3 +8,5 @@ HTTP_TO_HTTPS_HOSTS:
   - gbfs.prod.sharedmobility.ch
   - api.voiapp.io
   - gbfs.api.ridedott.com
+  - zeus.city
+  - yoio.rideatom.com


Reviews:
user: login: hbruch
body: Ok. From code perspective, checking only for `/public/v2/` to recognize dott might break, when other providers use the same url pattern, so need to remember this!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-05T10:23:34Z
user: login: ThorstenFroehlinghaus
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T11:43:58Z
user: login: derhuerst
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T10:00:30Z
user: login: ThorstenFroehlinghaus
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-06T11:16:11Z

Review comments:
diff_hunk: @@ -0,0 +1,46 @@
+from typing import Union
+
+from app.base_converter import BaseConverter
+
+
+class GbfsSetReturnConstraintConverter(BaseConverter):
+    hostnames = [
+        'gbfs.nextbike.net',
+        'apis.deutschebahn.com',
+        'stables.donkey.bike',
+        'data.lime.bike',
+        'mds.bird.co',
+        'gbfs.prod.sharedmobility.ch',
+        'api.voiapp.io',
+        'gbfs.api.ridedott.com',
+        'zeus.city',
+        'yoio.rideatom.com',
+    ]
+
+    # ensure that return_constraint is always set
+    # this attribute is necessary for intermodal routing
+    def convert(self, data: Union[dict, list], path: str) -> Union[dict, list]:
+        if not isinstance(data, dict):
+            return data
+
+        if path.endswith(('/vehicle_types', '/vehicle_types.json')):
+            fields = data.get('data')
+            if not isinstance(fields, dict):
+                return data
+            vehicle_types = fields.get('vehicle_types')
+            if not isinstance(vehicle_types, list):
+                return data
+            for vehicle_type in vehicle_types:
+                if 'return_constraint' in vehicle_type:
+                    continue
+                if vehicle_type.get('form_factor', '') == 'bicycle':
+                    vehicle_type['return_constraint'] = 'any_station'  # valid for most providers
+                if vehicle_type.get('form_factor', '') == 'scooter':
+                    vehicle_type['return_constraint'] = 'free_floating'  # valid for most providers
+                if path.startswith('/maps/gbfs/v2/nextbike_fg'):
+                    vehicle_type['return_constraint'] = 'hybrid'
+                if path.startswith('/public/v2/'):  # dott feed
+                    vehicle_type['return_constraint'] = 'hybrid'
path: app/converters/gbfs_set_return_constraint.py
user: login: hbruch
body: Are we sure, that only dott uses this very generic url pattern?
created_at: 2025-05-05T09:53:45Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,46 @@
+from typing import Union
+
+from app.base_converter import BaseConverter
+
+
+class GbfsSetReturnConstraintConverter(BaseConverter):
+    hostnames = [
+        'gbfs.nextbike.net',
+        'apis.deutschebahn.com',
+        'stables.donkey.bike',
+        'data.lime.bike',
+        'mds.bird.co',
+        'gbfs.prod.sharedmobility.ch',
+        'api.voiapp.io',
+        'gbfs.api.ridedott.com',
+        'zeus.city',
+        'yoio.rideatom.com',
+    ]
+
+    # ensure that return_constraint is always set
+    # this attribute is necessary for intermodal routing
+    def convert(self, data: Union[dict, list], path: str) -> Union[dict, list]:
+        if not isinstance(data, dict):
+            return data
+
+        if path.endswith(('/vehicle_types', '/vehicle_types.json')):
+            fields = data.get('data')
+            if not isinstance(fields, dict):
+                return data
+            vehicle_types = fields.get('vehicle_types')
+            if not isinstance(vehicle_types, list):
+                return data
+            for vehicle_type in vehicle_types:
+                if 'return_constraint' in vehicle_type:
+                    continue
+                if vehicle_type.get('form_factor', '') == 'bicycle':
+                    vehicle_type['return_constraint'] = 'any_station'  # valid for most providers
+                if vehicle_type.get('form_factor', '') == 'scooter':
+                    vehicle_type['return_constraint'] = 'free_floating'  # valid for most providers
+                if path.startswith('/maps/gbfs/v2/nextbike_fg'):
+                    vehicle_type['return_constraint'] = 'hybrid'
+                if path.startswith('/public/v2/'):  # dott feed
+                    vehicle_type['return_constraint'] = 'hybrid'
path: app/converters/gbfs_set_return_constraint.py
user: login: ThorstenFroehlinghaus
body: Yes, I checked this in https://github.com/mobidata-bw/ipl-orchestration/blob/main/etc/lamassu/feedproviders.yml. However, it would make sense to extend the convert function such that also the current hostname is provided.
created_at: 2025-05-05T11:43:58Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,46 @@
+from typing import Union
+
+from app.base_converter import BaseConverter
+
+
+class GbfsSetReturnConstraintConverter(BaseConverter):
+    hostnames = [
+        'gbfs.nextbike.net',
+        'apis.deutschebahn.com',
+        'stables.donkey.bike',
+        'data.lime.bike',
+        'mds.bird.co',
+        'gbfs.prod.sharedmobility.ch',
+        'api.voiapp.io',
+        'gbfs.api.ridedott.com',
+        'zeus.city',
+        'yoio.rideatom.com',
+    ]
+
+    # ensure that return_constraint is always set
+    # this attribute is necessary for intermodal routing
+    def convert(self, data: Union[dict, list], path: str) -> Union[dict, list]:
+        if not isinstance(data, dict):
+            return data
+
+        if path.endswith(('/vehicle_types', '/vehicle_types.json')):
+            fields = data.get('data')
+            if not isinstance(fields, dict):
+                return data
+            vehicle_types = fields.get('vehicle_types')
+            if not isinstance(vehicle_types, list):
+                return data
+            for vehicle_type in vehicle_types:
+                if 'return_constraint' in vehicle_type:
+                    continue
+                if vehicle_type.get('form_factor', '') == 'bicycle':
+                    vehicle_type['return_constraint'] = 'any_station'  # valid for most providers
+                if vehicle_type.get('form_factor', '') == 'scooter':
+                    vehicle_type['return_constraint'] = 'free_floating'  # valid for most providers
+                if path.startswith('/maps/gbfs/v2/nextbike_fg'):
+                    vehicle_type['return_constraint'] = 'hybrid'
+                if path.startswith('/public/v2/'):  # dott feed
+                    vehicle_type['return_constraint'] = 'hybrid'
path: app/converters/gbfs_set_return_constraint.py
user: login: derhuerst
body: Is there really no other way to recognise dott here? Expecting only one provider to use `/public/v2/` will break things in a *very* subtle way.
created_at: 2025-05-06T10:00:29Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,46 @@
+from typing import Union
+
+from app.base_converter import BaseConverter
+
+
+class GbfsSetReturnConstraintConverter(BaseConverter):
+    hostnames = [
+        'gbfs.nextbike.net',
+        'apis.deutschebahn.com',
+        'stables.donkey.bike',
+        'data.lime.bike',
+        'mds.bird.co',
+        'gbfs.prod.sharedmobility.ch',
+        'api.voiapp.io',
+        'gbfs.api.ridedott.com',
+        'zeus.city',
+        'yoio.rideatom.com',
+    ]
+
+    # ensure that return_constraint is always set
+    # this attribute is necessary for intermodal routing
+    def convert(self, data: Union[dict, list], path: str) -> Union[dict, list]:
+        if not isinstance(data, dict):
+            return data
+
+        if path.endswith(('/vehicle_types', '/vehicle_types.json')):
+            fields = data.get('data')
+            if not isinstance(fields, dict):
+                return data
+            vehicle_types = fields.get('vehicle_types')
+            if not isinstance(vehicle_types, list):
+                return data
+            for vehicle_type in vehicle_types:
+                if 'return_constraint' in vehicle_type:
+                    continue
+                if vehicle_type.get('form_factor', '') == 'bicycle':
+                    vehicle_type['return_constraint'] = 'any_station'  # valid for most providers
+                if vehicle_type.get('form_factor', '') == 'scooter':
+                    vehicle_type['return_constraint'] = 'free_floating'  # valid for most providers
+                if path.startswith('/maps/gbfs/v2/nextbike_fg'):
+                    vehicle_type['return_constraint'] = 'hybrid'
+                if path.startswith('/public/v2/'):  # dott feed
+                    vehicle_type['return_constraint'] = 'hybrid'
path: app/converters/gbfs_set_return_constraint.py
user: login: ThorstenFroehlinghaus
body: Currently, I would have to develop individual ipl-proxys for each provider - which is hard to maintain. Alternatively, https://github.com/mobidata-bw/ipl-proxy/blob/90276bb0e97ccf2890d3fc7f17a0c31c257edcd9/app/app.py#L73 should also provide the host information.
created_at: 2025-05-06T11:16:11Z
author_association: MEMBER

PR comments:
user: login: hbruch
created_at: 2025-05-05T09:54:49Z
body: I'm somewhat reluctant to set an optional attribute for that many providers. IMHO a client could (and should be able to) do such a reasoning on its own. Does MobiData BW want to (and can it) take responsibility for this information?
author_association: COLLABORATOR
user: login: ThorstenFroehlinghaus
created_at: 2025-05-05T10:12:36Z
body: Reinhard investigated the providers, so these values are our best guess. If each client performs its own reasoning, different clients might assume different values for the same providers. Instead, MobiData-BW should consolidate such information. We take responsibility for this information - which means that we can explain them, and - if necessary - correct them.
author_association: MEMBER