rubybikes 0.3.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +12 -0
- data/CONTRIBUTING.md +33 -0
- data/LICENSE +662 -0
- data/NOTICE +10 -0
- data/PROVIDERS.md +123 -0
- data/README.md +108 -0
- data/RELEASING.md +44 -0
- data/data/ATTRIBUTION.txt +290 -0
- data/data/geojson/ch_cantons.json +53 -0
- data/data/geojson/japan_prefectures.json +49 -0
- data/data/geojson/penang.json +63 -0
- data/data/graphql/charichari.graphql +12 -0
- data/data/graphql/ganxeta.graphql +22 -0
- data/data/graphql/otp.graphql +68 -0
- data/data/providers.json +1 -0
- data/data/pybikes/aksu.json +49 -0
- data/data/pybikes/ambici.json +166 -0
- data/data/pybikes/baksi.json +41 -0
- data/data/pybikes/batumvelo.json +19 -0
- data/data/pybikes/bcycle.json +326 -0
- data/data/pybikes/beryl.json +195 -0
- data/data/pybikes/bicicard.json +205 -0
- data/data/pybikes/bicicas.json +20 -0
- data/data/pybikes/bicielx.json +19 -0
- data/data/pybikes/biciferrolterra.json +21 -0
- data/data/pybikes/bicimad.json +16 -0
- data/data/pybikes/bicincitta.json +1551 -0
- data/data/pybikes/bicing.json +37 -0
- data/data/pybikes/bikeshare_ie.json +46 -0
- data/data/pybikes/bikeu.json +17 -0
- data/data/pybikes/bixi.json +21 -0
- data/data/pybikes/bizkaibizi.json +105 -0
- data/data/pybikes/careem.json +40 -0
- data/data/pybikes/charichari.json +106 -0
- data/data/pybikes/chartered_bike.json +86 -0
- data/data/pybikes/clujbike.json +17 -0
- data/data/pybikes/cyclocity.json +211 -0
- data/data/pybikes/cyclopolis.json +78 -0
- data/data/pybikes/cykleo.json +39 -0
- data/data/pybikes/decobike.json +18 -0
- data/data/pybikes/derp.json +114 -0
- data/data/pybikes/deutschebahn.json +97 -0
- data/data/pybikes/digitransit.json +123 -0
- data/data/pybikes/docomo.json +215 -0
- data/data/pybikes/easybike.json +160 -0
- data/data/pybikes/ecovelo.json +307 -0
- data/data/pybikes/emovity.json +17 -0
- data/data/pybikes/encicla.json +17 -0
- data/data/pybikes/fifteen.json +50 -0
- data/data/pybikes/fsm.json +16 -0
- data/data/pybikes/ganxeta.json +20 -0
- data/data/pybikes/gbfs.json +1578 -0
- data/data/pybikes/gira.json +20 -0
- data/data/pybikes/gyorbike.json +19 -0
- data/data/pybikes/hellocycling.json +248 -0
- data/data/pybikes/joco.json +22 -0
- data/data/pybikes/keolis.json +22 -0
- data/data/pybikes/kolumbus.json +18 -0
- data/data/pybikes/lastenvelo.json +30 -0
- data/data/pybikes/linkbike.json +17 -0
- data/data/pybikes/meiob.json +18 -0
- data/data/pybikes/mobhis.json +43 -0
- data/data/pybikes/movus.json +72 -0
- data/data/pybikes/nextbike.json +2443 -0
- data/data/pybikes/nextbike_gbfs.json +5 -0
- data/data/pybikes/nubija.json +18 -0
- data/data/pybikes/open_source_bike_share.json +28 -0
- data/data/pybikes/open_source_bike_share_v2.json +17 -0
- data/data/pybikes/openmove.json +20 -0
- data/data/pybikes/ov_fiets.json +20 -0
- data/data/pybikes/pedalada.json +66 -0
- data/data/pybikes/publibike.json +127 -0
- data/data/pybikes/qrobici.json +20 -0
- data/data/pybikes/roovee.json +245 -0
- data/data/pybikes/samba.json +52 -0
- data/data/pybikes/sejong.json +19 -0
- data/data/pybikes/seoulbike.json +18 -0
- data/data/pybikes/sharedmobility.json +63 -0
- data/data/pybikes/slovnaft_bajk.json +21 -0
- data/data/pybikes/smartbike.json +28 -0
- data/data/pybikes/smoove.json +26 -0
- data/data/pybikes/soltrafego.json +39 -0
- data/data/pybikes/tashu.json +20 -0
- data/data/pybikes/urbansharing.json +22 -0
- data/data/pybikes/velib.json +23 -0
- data/data/pybikes/velobike_kz.json +17 -0
- data/data/pybikes/velok.json +16 -0
- data/data/pybikes/veloway.json +20 -0
- data/data/pybikes/weelo.json +45 -0
- data/data/pybikes/wegoshare.json +141 -0
- data/data/pybikes/xian.json +19 -0
- data/data/pybikes/yelovelo.json +22 -0
- data/data/pybikes/youbike.json +173 -0
- data/data/upstream.json +7 -0
- data/lib/rubybikes/engine.rb +3 -0
- data/lib/rubybikes/native/authenticated.rb +95 -0
- data/lib/rubybikes/native/engine.rb +103 -0
- data/lib/rubybikes/native/gbfs.rb +169 -0
- data/lib/rubybikes/native/geometry.rb +53 -0
- data/lib/rubybikes/native/graphql.rb +135 -0
- data/lib/rubybikes/native/html_clients.rb +313 -0
- data/lib/rubybikes/native/json_providers.rb +174 -0
- data/lib/rubybikes/native/more_json.rb +126 -0
- data/lib/rubybikes/native/nextbike.rb +50 -0
- data/lib/rubybikes/native/provider.rb +84 -0
- data/lib/rubybikes/native/remaining_clients.rb +102 -0
- data/lib/rubybikes/native/response_cache.rb +78 -0
- data/lib/rubybikes/native/transport.rb +163 -0
- data/lib/rubybikes/native/xml_details.rb +159 -0
- data/lib/rubybikes/native.rb +30 -0
- data/lib/rubybikes/version.rb +5 -0
- data/lib/rubybikes.rb +201 -0
- data/sig/external.rbs +30 -0
- data/sig/manifest.yaml +8 -0
- data/sig/native.rbs +230 -0
- data/sig/rubybikes.rbs +143 -0
- metadata +230 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Native ports of PyBikes bixi.py, decobike.py, xian.py, bicincitta.py,
|
|
4
|
+
# pedalada.py and fsm.py. Copyright (C) eskerda, Martin Gonzalez Gomez,
|
|
5
|
+
# aronsky and contributors. Distributed under AGPL-3.0; see LICENSE and NOTICE.
|
|
6
|
+
|
|
7
|
+
class RubyBikes
|
|
8
|
+
module Native
|
|
9
|
+
class JsonProviders
|
|
10
|
+
DECOBIKE_HEADERS = {
|
|
11
|
+
'Accept' => 'application/xml, text/xml, */*; q=0.01',
|
|
12
|
+
'Accept-Language' => 'en-US,en;q=0.9',
|
|
13
|
+
'User-Agent' => 'Mozilla/5.0 (Linux; Android 13; Pixel 6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36',
|
|
14
|
+
'X-Requested-With' => 'com.citibike.miami'
|
|
15
|
+
}.freeze
|
|
16
|
+
BICINCITTA_STATUSES = { 0 => 'online', 1 => 'maintenance', 2 => 'offline', 3 => 'under construction', 4 => 'planned' }.freeze
|
|
17
|
+
DETAIL_JSON_HEADERS = { 'Content-Type' => 'application/json; charset=utf-8' }.freeze
|
|
18
|
+
FSM_ENDPOINT = 'https://www.tel-o-fun.co.il/api/privatearea/v1'
|
|
19
|
+
FSM_HEADERS = { 'Accept-Language' => 'en' }.freeze
|
|
20
|
+
XIAN_SOAP = <<~XML
|
|
21
|
+
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:q0="http://ws.itcast.cn/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
|
22
|
+
<soapenv:Body><q0:findBikeSites></q0:findBikeSites></soapenv:Body>
|
|
23
|
+
</soapenv:Envelope>
|
|
24
|
+
XML
|
|
25
|
+
|
|
26
|
+
# Metadata generated by these clients belongs to their description too,
|
|
27
|
+
# before the first network update.
|
|
28
|
+
def initialize(spec, parameters, key: nil)
|
|
29
|
+
super
|
|
30
|
+
case spec.fetch('module')
|
|
31
|
+
when 'bicincitta'
|
|
32
|
+
endpoint = p('endpoint') || 'https://www.bicincitta.com/'
|
|
33
|
+
@meta['source'] = URI.join(endpoint, "frmLeStazioni.aspx?ID=#{p('city_ids').fetch(0)}").to_s
|
|
34
|
+
when 'pedalada'
|
|
35
|
+
@meta['company'] = Array(@parameters.fetch('meta')['company']) + Array(spec.fetch('meta')['company'])
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def update_bixi
|
|
40
|
+
detail_xml(p('feed_url')).xpath('/stations/station').each do |node|
|
|
41
|
+
name = xml_text(node, 'name')
|
|
42
|
+
terminal = xml_text(node, 'terminalName')
|
|
43
|
+
extra = {
|
|
44
|
+
'uid' => number(xml_text(node, 'id')),
|
|
45
|
+
'name' => name,
|
|
46
|
+
'terminalName' => terminal,
|
|
47
|
+
'locked' => xml_boolean?(xml_text(node, 'locked')),
|
|
48
|
+
'installed' => xml_boolean?(xml_text(node, 'installed')),
|
|
49
|
+
'temporary' => xml_boolean?(xml_text(node, 'temporary')),
|
|
50
|
+
'installDate' => xml_text(node, 'installDate'),
|
|
51
|
+
'removalDate' => xml_text(node, 'removalDate')
|
|
52
|
+
}
|
|
53
|
+
updated = xml_text(node, 'latestUpdateTime')
|
|
54
|
+
extra['latestUpdateTime'] = updated if updated && updated != '0'
|
|
55
|
+
station("#{terminal} - #{name}", xml_text(node, 'lat'), xml_text(node, 'long'),
|
|
56
|
+
number(xml_text(node, 'nbBikes')), number(xml_text(node, 'nbEmptyDocks')), extra)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def update_decobike
|
|
61
|
+
detail_xml(p('feed_url'), headers: DECOBIKE_HEADERS).xpath('//location').each do |node|
|
|
62
|
+
uid = xml_text(node, 'Id')
|
|
63
|
+
address = xml_text(node, 'Address')
|
|
64
|
+
station("#{uid} - #{address}", xml_text(node, 'Latitude'), xml_text(node, 'Longitude'),
|
|
65
|
+
number(xml_text(node, 'Bikes')), number(xml_text(node, 'Dockings')),
|
|
66
|
+
{ 'uid' => uid, 'address' => address })
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def update_xian
|
|
71
|
+
document = detail_xml(p('endpoint'), method: 'POST', data: XIAN_SOAP,
|
|
72
|
+
headers: { 'Content-Type' => 'text/xml; charset=utf-8' })
|
|
73
|
+
payload = document.xpath('string(//ns1:out)', 'ns1' => 'http://inter.webservice.web.ptpportal.guloo.org').delete(' ')
|
|
74
|
+
JSON.parse(payload).each do |item|
|
|
75
|
+
slots = number(item.fetch('locknum'))
|
|
76
|
+
free = number(item.fetch('emptynum'))
|
|
77
|
+
station(item.fetch('sitename'), item.fetch('latitude'), item.fetch('longitude'), slots - free, free,
|
|
78
|
+
{ 'uid' => item.fetch('siteid'), 'slots' => slots, 'address' => item.fetch('location') })
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def update_bicincitta
|
|
83
|
+
endpoint = p('endpoint') || 'https://www.bicincitta.com/'
|
|
84
|
+
p('city_ids').each do |city|
|
|
85
|
+
payload = json(URI.join(endpoint, 'frmLeStazioniComune.aspx/RefreshStations').to_s,
|
|
86
|
+
method: 'POST', data: JSON.generate('IDComune' => city.to_s), headers: DETAIL_JSON_HEADERS)
|
|
87
|
+
payload.fetch('d').drop(1).each do |raw|
|
|
88
|
+
uid, lat, lon, name, number, status = raw.split('§')
|
|
89
|
+
extra = { 'uid' => uid, 'number' => number(number), 'status' => BICINCITTA_STATUSES.fetch(number(status)) }
|
|
90
|
+
detail = json(URI.join(endpoint, 'frmLeStazioni.aspx/RefreshPopup').to_s,
|
|
91
|
+
method: 'POST', data: JSON.generate('IDStazione' => uid.to_s), headers: DETAIL_JSON_HEADERS)
|
|
92
|
+
_, reviews, score, _, availability = detail.fetch('d').split('§').take(5)
|
|
93
|
+
extra['score'] = Float(score.tr(',', '.'))
|
|
94
|
+
extra['reviews'] = number(reviews)
|
|
95
|
+
station(name, bicincitta_coordinate(lat), bicincitta_coordinate(lon), availability.count('4'), availability.count('0'), extra)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def update_pedalada
|
|
101
|
+
endpoint = p('endpoint')
|
|
102
|
+
json("#{endpoint}/php/get-stations-details-map.php").each do |item|
|
|
103
|
+
uid = item.fetch('stationNumber')
|
|
104
|
+
slots = item.fetch('maximumNumberOfRides')
|
|
105
|
+
detail = json("#{endpoint}/php/get-station-details-and-rides.php", params: { 'n' => uid })
|
|
106
|
+
bikes = detail.fetch('rides').length
|
|
107
|
+
extra = {
|
|
108
|
+
'uid' => uid, 'slots' => slots, 'online' => item.fetch('state') == 1,
|
|
109
|
+
'photo' => "#{endpoint}/images/stations/station_#{uid}.jpg",
|
|
110
|
+
'status' => pedalada_status(detail.fetch('details').fetch(0))
|
|
111
|
+
}
|
|
112
|
+
station(item.fetch('stationName'), item.fetch('latitude'), item.fetch('longitude'), bikes, slots - bikes, extra)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def update_fsm
|
|
117
|
+
json("#{FSM_ENDPOINT}/map", headers: FSM_HEADERS).fetch('stations').each do |item|
|
|
118
|
+
detail = json("#{FSM_ENDPOINT}/station/#{item.fetch('id')}", headers: FSM_HEADERS)
|
|
119
|
+
electric = item.fetch('omniBikes')
|
|
120
|
+
normal = item.fetch('novatechBikes')
|
|
121
|
+
electric_slots = detail.fetch('availableOmniPoles')
|
|
122
|
+
normal_slots = detail.fetch('availableNovatechPoles')
|
|
123
|
+
extra = {
|
|
124
|
+
'uid' => item.fetch('id'), 'number' => item.fetch('stationNumber'),
|
|
125
|
+
'shabbat' => item.fetch('isShabbatStation'), 'has_ebikes' => true,
|
|
126
|
+
'ebikes' => electric, 'normal_bikes' => normal, 'normal_slots' => normal_slots,
|
|
127
|
+
'e_slots' => electric_slots, 'address' => detail.fetch('address')
|
|
128
|
+
}
|
|
129
|
+
station(item.fetch('name'), item.fetch('location').fetch('lat'), item.fetch('location').fetch('lng'),
|
|
130
|
+
normal + electric, normal_slots + electric_slots, extra)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def detail_xml(url, **options)
|
|
137
|
+
Nokogiri::XML(request(url, **options)) { |config| config.strict.nonet }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def xml_text(node, name)
|
|
141
|
+
node.at_xpath(name)&.text
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def xml_boolean?(value)
|
|
145
|
+
%w[yes true t 1].include?(value.downcase)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def bicincitta_coordinate(value)
|
|
149
|
+
Float(value[/\d+\.\d{5,15}/] || value)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def pedalada_status(detail)
|
|
153
|
+
return 'maintenance' if truthy?(detail.fetch('stationInMaintenance'))
|
|
154
|
+
|
|
155
|
+
truthy?(detail.fetch('stationIsOpen')) ? 'open' : 'closed'
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubocop:disable Style/OneClassPerFile -- The loader establishes data paths before exposing the native Engine alias.
|
|
4
|
+
|
|
5
|
+
require_relative 'version'
|
|
6
|
+
class RubyBikes
|
|
7
|
+
module Native
|
|
8
|
+
DATA = File.expand_path('../../data', File.dirname(__FILE__))
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
require_relative 'native/geometry'
|
|
12
|
+
require_relative 'native/response_cache'
|
|
13
|
+
require_relative 'native/transport'
|
|
14
|
+
require_relative 'native/provider'
|
|
15
|
+
require_relative 'native/gbfs'
|
|
16
|
+
require_relative 'native/nextbike'
|
|
17
|
+
require_relative 'native/json_providers'
|
|
18
|
+
require_relative 'native/engine'
|
|
19
|
+
require_relative 'native/authenticated'
|
|
20
|
+
require_relative 'native/graphql'
|
|
21
|
+
require_relative 'native/more_json'
|
|
22
|
+
require_relative 'native/remaining_clients'
|
|
23
|
+
require_relative 'native/html_clients'
|
|
24
|
+
require_relative 'native/xml_details'
|
|
25
|
+
class RubyBikes
|
|
26
|
+
Engine = Native::Engine unless const_defined?(:Engine, false)
|
|
27
|
+
end
|
|
28
|
+
require_relative '../rubybikes' unless defined?(RubyBikes::System)
|
|
29
|
+
|
|
30
|
+
# rubocop:enable Style/OneClassPerFile
|
data/lib/rubybikes.rb
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright (C) Eduardo Mucelli Rezende Oliveira
|
|
4
|
+
# Distributed under the AGPL license, see LICENSE.
|
|
5
|
+
|
|
6
|
+
require 'json'
|
|
7
|
+
require 'time'
|
|
8
|
+
|
|
9
|
+
require_relative 'rubybikes/version'
|
|
10
|
+
|
|
11
|
+
class RubyBikes
|
|
12
|
+
class Error < StandardError
|
|
13
|
+
attr_reader :details
|
|
14
|
+
|
|
15
|
+
def initialize(message, details: {})
|
|
16
|
+
@details = details
|
|
17
|
+
super(message)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class DependencyError < Error; end
|
|
22
|
+
class ProviderError < Error; end
|
|
23
|
+
class ProtocolError < Error; end
|
|
24
|
+
class TimeoutError < Error; end
|
|
25
|
+
|
|
26
|
+
class RateLimitedError < ProviderError
|
|
27
|
+
def retry_after = details.fetch('retry_after', 300).to_f
|
|
28
|
+
def host = details['host']
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class NoSystemFoundError < Error; end
|
|
32
|
+
class NoGetCriteriaError < Error; end
|
|
33
|
+
class APIKeyNotAvailableError < Error; end
|
|
34
|
+
|
|
35
|
+
def initialize(timeout: 60, keys: {}, engine: nil)
|
|
36
|
+
@engine = engine || Engine.new(timeout: timeout)
|
|
37
|
+
@keys = keys.transform_keys(&:to_s)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def catalog
|
|
41
|
+
@catalog ||= @engine.request('catalog').fetch('networks')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def engine_info = @engine.request('version')
|
|
45
|
+
def providers = @providers ||= @engine.request('providers').fetch('providers')
|
|
46
|
+
def tags = catalog.map { |entry| entry.fetch('tag') }
|
|
47
|
+
def labels = catalog.map { |entry| entry.fetch('label') }.uniq.sort
|
|
48
|
+
|
|
49
|
+
# Original string-keyed hashes and modern keyword/string forms are accepted.
|
|
50
|
+
def get(options = {}, **keywords)
|
|
51
|
+
raise ArgumentError, 'options must be a tag or hash' unless options.is_a?(String) || options.is_a?(Hash)
|
|
52
|
+
|
|
53
|
+
options = options.is_a?(String) ? { 'tag' => options } : options.transform_keys(&:to_s)
|
|
54
|
+
options = options.merge(keywords.transform_keys(&:to_s))
|
|
55
|
+
if (tag = options['tag'])
|
|
56
|
+
entry = catalog.find { |row| row['tag'] == tag }
|
|
57
|
+
raise NoSystemFoundError, "Unknown system tag: #{tag}" unless entry
|
|
58
|
+
|
|
59
|
+
key = options['api_key'] || options['key'] || @keys[tag] || @keys[entry['module']] || @keys[entry['class']]
|
|
60
|
+
description = @engine.request('describe', tag: tag, key: key)
|
|
61
|
+
System.new(@engine, description, key)
|
|
62
|
+
elsif (label = options['label'])
|
|
63
|
+
entries = catalog.select do |row|
|
|
64
|
+
[row['label'], row['class'], row['module']].any? { |value| value.to_s.casecmp?(label.to_s) }
|
|
65
|
+
end
|
|
66
|
+
entries.map { |entry| get(options.merge('tag' => entry['tag'])) }
|
|
67
|
+
else
|
|
68
|
+
raise NoGetCriteriaError, "Supply a 'tag' or 'label'"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Instantiate any upstream client, including clients without catalog entries.
|
|
73
|
+
# Parameters follow that client's PyBikes constructor (feed_url, domain, etc.).
|
|
74
|
+
def build(provider:, tag:, meta:, class_name: nil, api_key: nil, **parameters)
|
|
75
|
+
entries = providers.select { |row| row['class'].casecmp?(provider.to_s) } unless class_name
|
|
76
|
+
if !entries || entries.empty?
|
|
77
|
+
entries = providers.select do |row|
|
|
78
|
+
row['module'].casecmp?(provider.to_s) && (!class_name || row['class'].casecmp?(class_name.to_s))
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
raise NoSystemFoundError, "Unknown provider: #{provider}" if entries.empty?
|
|
82
|
+
raise ArgumentError, 'Select a class_name for this provider module' if entries.size > 1
|
|
83
|
+
|
|
84
|
+
entry = entries.first
|
|
85
|
+
key = api_key || @keys[tag] || @keys[entry['module']] || @keys[entry['class']]
|
|
86
|
+
definition = { 'module' => entry['module'], 'class' => entry['class'],
|
|
87
|
+
'parameters' => parameters.merge(meta: meta) }
|
|
88
|
+
# Detach nested caller-owned values so updates and process restarts keep
|
|
89
|
+
# using the same definition that was originally described.
|
|
90
|
+
definition = JSON.parse(JSON.generate(definition))
|
|
91
|
+
description = @engine.request('describe', tag: tag, key: key, definition: definition)
|
|
92
|
+
System.new(@engine, description, key, definition: definition)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def close = @engine.close
|
|
96
|
+
|
|
97
|
+
def self.open(**options)
|
|
98
|
+
factory = new(**options)
|
|
99
|
+
return factory unless block_given?
|
|
100
|
+
|
|
101
|
+
begin
|
|
102
|
+
yield factory
|
|
103
|
+
ensure
|
|
104
|
+
factory.close
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
class Station
|
|
109
|
+
attr_reader :id, :name, :latitude, :longitude, :bikes, :free, :timestamp, :extra
|
|
110
|
+
|
|
111
|
+
def initialize(row)
|
|
112
|
+
@id, @name = row.fetch('id'), row.fetch('name')
|
|
113
|
+
@latitude, @longitude = row.fetch('latitude'), row.fetch('longitude')
|
|
114
|
+
@bikes, @free, @extra = row['bikes'], row['free'], row.fetch('extra', {})
|
|
115
|
+
@timestamp = row['timestamp'] && Time.iso8601(row['timestamp']).utc
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def get_hash = id # rubocop:disable Naming/AccessorMethodName -- Retain the public compatibility method.
|
|
119
|
+
|
|
120
|
+
def to_h
|
|
121
|
+
{ 'id' => id, 'name' => name, 'latitude' => latitude, 'longitude' => longitude,
|
|
122
|
+
'bikes' => bikes, 'free' => free, 'timestamp' => timestamp&.iso8601(6), 'extra' => extra }
|
|
123
|
+
end
|
|
124
|
+
alias to_dict to_h
|
|
125
|
+
def to_json(state = nil) = to_h.to_json(state)
|
|
126
|
+
|
|
127
|
+
def to_geojson
|
|
128
|
+
{ 'type' => 'Feature', 'geometry' => { 'type' => 'Point', 'coordinates' => [longitude, latitude] },
|
|
129
|
+
'properties' => to_h.except('id', 'timestamp').merge('type' => 'station') }
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
class Vehicle
|
|
134
|
+
attr_reader :id, :latitude, :longitude, :kind, :timestamp, :extra
|
|
135
|
+
|
|
136
|
+
def initialize(row)
|
|
137
|
+
@id, @latitude, @longitude, @kind = row.values_at('id', 'latitude', 'longitude', 'kind')
|
|
138
|
+
@extra = row.fetch('extra', {})
|
|
139
|
+
@timestamp = row['timestamp'] && Time.iso8601(row['timestamp']).utc
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def get_hash = id # rubocop:disable Naming/AccessorMethodName -- Retain the public compatibility method.
|
|
143
|
+
|
|
144
|
+
def to_h
|
|
145
|
+
{ 'id' => id, 'latitude' => latitude, 'longitude' => longitude, 'kind' => kind,
|
|
146
|
+
'timestamp' => timestamp&.iso8601(6), 'extra' => extra }
|
|
147
|
+
end
|
|
148
|
+
alias to_dict to_h
|
|
149
|
+
def to_json(state = nil) = to_h.to_json(state)
|
|
150
|
+
|
|
151
|
+
def to_geojson
|
|
152
|
+
{ 'type' => 'Feature', 'geometry' => { 'type' => 'Point', 'coordinates' => [longitude, latitude] },
|
|
153
|
+
'properties' => to_h.except('timestamp').merge('type' => 'vehicle') }
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
class System
|
|
158
|
+
attr_reader :tag, :meta, :stations, :vehicles, :authed, :sync, :unifeed, :provider, :client_class
|
|
159
|
+
|
|
160
|
+
def initialize(engine, description, key, definition: nil)
|
|
161
|
+
@engine, @key = engine, key
|
|
162
|
+
@definition = definition
|
|
163
|
+
@tag, @meta, @authed, @sync = description.values_at('tag', 'meta', 'authed', 'sync')
|
|
164
|
+
@provider, @client_class, @unifeed = description.values_at('module', 'class', 'unifeed')
|
|
165
|
+
@stations, @vehicles = [], []
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def update(timeout: @engine.timeout, scraper: {})
|
|
169
|
+
arguments = { tag: tag, key: @key, scraper: scraper }
|
|
170
|
+
arguments[:definition] = @definition if @definition
|
|
171
|
+
row = @engine.request('update', timeout: timeout, **arguments)
|
|
172
|
+
raise ProtocolError, 'Provider returned a different system' unless row.fetch('tag') == tag
|
|
173
|
+
|
|
174
|
+
# Build the complete snapshot first, retaining the previous one on failure.
|
|
175
|
+
stations = row.fetch('stations').map { |station| Station.new(station) }
|
|
176
|
+
# @type var empty_vehicles: Array[RubyBikes::object]
|
|
177
|
+
empty_vehicles = []
|
|
178
|
+
vehicles = row.fetch('vehicles', empty_vehicles).map { |vehicle| Vehicle.new(vehicle) }
|
|
179
|
+
@meta, @stations, @vehicles = row.fetch('meta'), stations, vehicles
|
|
180
|
+
self
|
|
181
|
+
rescue KeyError, TypeError, ArgumentError
|
|
182
|
+
raise ProtocolError, 'Provider returned an invalid snapshot'
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def to_h
|
|
186
|
+
{ 'tag' => tag, 'meta' => meta, 'stations' => stations.map(&:to_h), 'vehicles' => vehicles.map(&:to_h) }
|
|
187
|
+
end
|
|
188
|
+
alias to_dict to_h
|
|
189
|
+
def to_json(state = nil) = to_h.to_json(state)
|
|
190
|
+
|
|
191
|
+
def to_geojson
|
|
192
|
+
{ 'type' => 'FeatureCollection', 'features' => (stations + vehicles).map(&:to_geojson) }
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Preserve the exception names used by existing RubyBikes consumers.
|
|
198
|
+
NoSystemFoundError = RubyBikes::NoSystemFoundError unless defined?(NoSystemFoundError)
|
|
199
|
+
NoGetCriteriaError = RubyBikes::NoGetCriteriaError unless defined?(NoGetCriteriaError)
|
|
200
|
+
APIKeyNotAvailableError = RubyBikes::APIKeyNotAvailableError unless defined?(APIKeyNotAvailableError)
|
|
201
|
+
require_relative 'rubybikes/engine'
|
data/sig/external.rbs
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Parser and cookie libraries form dynamic external-data boundaries.
|
|
2
|
+
module Nokogiri
|
|
3
|
+
def self.XML: (String) ?{ (untyped) -> untyped } -> untyped
|
|
4
|
+
def self.HTML: (String) -> untyped
|
|
5
|
+
module HTML
|
|
6
|
+
def self.fragment: (String) -> untyped
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module HTTP
|
|
11
|
+
class Cookie
|
|
12
|
+
def name: () -> String
|
|
13
|
+
def value: () -> String
|
|
14
|
+
end
|
|
15
|
+
class CookieJar
|
|
16
|
+
def initialize: () -> void
|
|
17
|
+
def cookies: (URI::Generic) -> Array[Cookie]
|
|
18
|
+
def parse: (String, URI::Generic) -> Array[Cookie]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# JSON generation accepts the optional JSON state passed by JSON.dump.
|
|
23
|
+
class Hash[unchecked out K, unchecked out V]
|
|
24
|
+
def to_json: (?untyped) -> String
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# RBS 3.9 omits the mandatory block on this documented synchronization API.
|
|
28
|
+
class Thread
|
|
29
|
+
def self.handle_interrupt: [A] (untyped) { () -> A } -> A
|
|
30
|
+
end
|
data/sig/manifest.yaml
ADDED
data/sig/native.rbs
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
class RubyBikes
|
|
2
|
+
module Native
|
|
3
|
+
DATA: String
|
|
4
|
+
type bounds = Hash[String, untyped] | Array[untyped] | nil
|
|
5
|
+
|
|
6
|
+
module Geometry
|
|
7
|
+
def self?.contains?: (bounds, Float, Float) -> bool
|
|
8
|
+
def self?.polygon?: (untyped, Float, Float) -> bool
|
|
9
|
+
def self?.ring: (untyped, Float, Float) -> (:inside | :outside | :boundary)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class ResponseCache
|
|
13
|
+
@ttl: Integer | Float
|
|
14
|
+
@max_bytes: Integer
|
|
15
|
+
@max_entries: Integer
|
|
16
|
+
@entries: Hash[untyped, Entry]
|
|
17
|
+
@bytes: Integer
|
|
18
|
+
@mutex: Mutex
|
|
19
|
+
class Entry < Struct[untyped]
|
|
20
|
+
attr_accessor response: Transport::Response
|
|
21
|
+
attr_accessor stored_at: Float
|
|
22
|
+
attr_accessor bytes: Integer
|
|
23
|
+
def self.new: (response: Transport::Response, stored_at: Float, bytes: Integer) -> Entry
|
|
24
|
+
def initialize: (response: Transport::Response, stored_at: Float, bytes: Integer) -> void
|
|
25
|
+
end
|
|
26
|
+
def initialize: (?ttl: Integer | Float, ?max_bytes: Integer, ?max_entries: Integer) -> void
|
|
27
|
+
def get: (untyped, Float) -> Transport::Response?
|
|
28
|
+
def put: (untyped, Transport::Response, Float) -> void
|
|
29
|
+
private
|
|
30
|
+
def atomic: [A] () { () -> A } -> A
|
|
31
|
+
def expire: (Float) -> void
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class Transport
|
|
35
|
+
@clock: ^() -> Float
|
|
36
|
+
@deadline: Float
|
|
37
|
+
@headers: Hash[String, String]
|
|
38
|
+
@proxy: URI::Generic?
|
|
39
|
+
@cooldowns: Hash[String, Float]
|
|
40
|
+
@cookies: untyped
|
|
41
|
+
@cache: ResponseCache
|
|
42
|
+
@last_response: Response
|
|
43
|
+
@deadline_active: bool
|
|
44
|
+
attr_reader headers: Hash[String, String]
|
|
45
|
+
attr_reader last_response: Response
|
|
46
|
+
class Response < Struct[untyped]
|
|
47
|
+
attr_accessor status: Integer
|
|
48
|
+
attr_accessor headers: Hash[String, String]
|
|
49
|
+
attr_accessor body: String
|
|
50
|
+
def self.new: (status: Integer, headers: Hash[String, String], body: String) -> Response
|
|
51
|
+
def initialize: (status: Integer, headers: Hash[String, String], body: String) -> void
|
|
52
|
+
end
|
|
53
|
+
def initialize: (?timeout: Numeric, ?headers: Hash[String, String], ?proxy: String?, ?cooldowns: Hash[String, Float], ?cache: ResponseCache?, ?clock: ^() -> Float) -> void
|
|
54
|
+
def remaining: () -> Float
|
|
55
|
+
def within_deadline: [A] () { () -> A } -> A
|
|
56
|
+
def request: (String, ?method: String, ?params: untyped, ?data: untyped, ?headers: Hash[String, String], ?raw: bool, ?skip_cache: bool, ?default_encoding: String) -> String
|
|
57
|
+
private
|
|
58
|
+
def perform_request: (String, method: String, params: untyped, data: untyped, headers: Hash[String, String], raw: bool, skip_cache: bool, default_encoding: String) -> String
|
|
59
|
+
def decode: (Response, bool, String) -> String
|
|
60
|
+
def transport_uri: (String) -> URI::HTTP
|
|
61
|
+
def fetch: (URI::HTTP, String, untyped, Hash[String, String], Integer) -> Response
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class Provider
|
|
65
|
+
@spec: object
|
|
66
|
+
@parameters: object
|
|
67
|
+
@tag: String
|
|
68
|
+
@meta: object
|
|
69
|
+
@key: untyped
|
|
70
|
+
@transport: Transport
|
|
71
|
+
@stations: Array[station_row]
|
|
72
|
+
@vehicles: Array[vehicle_row]
|
|
73
|
+
attr_reader tag: String
|
|
74
|
+
attr_reader meta: object
|
|
75
|
+
attr_reader spec: object
|
|
76
|
+
attr_reader parameters: object
|
|
77
|
+
def initialize: (object, object, ?key: api_key) -> void
|
|
78
|
+
def snapshot: (Transport) -> object
|
|
79
|
+
def update: () -> void
|
|
80
|
+
def station: (String?, untyped, untyped, ?Integer?, ?Integer?, ?object) -> Array[station_row]?
|
|
81
|
+
def vehicle: (untyped, untyped, ?object, ?String) -> Array[vehicle_row]?
|
|
82
|
+
def request: (String, **untyped) -> String
|
|
83
|
+
def json: (String, **untyped) -> untyped
|
|
84
|
+
def xml: (String, **untyped) -> untyped
|
|
85
|
+
def html: (String, **untyped) -> untyped
|
|
86
|
+
def p: (String, ?untyped) -> untyped
|
|
87
|
+
def number: (untyped) -> Integer
|
|
88
|
+
def list: (untyped) -> Array[untyped]
|
|
89
|
+
def integer: (untyped) -> Integer?
|
|
90
|
+
def truthy?: (untyped) -> bool?
|
|
91
|
+
def iso_date: (untyped) -> String?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
class Gbfs < Provider
|
|
95
|
+
@feed_url: String
|
|
96
|
+
def initialize: (object, object, ?key: api_key) -> void
|
|
97
|
+
def update: () -> void
|
|
98
|
+
def vehicle_kind: (object) -> [String?, String?]
|
|
99
|
+
private
|
|
100
|
+
def discover_feeds: (untyped) -> Hash[String, String]
|
|
101
|
+
def parse_gbfs_station: (object, object) -> void
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
class Nextbike < Provider
|
|
105
|
+
def update: () -> void
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
class JsonProviders < Provider
|
|
109
|
+
EMOVITY_USER_AGENT: String
|
|
110
|
+
SAMBA_USER_AGENT: String
|
|
111
|
+
DECOBIKE_HEADERS: Hash[String, String]
|
|
112
|
+
BICINCITTA_STATUSES: Hash[Integer, String]
|
|
113
|
+
DETAIL_JSON_HEADERS: Hash[String, String]
|
|
114
|
+
FSM_ENDPOINT: String
|
|
115
|
+
FSM_HEADERS: Hash[String, String]
|
|
116
|
+
XIAN_SOAP: String
|
|
117
|
+
def initialize: (object, object, ?key: api_key) -> void
|
|
118
|
+
def update: () -> void
|
|
119
|
+
def update_aksu: () -> void
|
|
120
|
+
def update_baksi: () -> void
|
|
121
|
+
def update_batumvelo: () -> void
|
|
122
|
+
def update_bicicard: () -> void
|
|
123
|
+
def update_bicicas: () -> void
|
|
124
|
+
def update_bicielx: () -> void
|
|
125
|
+
def update_biciferrolterra: () -> void
|
|
126
|
+
def update_bicimad: () -> void
|
|
127
|
+
def update_bicincitta: () -> void
|
|
128
|
+
def update_bicing: () -> void
|
|
129
|
+
def update_bikeshare_ie: () -> void
|
|
130
|
+
def update_bikeu: () -> void
|
|
131
|
+
def update_bixi: () -> void
|
|
132
|
+
def update_charichari: () -> void
|
|
133
|
+
def update_chartered_bike: () -> void
|
|
134
|
+
def update_clujbike: () -> void
|
|
135
|
+
def update_cyclopolis: () -> void
|
|
136
|
+
def update_cyclopolis_api: () -> void
|
|
137
|
+
def update_cykleo: () -> void
|
|
138
|
+
def update_decobike: () -> void
|
|
139
|
+
def update_digitransit: () -> void
|
|
140
|
+
def update_easybike: () -> void
|
|
141
|
+
def update_emovity: () -> void
|
|
142
|
+
def update_encicla: () -> void
|
|
143
|
+
def update_fifteen: () -> void
|
|
144
|
+
def update_fsm: () -> void
|
|
145
|
+
def update_ganxeta: () -> void
|
|
146
|
+
def update_gira: () -> void
|
|
147
|
+
def update_gyorbike: () -> void
|
|
148
|
+
def update_joco: () -> void
|
|
149
|
+
def update_keolis: () -> void
|
|
150
|
+
def update_kolumbus: () -> void
|
|
151
|
+
def update_lastenvelo: () -> void
|
|
152
|
+
def update_linkbike: () -> void
|
|
153
|
+
def update_meiob: () -> void
|
|
154
|
+
def update_mobhis: () -> void
|
|
155
|
+
def update_movus: () -> void
|
|
156
|
+
def update_nubija: () -> void
|
|
157
|
+
def update_open_source_bike_share: () -> void
|
|
158
|
+
def update_openmove: () -> void
|
|
159
|
+
def update_otp: () -> void
|
|
160
|
+
def update_ov_fiets: () -> void
|
|
161
|
+
def update_pedalada: () -> void
|
|
162
|
+
def update_publibike: () -> void
|
|
163
|
+
def update_qrobici: () -> void
|
|
164
|
+
def update_roovee: () -> void
|
|
165
|
+
def update_samba: () -> void
|
|
166
|
+
def update_sejong: () -> void
|
|
167
|
+
def update_seoulbike: () -> void
|
|
168
|
+
def update_slovnaft_bajk: () -> void
|
|
169
|
+
def update_smartbike: () -> void
|
|
170
|
+
def update_smoove: () -> void
|
|
171
|
+
def update_soltrafego: () -> void
|
|
172
|
+
def update_tashu: () -> void
|
|
173
|
+
def update_urbansharing: () -> void
|
|
174
|
+
def update_velobike_kz: () -> void
|
|
175
|
+
def update_velok: () -> void
|
|
176
|
+
def update_veloway: () -> void
|
|
177
|
+
def update_weelo: () -> void
|
|
178
|
+
def update_wegoshare: () -> void
|
|
179
|
+
def update_xian: () -> void
|
|
180
|
+
def update_yelovelo: () -> void
|
|
181
|
+
def update_youbike: () -> void
|
|
182
|
+
def lower_keys: (untyped) -> untyped
|
|
183
|
+
def graphql: (String, String, ?object, ?operation: String?) -> untyped
|
|
184
|
+
def otp_type: (object) -> [String?, String]
|
|
185
|
+
private
|
|
186
|
+
def html_scan: (String, Regexp) -> Array[untyped]
|
|
187
|
+
def html_match: (String, Regexp) -> MatchData
|
|
188
|
+
def html_capture: (String, Regexp) -> String
|
|
189
|
+
def html_text: (untyped, String) -> String
|
|
190
|
+
def gyorbike_count: (untyped, String) -> Integer
|
|
191
|
+
def velobike_number: (untyped) -> Integer
|
|
192
|
+
def detail_xml: (String, **untyped) -> untyped
|
|
193
|
+
def xml_text: (untyped, String) -> untyped
|
|
194
|
+
def xml_boolean?: (String) -> bool
|
|
195
|
+
def bicincitta_coordinate: (String) -> Float
|
|
196
|
+
def pedalada_status: (object) -> String
|
|
197
|
+
def cyclopolis_count: (String) -> Integer
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
class DataLiteral
|
|
201
|
+
@scanner: StringScanner
|
|
202
|
+
def self.parse: (String) -> json
|
|
203
|
+
def initialize: (String) -> void
|
|
204
|
+
def parse: () -> json
|
|
205
|
+
private
|
|
206
|
+
def whitespace: () -> Integer?
|
|
207
|
+
def value: (Integer) -> json
|
|
208
|
+
def sequence: (Integer, String) -> Array[json]
|
|
209
|
+
def string: () -> String
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
class Engine
|
|
213
|
+
MANIFEST: object
|
|
214
|
+
@timeout: Float
|
|
215
|
+
@providers: Array[object]
|
|
216
|
+
@definitions: Hash[String, object]
|
|
217
|
+
@mutex: Mutex
|
|
218
|
+
@cooldowns: Hash[String, Float]
|
|
219
|
+
@systems: Hash[String, Provider]
|
|
220
|
+
@response_cache: ResponseCache
|
|
221
|
+
attr_reader timeout: Float
|
|
222
|
+
def initialize: (?timeout: Numeric) -> void
|
|
223
|
+
def request: (String, ?timeout: Numeric, **untyped) -> untyped
|
|
224
|
+
def close: () -> void
|
|
225
|
+
private
|
|
226
|
+
def catalog_instances: (untyped) -> Array[untyped]
|
|
227
|
+
def find_spec: (object) -> object
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|