gamespy_query 0.2.0pre2 → 0.2.0pre3

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.
data/bin/gamespy_query CHANGED
File without changes
@@ -13,7 +13,7 @@ module GamespyQuery
13
13
  module Tools
14
14
  STR_EMPTY = ""
15
15
  CHAR_N = "\n"
16
-
16
+
17
17
  module_function
18
18
  # Provides access to the logger object
19
19
  # Will use ActionController::Base.logger if available
@@ -76,9 +76,9 @@ STR
76
76
  # Integer / Float actually String Regex
77
77
  RX_S = /\A\-?0[0-9]+.*\Z/
78
78
 
79
- # Clean value, convert if possible
79
+ # Convert data type and strip tags
80
80
  # @param [String] value String to convert
81
- def clean(value) # TODO: Force String, Integer, Float etc?
81
+ def convert_type(value) # TODO: Force String, Integer, Float etc?
82
82
  case value
83
83
  when STR_X0
84
84
  nil
@@ -99,25 +99,18 @@ STR
99
99
  number
100
100
  end
101
101
 
102
+ STR_UTF8 = 'UTF-8'
103
+
102
104
  # Convert string to UTF-8, stripping out all invalid/undefined characters
103
105
  # @param [String] str String to convert
104
106
  def encode_string(str)
105
107
  #Tools.debug {"Getting string #{str}"}
106
- _encode_string str
107
- end
108
-
109
- if RUBY_PLATFORM =~ /mswin32/
110
- # Get UTF-8 string from string
111
- # @param [String] str
112
- def _encode_string(str)
113
- System::Text::Encoding.UTF8.GetString(System::Array.of(System::Byte).new(str.bytes.to_a)).to_s # # begin; System::Text::Encoding.USASCII.GetString(reply[0]).to_s; rescue nil, Exception => e; Tools.log_exception(e); reply[0].map {|e| e.chr}.join; end
114
- end
115
- else
116
- # Get UTF-8 string from string
117
- # @param [String] str
118
- def _encode_string(str)
119
- #(str + ' ').encode("US-ASCII", invalid: :replace, undef: :replace)[0..-3]
120
- str.bytes.to_a.pack("U*")
108
+ #System::Text::Encoding.UTF8.GetString(System::Array.of(System::Byte).new(str.bytes.to_a)).to_s # # begin; System::Text::Encoding.USASCII.GetString(reply[0]).to_s; rescue nil, Exception => e; Tools.log_exception(e); reply[0].map {|e| e.chr}.join; end
109
+ begin
110
+ str.bytes.to_a.pack('C*').force_encoding(STR_UTF8)
111
+ rescue nil, Exception => e
112
+ Tools.log_exception e
113
+ str.encode("UTF-8", invalid: :replace, undef: :replace)
121
114
  end
122
115
  end
123
116
  end
@@ -1,8 +1,12 @@
1
1
  module GamespyQuery
2
2
  # Provides access to the Gamespy Master browser
3
3
  class Master < Base
4
+ # TODO: gslist.exe output encoding seems to be a problem.
5
+ # not been able to find a solution yet to get unicode instead of garbelled characters
6
+ # If impossible, perhaps should try custom implementation of master query
7
+
4
8
  PARAMS = [:hostname, :gamever, :gametype, :gamemode, :numplayers, :maxplayers, :password, :equalModRequired, :mission, :mapname,
5
- :mod, :signatures, :verifysignatures, :gamestate, :dedicated, :platform, :sv_battleeye, :language, :difficulty]
9
+ :mod, :signatures, :verifysignatures, :gamestate, :dedicated, :platform, :sv_battleye, :language, :difficulty]
6
10
 
7
11
  RX_ADDR_LINE = /^[\s\t]*([\d\.]+)[\s\t:]*(\d+)[\s\t]*(.*)$/
8
12
 
@@ -13,6 +17,14 @@ module GamespyQuery
13
17
  "\\\\"
14
18
  end
15
19
 
20
+ path = defined?(Rails) ? File.join(Rails.root, "config") : File.join(Dir.pwd, "config")
21
+ DEFAULT_GEOIP_PATH = case RUBY_PLATFORM
22
+ when /-mingw32$/, /-mswin32$/
23
+ path.gsub("/", "\\")
24
+ else
25
+ path
26
+ end
27
+
16
28
  # Geo settings
17
29
  attr_reader :geo
18
30
 
@@ -22,8 +34,8 @@ module GamespyQuery
22
34
  # Initializes the instance
23
35
  # @param [String] geo Geo string
24
36
  # @param [String] game Game string
25
- def initialize(geo = nil, game = "arma2oapc")
26
- @geo, @game = geo, game
37
+ def initialize(geo = nil, game = "arma2oapc", geoip_path = nil)
38
+ @geo, @game, @geoip_path = geo, game, geoip_path
27
39
  end
28
40
 
29
41
  # Convert the master browser data to hash
@@ -118,20 +130,7 @@ module GamespyQuery
118
130
 
119
131
  # Get geoip_path
120
132
  def geoip_path
121
- return File.join(Dir.pwd, "config") unless defined?(Rails)
122
-
123
- case RUBY_PLATFORM
124
- when /-mingw32$/, /-mswin32$/
125
- File.join(Rails.root, "config").gsub("/", "\\")
126
- else
127
- File.join(Rails.root, "config")
128
- end
133
+ @geoip_path || DEFAULT_GEOIP_PATH
129
134
  end
130
135
  end
131
136
  end
132
-
133
- if $0 == __FILE__
134
- master = GamespyQuery::Master.new
135
- r = master.read
136
- puts r
137
- end
@@ -30,13 +30,13 @@ module GamespyQuery
30
30
  # - Array, packetDATA ordered already by packetID
31
31
  def initialize(packets)
32
32
  @packets = case packets
33
- when Hash
34
- packets.keys.sort.map{|key| packets[key] }
35
- when Array
36
- packets
37
- else
38
- raise UnsupportedFormat, "Unsupported format: #{packets.class}"
39
- end
33
+ when Hash
34
+ packets.keys.sort.map{|key| packets[key] }
35
+ when Array
36
+ packets
37
+ else
38
+ raise UnsupportedFormat, "Unsupported format: #{packets.class}"
39
+ end
40
40
  end
41
41
 
42
42
  # Parse game and player data to hash
@@ -48,7 +48,7 @@ module GamespyQuery
48
48
  data[:game] = {} # Key: InfoKey, Value: InfoValue
49
49
  data[:players] = {} # Key: InfoType, Value: Array of Values
50
50
  player_info = false
51
- player_data = "".encode "ASCII-8BIT"
51
+ player_data = ""
52
52
 
53
53
  # Parse the packets
54
54
  @packets.each do |packet|
@@ -70,12 +70,12 @@ module GamespyQuery
70
70
  else
71
71
  # GameData-only
72
72
  data[:game].merge!(parse_game_data(packet))
73
- end
73
+ end
74
74
  end
75
75
  end
76
76
 
77
77
  # Parse player_data
78
- data[:players] = parse_player_data(player_data)
78
+ data[:players] = parse_player_data(encode_string player_data)
79
79
 
80
80
  data
81
81
  end
@@ -168,7 +168,7 @@ module GamespyQuery
168
168
  player_data[player_data.keys[i]] << encode_string(entry.sub(STR_X0, STR_EMPTY))
169
169
  str.sub!(entry, STR_EMPTY)
170
170
  end
171
-
171
+
172
172
  # Search for SIX string to overwrite last entry
173
173
  new_player_data = []
174
174
  overwrite = false
@@ -177,7 +177,6 @@ module GamespyQuery
177
177
  overwrite = true # tag so that the next entry will overwrite the latest entry
178
178
  next # ignore
179
179
  else
180
- info = clean(info) if [2,3].include?(i) # Apply data_type conversion for Score and Deaths
181
180
  if overwrite
182
181
  new_player_data[-1] = info # Overwrite latest entry
183
182
  overwrite = false # done the overwrite
@@ -5,83 +5,6 @@ require 'yaml'
5
5
  require 'socket'
6
6
 
7
7
  module GamespyQuery
8
- # Provides socket functionality on multiple platforms
9
- # TODO
10
- module MultiSocket
11
- # Create socket
12
- def create_socket(*params)
13
- Tools.debug {"Creating socket #{params}"}
14
- _create_socket(*params)
15
- end
16
-
17
- # Write socket
18
- def socket_send(*params)
19
- Tools.debug {"Sending socket #{params}"}
20
- _socket_send(*params)
21
- end
22
-
23
- # Read socket
24
- def socket_receive(*params)
25
- Tools.debug {"Receiving socket #{params}"}
26
- _socket_receive(*params)
27
- end
28
-
29
- # Close socket
30
- def socket_close(*params)
31
- Tools.debug {"Closing socket #{params}"}
32
- @s.close
33
- end
34
-
35
- if RUBY_PLATFORM =~ /mswin32/
36
- include System::Net
37
- include System::Net::Sockets
38
-
39
- # Create socket
40
- def _create_socket(host, port)
41
- @ip_end_point = IPEndPoint.new(IPAddress.Any, 0)
42
- @s = UdpClient.new
43
- @s.client.receive_timeout = DEFAULT_TIMEOUT * 1000
44
- @s.connect(host, port.to_i)
45
- end
46
-
47
- # Write socket
48
- def _socket_send(packet)
49
- @s.Send(packet, packet.length)
50
- end
51
-
52
- # Read socket
53
- def _socket_receive
54
- @s.Receive(@ip_end_point)
55
- end
56
-
57
- else
58
-
59
- # Create socket
60
- def _create_socket(host, port)
61
- @s = UDPSocket.new
62
- @s.connect(host, port)
63
- end
64
-
65
- # Write socket
66
- def _socket_send(packet)
67
- @s.puts(packet)
68
- end
69
-
70
- # Read socket
71
- def _socket_receive
72
- begin
73
- Timeout::timeout(DEFAULT_TIMEOUT) do
74
- @s.recvfrom(RECEIVE_SIZE)
75
- end
76
- rescue Timeout::Error
77
- raise TimeoutError, "TimeOut on #{self}"
78
- ensure
79
- @s.close
80
- end
81
- end
82
- end
83
- end
84
-
85
8
  # Provides direct connection functionality to gamespy enabled game servers
86
9
  # This query contains up to 7x more information than the gamespy master browser query
87
10
  # For example, player lists with info (teams, scores, deaths) are only available by using direct connection
@@ -117,6 +40,8 @@ module GamespyQuery
117
40
  RX_CHALLENGE2 = /[^0-9\-]/si
118
41
  RX_SPLITNUM = /^splitnum\x00(.)/i
119
42
 
43
+ PLATFORM_IR = /-mswin32/
44
+
120
45
  # TODO: Support pings
121
46
  # TODO: Handle .NET native sockets
122
47
  STATE_INIT, STATE_SENT_CHALLENGE, STATE_RECEIVED_CHALLENGE, STATE_SENT_CHALLENGE_RESPONSE, STATE_RECEIVE_DATA, STATE_READY = 0, 1, 2, 3, 4, 5
@@ -170,15 +95,11 @@ module GamespyQuery
170
95
  else
171
96
  raise NotInWriteState, "NotInWriteState, #{self}"
172
97
  end
173
- rescue NotInWriteState => e
174
- r = false
175
- self.failed = true
176
- close unless closed?
177
- rescue => e
178
- Tools.log_exception e
98
+ rescue nil, Exception => e
179
99
  self.failed = true
180
100
  r = nil
181
101
  close unless closed?
102
+ raise e
182
103
  end
183
104
 
184
105
  =begin
@@ -192,6 +113,31 @@ module GamespyQuery
192
113
  r
193
114
  end
194
115
 
116
+ # Temp Workaround for IO.select issue on IR
117
+ def _read_non_block
118
+ if RUBY_PLATFORM =~ PLATFORM_IR
119
+ time_end = Time.now + DEFAULT_TIMEOUT
120
+ success = false
121
+ until success || Time.now >= time_end
122
+ begin
123
+ d = self.recvfrom_nonblock(RECEIVE_SIZE)
124
+ success = d
125
+ rescue SocketError
126
+
127
+ rescue nil, Exception => e
128
+ Tools.log_exception(e)
129
+ end
130
+ end
131
+ if success
132
+ success
133
+ else
134
+ raise TimeOutError, "The read operation has timedout"
135
+ end
136
+ else
137
+ self.recvfrom_nonblock(RECEIVE_SIZE)
138
+ end
139
+ end
140
+
195
141
  # Handle the read state
196
142
  def handle_read
197
143
  # Tools.debug {"Read: #{self.inspect}, #{self.state}"}
@@ -200,14 +146,14 @@ module GamespyQuery
200
146
  begin
201
147
  case self.state
202
148
  when STATE_SENT_CHALLENGE
203
- data = self.recvfrom_nonblock(RECEIVE_SIZE)
149
+ data = _read_non_block
204
150
  Tools.debug {"Read (1): #{self.inspect}: #{data}"}
205
151
 
206
152
  handle_challenge data[0]
207
153
 
208
154
  self.state = STATE_RECEIVED_CHALLENGE
209
155
  when STATE_SENT_CHALLENGE_RESPONSE, STATE_RECEIVE_DATA
210
- data = self.recvfrom_nonblock(RECEIVE_SIZE)
156
+ data = _read_non_block
211
157
  Tools.debug {"Read (3,4): #{self.inspect}: #{data}"}
212
158
  self.state = STATE_RECEIVE_DATA
213
159
 
@@ -227,16 +173,11 @@ module GamespyQuery
227
173
  else
228
174
  raise NotInReadState, "NotInReadState, #{self}"
229
175
  end
230
- rescue NotInReadState => e
231
- r = false
232
- self.failed = true
233
- close unless closed?
234
- rescue => e
235
- # TODO: Simply raise the exception?
236
- Tools.log_exception(e)
176
+ rescue nil, Exception => e
237
177
  self.failed = true
238
178
  r = nil
239
179
  close unless closed?
180
+ raise e
240
181
  end
241
182
  r
242
183
  end
@@ -290,6 +231,7 @@ module GamespyQuery
290
231
  # @param [String] reply Reply from server
291
232
  def sync reply = self.fetch
292
233
  game_data, key = {}, nil
234
+ Tools.debug {"DATA: #{reply.inspect}"}
293
235
  return game_data if reply.nil? || reply.empty?
294
236
 
295
237
  parser = Parser.new(reply)
@@ -305,18 +247,19 @@ module GamespyQuery
305
247
 
306
248
  # Fetch all packets from socket
307
249
  def fetch
250
+ Tools.debug {"FUCK ME"}
308
251
  pings = []
309
252
  r = self.data
310
253
  begin
311
- until valid?
254
+ until valid? || failed
312
255
  if handle_state
313
- if IO.select(nil, [self], nil, DEFAULT_TIMEOUT)
256
+ if RUBY_PLATFORM =~ PLATFORM_IR || IO.select(nil, [self], nil, DEFAULT_TIMEOUT)
314
257
  handle_write
315
258
  else
316
259
  raise TimeOutError, "TimeOut during write, #{self}"
317
260
  end
318
261
  else
319
- if IO.select([self], nil, nil, DEFAULT_TIMEOUT)
262
+ if RUBY_PLATFORM =~ PLATFORM_IR || IO.select([self], nil, nil, DEFAULT_TIMEOUT)
320
263
  handle_read
321
264
  else
322
265
  raise TimeOutError, "TimeOut during read, #{self}"
@@ -341,4 +284,83 @@ module GamespyQuery
341
284
  r
342
285
  end
343
286
  end
287
+
288
+ # Provides socket functionality on multiple platforms
289
+ # TODO
290
+ =begin
291
+ module MultiSocket
292
+ # Create socket
293
+ def create_socket(*params)
294
+ Tools.debug {"Creating socket #{params}"}
295
+ _create_socket(*params)
296
+ end
297
+
298
+ # Write socket
299
+ def socket_send(*params)
300
+ Tools.debug {"Sending socket #{params}"}
301
+ _socket_send(*params)
302
+ end
303
+
304
+ # Read socket
305
+ def socket_receive(*params)
306
+ Tools.debug {"Receiving socket #{params}"}
307
+ _socket_receive(*params)
308
+ end
309
+
310
+ # Close socket
311
+ def socket_close(*params)
312
+ Tools.debug {"Closing socket #{params}"}
313
+ @s.close
314
+ end
315
+
316
+ if RUBY_PLATFORM =~ /mswin32/
317
+ include System::Net
318
+ include System::Net::Sockets
319
+
320
+ # Create socket
321
+ def _create_socket(host, port)
322
+ @ip_end_point = IPEndPoint.new(IPAddress.Any, 0)
323
+ @s = UdpClient.new
324
+ @s.client.receive_timeout = DEFAULT_TIMEOUT * 1000
325
+ @s.connect(host, port.to_i)
326
+ end
327
+
328
+ # Write socket
329
+ def _socket_send(packet)
330
+ @s.Send(packet, packet.length)
331
+ end
332
+
333
+ # Read socket
334
+ def _socket_receive
335
+ @s.Receive(@ip_end_point)
336
+ end
337
+
338
+ else
339
+
340
+ # Create socket
341
+ def _create_socket(host, port)
342
+ @s = UDPSocket.new
343
+ @s.connect(host, port)
344
+ end
345
+
346
+ # Write socket
347
+ def _socket_send(packet)
348
+ @s.puts(packet)
349
+ end
350
+
351
+ # Read socket
352
+ def _socket_receive
353
+ begin
354
+ Timeout::timeout(DEFAULT_TIMEOUT) do
355
+ @s.recvfrom(RECEIVE_SIZE)
356
+ end
357
+ rescue Timeout::Error
358
+ raise TimeoutError, "TimeOut on #{self}"
359
+ ensure
360
+ @s.close
361
+ end
362
+ end
363
+ end
364
+ end
365
+ =end
344
366
  end
@@ -54,10 +54,10 @@ module GamespyQuery
54
54
  Tools.debug {"Sockets: #{queue.size}, AddrsLeft: #{@addrs.size}, ReadReady: #{"#{ready[0].size} / #{read_sockets.size}, WriteReady: #{ready[1].size} / #{write_sockets.size}, ExcReady: #{ready[2].size} / #{queue.size}" unless ready.nil?}"}
55
55
 
56
56
  # Read
57
- ready[0].each { |s| queue.delete(s) unless s.handle_read() }
57
+ ready[0].each { |s| begin; s.handle_read(); rescue nil, Exception => e; queue.delete(s); end }
58
58
 
59
59
  # Write
60
- ready[1].each { |s| queue.delete(s) unless s.handle_write() }
60
+ ready[1].each { |s| begin; s.handle_write(); rescue nil, Exception => e; queue.delete(s); end }
61
61
 
62
62
  # Exceptions
63
63
  #ready[2].each { |s| queue.delete(s) unless s.handle_exc }
@@ -104,21 +104,3 @@ module GamespyQuery
104
104
  end
105
105
  end
106
106
  end
107
-
108
- if $0 == __FILE__
109
- require_relative 'master'
110
- srv = File.open(ARGV[0] || "servers.txt") { |f| f.read }
111
- master = GamespyQuery::Master.new
112
- addrs = master.get_server_list srv
113
-
114
- time_start = Time.now
115
- sm = GamespyQuery::SocketMaster.new(addrs)
116
- sockets = sm.process!
117
- time_taken = Time.now - time_start
118
-
119
- cool = sockets.count {|v| v.valid? }
120
- dude = sockets.size - cool
121
-
122
- puts "Success: #{cool}, Failed: #{dude}"
123
- puts "Took: #{time_taken}s"
124
- end
@@ -1,4 +1,4 @@
1
1
  module GamespyQuery
2
2
  # Version of the library
3
- VERSION = "0.2.0pre2"
3
+ VERSION = "0.2.0pre3"
4
4
  end
data/lib/gamespy_query.rb CHANGED
@@ -22,19 +22,3 @@ module GamespyQuery
22
22
  "GamespyQuery version #{VERSION}"
23
23
  end
24
24
  end
25
-
26
-
27
- if $0 == __FILE__
28
- host, port = if ARGV.size > 1
29
- ARGV
30
- else
31
- ARGV[0].split(":")
32
- end
33
- time_start = Time.now
34
- g = GamespyQuery::Socket.new("#{host}:#{port}")
35
- r = g.sync
36
- time_taken = Time.now - time_start
37
- puts "Took: #{time_taken}s"
38
- exit unless r
39
- puts r.to_yaml
40
- end
@@ -19,8 +19,8 @@ context "Funcs" do
19
19
  # TODO: This method doesnt do anything atm
20
20
  asserts("strip_tags") { topic.strip_tags "test" }.equals "test"
21
21
 
22
- asserts("clean integer") { topic.clean "1" }.equals 1
23
- asserts("clean float") { topic.clean "1.5" }.equals 1.5
22
+ asserts("convert integer") { topic.convert_type "1" }.equals 1
23
+ asserts("convert float") { topic.convert_type "1.5" }.equals 1.5
24
24
 
25
25
  asserts("encode_string") { topic.encode_string("test encoding").encoding }.equals Encoding.find("UTF-8")
26
26
 
@@ -16,8 +16,8 @@ context "Parser" do
16
16
  setup { topic[:game] }
17
17
  denies("Game data") { topic }.empty
18
18
 
19
- asserts("gamever") { topic["gamever"] }.equals "1.59.79548"
20
- asserts("sv_battleye") { topic["sv_battleye"] }.equals "1"
19
+ asserts("gamever") { topic[:gamever] }.equals "1.59.79548"
20
+ asserts("sv_battleye") { topic[:sv_battleye] }.equals "1"
21
21
  end
22
22
 
23
23
  context "Players data" do
@@ -32,15 +32,15 @@ context "Parser" do
32
32
  context "First element" do
33
33
  asserts("Name") { topic[:names][0] }.equals "Skilllos"
34
34
  asserts("Team") { topic[:teams][0] }.equals ""
35
- asserts("Score") { topic[:scores][0] }.equals 371
36
- asserts("Deaths") { topic[:deaths][0] }.equals 24
35
+ asserts("Score") { topic[:scores][0] }.equals "371"
36
+ asserts("Deaths") { topic[:deaths][0] }.equals "24"
37
37
  end
38
38
 
39
39
  context "Tenth element" do
40
40
  asserts("Name") { topic[:names][10] }.equals "DrHat"
41
41
  asserts("Team") { topic[:teams][10] }.equals ""
42
- asserts("Score") { topic[:scores][10] }.equals 37
43
- asserts("Deaths") { topic[:deaths][10] }.equals 8
42
+ asserts("Score") { topic[:scores][10] }.equals "37"
43
+ asserts("Deaths") { topic[:deaths][10] }.equals "8"
44
44
  end
45
45
  end
46
46
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gamespy_query
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0pre2
4
+ version: 0.2.0pre3
5
5
  prerelease: 5
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-03-09 00:00:00.000000000 Z
12
+ date: 2012-03-10 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: cri
16
- requirement: &15149120 !ruby/object:Gem::Requirement
16
+ requirement: &10524320 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *15149120
24
+ version_requirements: *10524320
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: riot
27
- requirement: &15148220 !ruby/object:Gem::Requirement
27
+ requirement: &10523880 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '0'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *15148220
35
+ version_requirements: *10523880
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: yard
38
- requirement: &15147420 !ruby/object:Gem::Requirement
38
+ requirement: &10523420 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,7 +43,7 @@ dependencies:
43
43
  version: '0'
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *15147420
46
+ version_requirements: *10523420
47
47
  description: ''
48
48
  email:
49
49
  - sb@dev-heaven.net