Repo created
This commit is contained in:
parent
4af19165ec
commit
68073add76
12458 changed files with 12350765 additions and 2 deletions
278
tools/python/test_server/server/ResponseProvider.py
Normal file
278
tools/python/test_server/server/ResponseProvider.py
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
from __future__ import print_function
|
||||
|
||||
import jsons
|
||||
import logging
|
||||
import os
|
||||
|
||||
# Should match size defined in platform/platform_tests/downloader_tests/downloader_test.cpp
|
||||
BIG_FILE_SIZE = 47684
|
||||
|
||||
|
||||
class Payload:
|
||||
def __init__(self, message, response_code=200, headers={}):
|
||||
self.__response_code = response_code
|
||||
self.__message = message if type(message) is bytes else message.encode('utf8')
|
||||
self.__headers = headers
|
||||
|
||||
|
||||
def response_code(self):
|
||||
"""
|
||||
Response code to send to the client.
|
||||
"""
|
||||
return self.__response_code
|
||||
|
||||
|
||||
def message(self):
|
||||
"""
|
||||
The message to send to the client.
|
||||
"""
|
||||
return self.__message
|
||||
|
||||
|
||||
def length(self):
|
||||
"""
|
||||
The length of the response.
|
||||
"""
|
||||
return len(self.message())
|
||||
|
||||
|
||||
def headers(self):
|
||||
"""
|
||||
The headers to be sent to the client. Please, note, that these do not include
|
||||
the Content-Length header, which you need to send separately.
|
||||
"""
|
||||
return self.__headers
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return "{}: {}: {}".format(self.response_code(), self.length(), self.message())
|
||||
|
||||
|
||||
class ResponseProviderMixin:
|
||||
"""
|
||||
A mixin (basically, an interface) that the web-server that we might use relies on.
|
||||
|
||||
In this implementation, the job of the web-server is just to get the request
|
||||
(the url and the headers), and to send the response as it knows how. It isn't
|
||||
its job to decide how to respond to what request. It is the job of the
|
||||
ResponseProvider.
|
||||
|
||||
In your web-server you should initialize the ResponseProvider, and ask it for
|
||||
|
||||
response_for_url_and_headers(url, headers)
|
||||
|
||||
Which will return a Payload object that the server must send as response.
|
||||
|
||||
The server might be notified when a particular request has been received:
|
||||
|
||||
got_pinged(self) - someone sent a ping request. The Response provider will
|
||||
respond with "pong" and call this method of the server. You might want to
|
||||
increment the count of active users, for ping is the request that new instances
|
||||
of servers send to check if other servers are currently serving.
|
||||
|
||||
kill(self) - someone sent the kill request, which means that that someone
|
||||
no longer needs this server to serve. You might want to decrement the count of
|
||||
active users and/or stop the server.
|
||||
"""
|
||||
|
||||
def dispatch_response(self, payload):
|
||||
"""
|
||||
Define this mehtod to dispatch the response received from the ResponseProvider
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def got_pinged(self):
|
||||
"""
|
||||
A ping request has been received. In most scenarios it means that the number of
|
||||
users of this server has increased by 1.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def kill(self):
|
||||
"""
|
||||
Someone no longer needs this server. Decrement the number of users and stop
|
||||
the server if the number fell to 0.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class ResponseProvider:
|
||||
def __init__(self, delegate):
|
||||
self.headers = list()
|
||||
self.delegate = delegate
|
||||
self.byterange = None
|
||||
self.is_chunked = False
|
||||
self.response_code = 200
|
||||
|
||||
|
||||
def pong(self):
|
||||
self.delegate.got_pinged()
|
||||
return Payload("pong")
|
||||
|
||||
|
||||
def my_id(self):
|
||||
return Payload(str(os.getpid()))
|
||||
|
||||
|
||||
def strip_query(self, url):
|
||||
query_start = url.find("?")
|
||||
if (query_start > 0):
|
||||
return url[:query_start]
|
||||
return url
|
||||
|
||||
|
||||
def response_for_url_and_headers(self, url, headers):
|
||||
self.headers = headers
|
||||
self.chunk_requested()
|
||||
url = self.strip_query(url)
|
||||
try:
|
||||
return {
|
||||
"/unit_tests/1.txt": self.test1,
|
||||
"/unit_tests/notexisting_unittest": self.test_404,
|
||||
"/unit_tests/permanent": self.test_301,
|
||||
"/unit_tests/47kb.file": self.test_47_kb,
|
||||
# Following two URIs are used to test downloading failures on different platforms.
|
||||
"/unit_tests/mac/1234/Uruguay.mwm": self.test_404,
|
||||
"/unit_tests/linux/1234/Uruguay.mwm": self.test_404,
|
||||
"/ping": self.pong,
|
||||
"/kill": self.kill,
|
||||
"/id": self.my_id,
|
||||
"/partners/time": self.partners_time,
|
||||
"/partners/price": self.partners_price,
|
||||
"/booking/hotelAvailability": self.partners_hotel_availability,
|
||||
"/booking/deals": self.partners_hotels_with_deals,
|
||||
"/booking/blockAvailability": self.partners_block_availability,
|
||||
"/partners/taxi_info": self.partners_yandex_taxi_info,
|
||||
"/partners/get-offers-in-bbox/": self.partners_rent_nearby,
|
||||
"/partners/CalculateByCoords": self.partners_calculate_by_coords,
|
||||
"/gallery/v2/search/": self.promo_gallery_city,
|
||||
"/single/empty/gallery/v2/search/": self.promo_gallery_city_single_empty,
|
||||
"/single/gallery/v2/search/": self.promo_gallery_city_single,
|
||||
"/partners/oauth/token": self.freenow_auth_token,
|
||||
"/partners/service-types": self.freenow_service_types,
|
||||
"/gallery/v2/map": self.guides_on_map_gallery,
|
||||
"/partners/get_supported_tariffs": self.citymobil_supported_tariffs,
|
||||
"/partners/calculate_price": self.citymobil_calculate_price,
|
||||
}.get(url, self.test_404)()
|
||||
except Exception as e:
|
||||
logging.error("test_server: Can't build server response", exc_info=e)
|
||||
return self.test_404()
|
||||
|
||||
|
||||
def chunk_requested(self):
|
||||
if "range" in self.headers:
|
||||
self.is_chunked = True
|
||||
self.response_code = 206
|
||||
meaningful_string = self.headers["range"][6:]
|
||||
first, last = meaningful_string.split("-")
|
||||
self.byterange = (int(first), int(last))
|
||||
|
||||
|
||||
def trim_message(self, message):
|
||||
if not self.is_chunked:
|
||||
return message
|
||||
return message[self.byterange[0]: self.byterange[1] + 1]
|
||||
|
||||
|
||||
def test1(self):
|
||||
init_message = "Test1"
|
||||
message = self.trim_message(init_message)
|
||||
size = len(init_message)
|
||||
self.check_byterange(size)
|
||||
headers = self.chunked_response_header(size)
|
||||
|
||||
return Payload(message, self.response_code, headers)
|
||||
|
||||
|
||||
def test_404(self):
|
||||
return Payload("", response_code=404)
|
||||
|
||||
|
||||
def test_301(self):
|
||||
return Payload("", 301, {"Location" : "google.com"})
|
||||
|
||||
|
||||
def check_byterange(self, size):
|
||||
if self.byterange is None:
|
||||
self.byterange = (0, size)
|
||||
|
||||
def chunked_response_header(self, size):
|
||||
return {
|
||||
"Content-Range" : "bytes {start}-{end}/{out_of}".format(start=self.byterange[0],
|
||||
end=self.byterange[1], out_of=size)
|
||||
}
|
||||
|
||||
|
||||
def test_47_kb(self):
|
||||
self.check_byterange(BIG_FILE_SIZE)
|
||||
headers = self.chunked_response_header(BIG_FILE_SIZE)
|
||||
message = self.trim_message(self.message_for_47kb_file())
|
||||
|
||||
return Payload(message, self.response_code, headers)
|
||||
|
||||
|
||||
def message_for_47kb_file(self):
|
||||
message = []
|
||||
for i in range(0, BIG_FILE_SIZE + 1):
|
||||
message.append(i // 256)
|
||||
message.append(i % 256)
|
||||
|
||||
return bytes(message)
|
||||
|
||||
|
||||
# Partners_api_tests
|
||||
def partners_time(self):
|
||||
return Payload(jsons.PARTNERS_TIME)
|
||||
|
||||
|
||||
def partners_price(self):
|
||||
return Payload(jsons.PARTNERS_PRICE)
|
||||
|
||||
def partners_hotel_availability(self):
|
||||
return Payload(jsons.HOTEL_AVAILABILITY)
|
||||
|
||||
def partners_hotels_with_deals(self):
|
||||
return Payload(jsons.HOTELS_WITH_DEALS)
|
||||
|
||||
def partners_block_availability(self):
|
||||
return Payload(jsons.BLOCK_AVAILABILITY)
|
||||
|
||||
def partners_yandex_taxi_info(self):
|
||||
return Payload(jsons.PARTNERS_TAXI_INFO)
|
||||
|
||||
def partners_rent_nearby(self):
|
||||
return Payload(jsons.PARTNERS_RENT_NEARBY)
|
||||
|
||||
def partners_calculate_by_coords(self):
|
||||
return Payload(jsons.PARTNERS_CALCULATE_BY_COORDS)
|
||||
|
||||
def promo_gallery_city(self):
|
||||
return Payload(jsons.PROMO_GALLERY_CITY)
|
||||
|
||||
def promo_gallery_city_single_empty(self):
|
||||
return Payload(jsons.PROMO_GALLERY_CITY_SINGLE_EMPTY)
|
||||
|
||||
def promo_gallery_city_single(self):
|
||||
return Payload(jsons.PROMO_GALLERY_CITY_SINGLE)
|
||||
|
||||
def freenow_auth_token(self):
|
||||
return Payload(jsons.FREENOW_AUTH_TOKEN)
|
||||
|
||||
def freenow_service_types(self):
|
||||
return Payload(jsons.FREENOW_SERVICE_TYPES)
|
||||
|
||||
def guides_on_map_gallery(self):
|
||||
return Payload(jsons.GUIDES_ON_MAP_GALLERY)
|
||||
|
||||
def citymobil_supported_tariffs(self):
|
||||
return Payload(jsons.CITYMOBIL_SUPPORTED_TARIFFS)
|
||||
|
||||
def citymobil_calculate_price(self):
|
||||
return Payload(jsons.CITYMOBIL_CALCULATE_PRICE)
|
||||
|
||||
def kill(self):
|
||||
logging.debug("Kill called in ResponseProvider")
|
||||
self.delegate.kill()
|
||||
return Payload("Bye...")
|
||||
180
tools/python/test_server/server/SiblingKiller.py
Normal file
180
tools/python/test_server/server/SiblingKiller.py
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
from __future__ import print_function
|
||||
|
||||
import logging
|
||||
|
||||
import os
|
||||
import re
|
||||
from urllib.error import URLError
|
||||
from urllib.request import urlopen
|
||||
import socket
|
||||
from subprocess import Popen, PIPE
|
||||
from time import sleep
|
||||
import sys
|
||||
|
||||
|
||||
import logging
|
||||
|
||||
class SiblingKiller:
|
||||
|
||||
def __init__(self, port, ping_timeout):
|
||||
self.all_processes = self.ps_dash_w()
|
||||
self.all_pids = self.all_process_ids()
|
||||
self.__allow_serving = False
|
||||
self.__my_pid = self.my_process_id()
|
||||
self.port = port
|
||||
self.ping_timeout = ping_timeout
|
||||
logging.debug(f"Sibling killer: my process id = {self.__my_pid}")
|
||||
|
||||
|
||||
def allow_serving(self):
|
||||
return self.__allow_serving
|
||||
|
||||
def give_process_time_to_kill_port_user(self):
|
||||
sleep(5)
|
||||
|
||||
|
||||
def kill_siblings(self):
|
||||
"""
|
||||
The idea is to get the list of all processes by the current user, check which one of them is using the port.
|
||||
If there is such a process, let's wait for 10 seconds for it to start serving, if it doesn't, kill it.
|
||||
|
||||
If there is NO such process, let's see if there is a process with the same name as us but with a lower process id.
|
||||
We shall wait for 10 seconds for it to start, if it doesn't, kill it.
|
||||
|
||||
If we are the process with the same name as ours and with the lowest process id, let's start serving and kill everyone else.
|
||||
"""
|
||||
|
||||
if self.wait_for_server():
|
||||
self.__allow_serving = False
|
||||
logging.debug("There is a server that is currently serving on our port... Disallowing to start a new one")
|
||||
return
|
||||
|
||||
logging.debug("There are no servers that are currently serving. Will try to kill our siblings.")
|
||||
|
||||
|
||||
sibs = list(self.siblings())
|
||||
|
||||
for sibling in sibs:
|
||||
logging.debug(f"Checking whether we should kill sibling id: {sibling}")
|
||||
|
||||
self.give_process_time_to_kill_port_user()
|
||||
|
||||
if self.wait_for_server():
|
||||
serving_pid = self.serving_process_id()
|
||||
if serving_pid:
|
||||
logging.debug(f"There is a serving sibling with process id: {serving_pid}")
|
||||
self.kill(pids=list(map(lambda x: x != serving_pid, sibs)))
|
||||
self.__allow_serving = False
|
||||
return
|
||||
else:
|
||||
self.kill(pid=sibling)
|
||||
|
||||
self.kill_process_on_port() # changes __allow_serving to True if the process was alive and serving
|
||||
|
||||
|
||||
|
||||
def kill(self, pid=0, pids=[]):
|
||||
if not pid and not pids:
|
||||
logging.debug("There are no siblings to kill")
|
||||
return
|
||||
if pid and pids:
|
||||
raise Exception("Use either one pid or multiple pids")
|
||||
|
||||
hitlist = ""
|
||||
if pid:
|
||||
hitlist = str(pid)
|
||||
if pids:
|
||||
hitlist = " ".join(map(str, pids))
|
||||
|
||||
command = f"kill -9 {hitlist}"
|
||||
self.exec_command(command)
|
||||
|
||||
|
||||
def siblings(self):
|
||||
my_name = self.my_process_name()
|
||||
return filter(lambda x: x < self.__my_pid,
|
||||
map(lambda x: int(x.split(" ")[1]),
|
||||
filter(lambda x: my_name in x, self.all_processes)))
|
||||
|
||||
|
||||
def kill_process_on_port(self):
|
||||
process_on_port = self.process_using_port(self.port)
|
||||
|
||||
if not self.wait_for_server():
|
||||
self.kill(pid=process_on_port)
|
||||
self.__allow_serving = True
|
||||
|
||||
|
||||
def all_process_ids(self):
|
||||
pid = lambda x: int(x.split(" ")[1])
|
||||
return map(pid, self.all_processes)
|
||||
|
||||
|
||||
def process_using_port(self, port):
|
||||
def isListenOnPort(pid):
|
||||
info_line = self.exec_command(f"lsof -a -p{pid} -i4")
|
||||
return info_line.endswith("(LISTEN)") and str(port) in info_line
|
||||
|
||||
listening_process = list(filter(isListenOnPort, self.all_pids))
|
||||
|
||||
if len(listening_process) > 1:
|
||||
pass
|
||||
# We should panic here
|
||||
|
||||
if not listening_process:
|
||||
return None
|
||||
|
||||
return listening_process[0]
|
||||
|
||||
|
||||
def my_process_id(self):
|
||||
return os.getpid()
|
||||
|
||||
|
||||
def my_process_name(self):
|
||||
return " ".join(sys.argv)
|
||||
|
||||
|
||||
def ps_dash_w(self):
|
||||
not_header = lambda x: x and not x.startswith("UID")
|
||||
output = self.exec_command("ps -f").split("\n")
|
||||
return list(filter(not_header, list(re.sub("\s{1,}", " ", x.strip()) for x in output)))
|
||||
|
||||
|
||||
def wait_for_server(self):
|
||||
for i in range(0, 2):
|
||||
if self.ping(): # unsuccessful ping takes 5 seconds (look at PING_TIMEOUT) iff there is a dead server occupying the port
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def ping(self):
|
||||
html = None
|
||||
try:
|
||||
response = urlopen(f"http://localhost:{self.port}/ping", timeout=self.ping_timeout)
|
||||
html = response.read()
|
||||
except (URLError, socket.timeout):
|
||||
pass
|
||||
|
||||
logging.debug(f"Pinging returned html: {html}")
|
||||
|
||||
return html == "pong"
|
||||
|
||||
|
||||
def serving_process_id(self):
|
||||
try:
|
||||
response = urlopen(f"http://localhost:{self.port}/id", timeout=self.ping_timeout)
|
||||
resp = response.read()
|
||||
id = int(resp)
|
||||
return id
|
||||
|
||||
except:
|
||||
logging.info("Couldn't get id of a serving process (the PID of the server that responded to pinging)")
|
||||
return None
|
||||
|
||||
def exec_command(self, command):
|
||||
logging.debug(f">> {command}")
|
||||
p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE, text=True)
|
||||
output, err = p.communicate()
|
||||
p.wait()
|
||||
return output
|
||||
8
tools/python/test_server/server/config.py
Normal file
8
tools/python/test_server/server/config.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
PORT = 34568
|
||||
|
||||
# timeout for the self destruction timer - how much time
|
||||
# passes between the last request and the server killing
|
||||
# itself
|
||||
LIFESPAN = 180.0
|
||||
|
||||
PING_TIMEOUT = 5 # Nubmer of seconds to wait for ping response
|
||||
957
tools/python/test_server/server/jsons.py
Normal file
957
tools/python/test_server/server/jsons.py
Normal file
|
|
@ -0,0 +1,957 @@
|
|||
# coding=utf-8
|
||||
|
||||
PARTNERS_PRICE = """
|
||||
{
|
||||
"prices": [
|
||||
{
|
||||
"currency_code": "USD",
|
||||
"display_name": "POOL",
|
||||
"distance": 10.93,
|
||||
"duration": 1320,
|
||||
"estimate": "$13-17",
|
||||
"high_estimate": 18,
|
||||
"localized_display_name": "POOL",
|
||||
"low_estimate": 13,
|
||||
"minimum": null,
|
||||
"product_id": "bc300c14-c30d-4d3f-afcb-19b240c16a13",
|
||||
"surge_multiplier": 1.0
|
||||
},
|
||||
{
|
||||
"currency_code": "USD",
|
||||
"display_name": "uberX",
|
||||
"distance": 10.93,
|
||||
"duration": 1320,
|
||||
"estimate": "$15-21",
|
||||
"high_estimate": 21,
|
||||
"localized_display_name": "uberX",
|
||||
"low_estimate": 15,
|
||||
"minimum": 6,
|
||||
"product_id": "dee8691c-8b48-4637-b048-300eee72d58d",
|
||||
"surge_multiplier": 1.0
|
||||
},
|
||||
{
|
||||
"currency_code": "USD",
|
||||
"display_name": "uberX + Car Seat",
|
||||
"distance": 10.93,
|
||||
"duration": 1320,
|
||||
"estimate": "$26-31",
|
||||
"high_estimate": 31,
|
||||
"localized_display_name": "uberX + Car Seat",
|
||||
"low_estimate": 26,
|
||||
"minimum": 15,
|
||||
"product_id": "bc98a16f-ad72-41a3-8624-809ce654ac57",
|
||||
"surge_multiplier": 1.0
|
||||
},
|
||||
{
|
||||
"currency_code": "USD",
|
||||
"display_name": "uberXL",
|
||||
"distance": 10.93,
|
||||
"duration": 1320,
|
||||
"estimate": "$27-36",
|
||||
"high_estimate": 36,
|
||||
"localized_display_name": "uberXL",
|
||||
"low_estimate": 27,
|
||||
"minimum": 7,
|
||||
"product_id": "9ffa937e-7d2e-4bcf-bc2b-ffec4ef24380",
|
||||
"surge_multiplier": 1.0
|
||||
},
|
||||
{
|
||||
"currency_code": "USD",
|
||||
"display_name": "UberBLACK",
|
||||
"distance": 10.93,
|
||||
"duration": 1320,
|
||||
"estimate": "$48-63",
|
||||
"high_estimate": 63,
|
||||
"localized_display_name": "UberBLACK",
|
||||
"low_estimate": 48,
|
||||
"minimum": 15,
|
||||
"product_id": "a52a9012-d73e-4127-8440-f273cddfd307",
|
||||
"surge_multiplier": 1.0
|
||||
},
|
||||
{
|
||||
"currency_code": "USD",
|
||||
"display_name": "BLACK CAR + Car Seat",
|
||||
"distance": 10.93,
|
||||
"duration": 1320,
|
||||
"estimate": "$58-73",
|
||||
"high_estimate": 73,
|
||||
"localized_display_name": "BLACK CAR + Car Seat",
|
||||
"low_estimate": 58,
|
||||
"minimum": 25,
|
||||
"product_id": "2a299c73-098d-47cd-b32c-825cb155f82a",
|
||||
"surge_multiplier": 1.0
|
||||
},
|
||||
{
|
||||
"currency_code": "USD",
|
||||
"display_name": "UberSUV",
|
||||
"distance": 10.93,
|
||||
"duration": 1320,
|
||||
"estimate": "$59-75",
|
||||
"high_estimate": 75,
|
||||
"localized_display_name": "UberSUV",
|
||||
"low_estimate": 59,
|
||||
"minimum": 25,
|
||||
"product_id": "4e6fd14c-3866-40f1-b173-f12aeb8fbbd0",
|
||||
"surge_multiplier": 1.0
|
||||
},
|
||||
{
|
||||
"currency_code": "USD",
|
||||
"display_name": "SUV + Car Seat",
|
||||
"distance": 10.93,
|
||||
"duration": 1320,
|
||||
"estimate": "$65-80",
|
||||
"high_estimate": 80,
|
||||
"localized_display_name": "SUV + Car Seat",
|
||||
"low_estimate": 65,
|
||||
"minimum": 35,
|
||||
"product_id": "74766497-b951-4eae-98c9-a67d87e2c0c4",
|
||||
"surge_multiplier": 1.0
|
||||
},
|
||||
{
|
||||
"currency_code": null,
|
||||
"display_name": "Wheelchair",
|
||||
"distance": 10.93,
|
||||
"duration": 1320,
|
||||
"estimate": "Metered",
|
||||
"high_estimate": null,
|
||||
"localized_display_name": "Wheelchair",
|
||||
"low_estimate": null,
|
||||
"minimum": null,
|
||||
"product_id": "89f38d7a-d184-4054-9f2e-6b57c94143d6",
|
||||
"surge_multiplier": 1.0
|
||||
},
|
||||
{
|
||||
"currency_code": null,
|
||||
"display_name": "uberTAXI",
|
||||
"distance": 10.93,
|
||||
"duration": 1320,
|
||||
"estimate": "Metered",
|
||||
"high_estimate": null,
|
||||
"localized_display_name": "uberTAXI",
|
||||
"low_estimate": null,
|
||||
"minimum": null,
|
||||
"product_id": "f67c83fb-4668-42eb-9aa1-ab32e710c8bf",
|
||||
"surge_multiplier": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
PARTNERS_TIME = """
|
||||
{
|
||||
"times": [
|
||||
{
|
||||
"display_name": "POOL",
|
||||
"estimate": 360,
|
||||
"localized_display_name": "POOL",
|
||||
"product_id": "bc300c14-c30d-4d3f-afcb-19b240c16a13"
|
||||
},
|
||||
{
|
||||
"display_name": "uberX",
|
||||
"estimate": 300,
|
||||
"localized_display_name": "uberX",
|
||||
"product_id": "dee8691c-8b48-4637-b048-300eee72d58d"
|
||||
},
|
||||
{
|
||||
"display_name": "uberXL",
|
||||
"estimate": 360,
|
||||
"localized_display_name": "uberXL",
|
||||
"product_id": "9ffa937e-7d2e-4bcf-bc2b-ffec4ef24380"
|
||||
},
|
||||
{
|
||||
"display_name": "UberBLACK",
|
||||
"estimate": 300,
|
||||
"localized_display_name": "UberBLACK",
|
||||
"product_id": "a52a9012-d73e-4127-8440-f273cddfd307"
|
||||
},
|
||||
{
|
||||
"display_name": "BLACK CAR + Car Seat",
|
||||
"estimate": 300,
|
||||
"localized_display_name": "BLACK CAR + Car Seat",
|
||||
"product_id": "2a299c73-098d-47cd-b32c-825cb155f82a"
|
||||
},
|
||||
{
|
||||
"display_name": "UberSUV",
|
||||
"estimate": 300,
|
||||
"localized_display_name": "UberSUV",
|
||||
"product_id": "4e6fd14c-3866-40f1-b173-f12aeb8fbbd0"
|
||||
},
|
||||
{
|
||||
"display_name": "SUV + Car Seat",
|
||||
"estimate": 300,
|
||||
"localized_display_name": "SUV + Car Seat",
|
||||
"product_id": "74766497-b951-4eae-98c9-a67d87e2c0c4"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
HOTEL_AVAILABILITY = """
|
||||
{
|
||||
"result": [
|
||||
{
|
||||
"hotel_currency_code": "EUR",
|
||||
"hotel_id": 10623,
|
||||
"price": 801
|
||||
},
|
||||
{
|
||||
"hotel_currency_code": "USD",
|
||||
"hotel_id": 10624,
|
||||
"price": 802
|
||||
},
|
||||
{
|
||||
"hotel_currency_code": "RUR",
|
||||
"hotel_id": 10625,
|
||||
"price": 803
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
HOTELS_WITH_DEALS = """
|
||||
{
|
||||
"result": [
|
||||
{
|
||||
"hotel_currency_code": "EUR",
|
||||
"hotel_id": 10622,
|
||||
"price": 801
|
||||
},
|
||||
{
|
||||
"hotel_currency_code": "USD",
|
||||
"hotel_id": 10624,
|
||||
"price": 802
|
||||
},
|
||||
{
|
||||
"hotel_currency_code": "RUR",
|
||||
"hotel_id": 10626,
|
||||
"price": 803
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
BLOCK_AVAILABILITY = """
|
||||
{
|
||||
"result": [
|
||||
{
|
||||
"direct_payment": true,
|
||||
"checkin": "2018-06-16",
|
||||
"hotel_id": 61394,
|
||||
"block": [
|
||||
{
|
||||
"room_description": "Более просторные апартаменты-студио с кухней открытой планировки, телевизором с плоским экраном и бесплатным Wi-Fi.По запросу предоставляется DVD-плеер.",
|
||||
"taxes": "НДС в размере 7 % , городской налог в размере 5 % ",
|
||||
"rack_rate": {
|
||||
"currency": "EUR",
|
||||
"price": 0,
|
||||
"other_currency": {
|
||||
"currency": "RUB",
|
||||
"price": 0
|
||||
}
|
||||
},
|
||||
"block_id": "6139409_116589412_2_1_0",
|
||||
"max_occupancy": 2,
|
||||
"refundable": false,
|
||||
"breakfast_included": true,
|
||||
"is_smart_deal": false,
|
||||
"incremental_price": [
|
||||
{
|
||||
"other_currency": {
|
||||
"currency": "RUB",
|
||||
"price": 8405.46
|
||||
},
|
||||
"price": 116,
|
||||
"currency": "EUR"
|
||||
}
|
||||
],
|
||||
"photos": [
|
||||
{
|
||||
"url_original": "https://q-xx.bstatic.com/images/hotel/max500/437/43793388.jpg",
|
||||
"photo_id": 43793388,
|
||||
"url_max300": "https://q-xx.bstatic.com/images/hotel/max300/437/43793388.jpg",
|
||||
"url_square60": "https://q-xx.bstatic.com/images/hotel/square60/437/43793388.jpg"
|
||||
}
|
||||
],
|
||||
"deposit_required": false,
|
||||
"name": "Синьо Студио 1 - Стоимость не возвращается",
|
||||
"is_last_minute_deal": false,
|
||||
"min_price": {
|
||||
"other_currency": {
|
||||
"currency": "RUB",
|
||||
"price": 8405.46
|
||||
},
|
||||
"price": 116,
|
||||
"currency": "EUR"
|
||||
},
|
||||
"refundable_until": "",
|
||||
"room_id": 6139409
|
||||
}
|
||||
],
|
||||
"checkout": "2018-06-17"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
PARTNERS_TAXI_INFO = """
|
||||
{
|
||||
"currency": "RUB",
|
||||
"distance": 6888.846981748964,
|
||||
"options": [
|
||||
{
|
||||
"class_level": 50,
|
||||
"class_name": "econom",
|
||||
"min_price": 129,
|
||||
"price": 344,
|
||||
"waiting_time": 527.8793726078095
|
||||
},
|
||||
{
|
||||
"class_level": 70,
|
||||
"class_name": "business",
|
||||
"min_price": 239,
|
||||
"price": 504,
|
||||
"waiting_time": 76.37023611385494
|
||||
},
|
||||
{
|
||||
"class_level": 100,
|
||||
"class_name": "comfortplus",
|
||||
"min_price": 239,
|
||||
"price": 557,
|
||||
"waiting_time": 99.0058955445591
|
||||
},
|
||||
{
|
||||
"class_level": 200,
|
||||
"class_name": "minivan",
|
||||
"min_price": 239,
|
||||
"price": 532,
|
||||
"waiting_time": 322.77413167989687
|
||||
},
|
||||
{
|
||||
"class_level": 300,
|
||||
"class_name": "vip",
|
||||
"min_price": 359,
|
||||
"price": 799,
|
||||
"waiting_time": 223.34814145904883
|
||||
}
|
||||
],
|
||||
"time": 1057.7440430297368
|
||||
}
|
||||
"""
|
||||
|
||||
PARTNERS_RENT_NEARBY = """
|
||||
{
|
||||
"clusters": [
|
||||
{
|
||||
"lat": 55.80529,
|
||||
"lng": 37.508274,
|
||||
"offers": [
|
||||
{
|
||||
"flatType": "rooms",
|
||||
"roomsCount": 2,
|
||||
"priceRur": 50000.0,
|
||||
"floorNumber": 8,
|
||||
"floorsCount": 9,
|
||||
"photosCount": 11,
|
||||
"url": "https://cian.ru/rent/flat/159026341",
|
||||
"address": "Ленинградский просп., 77К2"
|
||||
},
|
||||
{
|
||||
"flatType": "rooms",
|
||||
"roomsCount": 1,
|
||||
"priceRur": 39999.0,
|
||||
"floorNumber": 1,
|
||||
"floorsCount": 12,
|
||||
"photosCount": 13,
|
||||
"url": "https://cian.ru/rent/flat/157827964",
|
||||
"address": "Ленинградский просп., 77К2"
|
||||
},
|
||||
{
|
||||
"flatType": "rooms",
|
||||
"roomsCount": 2,
|
||||
"priceRur": 58000.0,
|
||||
"floorNumber": 6,
|
||||
"floorsCount": 9,
|
||||
"photosCount": 9,
|
||||
"url": "https://cian.ru/rent/flat/159523671",
|
||||
"address": "Ленинградский просп., 77К2"
|
||||
}
|
||||
],
|
||||
"count": 3,
|
||||
"url": "https://cian.ru/cat.php?deal_type=rent&offer_type=flat&engine_version=2&house%5B0%5D=26696"
|
||||
},
|
||||
{
|
||||
"lat": 55.805776,
|
||||
"lng": 37.50946,
|
||||
"offers": [
|
||||
{
|
||||
"flatType": "rooms",
|
||||
"roomsCount": 1,
|
||||
"priceRur": 39000.0,
|
||||
"floorNumber": 2,
|
||||
"floorsCount": 9,
|
||||
"photosCount": 9,
|
||||
"url": "https://cian.ru/rent/flat/158786442",
|
||||
"address": "Ленинградский просп., 77А"
|
||||
}
|
||||
],
|
||||
"count": 1,
|
||||
"url": "https://cian.ru/cat.php?deal_type=rent&offer_type=flat&engine_version=2&house%5B0%5D=2009028"
|
||||
},
|
||||
{
|
||||
"lat": 55.808306,
|
||||
"lng": 37.5008,
|
||||
"offers": [
|
||||
{
|
||||
"flatType": "rooms",
|
||||
"roomsCount": 2,
|
||||
"priceRur": 50000.0,
|
||||
"floorNumber": 6,
|
||||
"floorsCount": 10,
|
||||
"photosCount": 9,
|
||||
"url": "https://cian.ru/rent/flat/155419837",
|
||||
"address": "Волоколамское ш., 6"
|
||||
}
|
||||
],
|
||||
"count": 1,
|
||||
"url": "https://cian.ru/cat.php?deal_type=rent&offer_type=flat&engine_version=2&house%5B0%5D=1692086"
|
||||
},
|
||||
{
|
||||
"lat": 55.805999,
|
||||
"lng": 37.503738,
|
||||
"offers": [
|
||||
{
|
||||
"flatType": "rooms",
|
||||
"roomsCount": 2,
|
||||
"priceRur": 70000.0,
|
||||
"floorNumber": 4,
|
||||
"floorsCount": 9,
|
||||
"photosCount": 0,
|
||||
"url": "https://cian.ru/rent/flat/159765700",
|
||||
"address": "Волоколамское ш., 1кА"
|
||||
}
|
||||
],
|
||||
"count": 1,
|
||||
"url": "https://cian.ru/cat.php?deal_type=rent&offer_type=flat&engine_version=2&house%5B0%5D=1009214"
|
||||
},
|
||||
{
|
||||
"lat": 55.805361,
|
||||
"lng": 37.507124,
|
||||
"offers": [
|
||||
{
|
||||
"flatType": "rooms",
|
||||
"roomsCount": 1,
|
||||
"priceRur": 45000.0,
|
||||
"floorNumber": 1,
|
||||
"floorsCount": 9,
|
||||
"photosCount": 6,
|
||||
"url": "https://cian.ru/rent/flat/158673214",
|
||||
"address": "Ленинградский просп., 77К3"
|
||||
},
|
||||
{
|
||||
"flatType": "rooms",
|
||||
"roomsCount": 2,
|
||||
"priceRur": 48000.0,
|
||||
"floorNumber": 5,
|
||||
"floorsCount": 9,
|
||||
"photosCount": 15,
|
||||
"url": "https://cian.ru/rent/flat/158613232",
|
||||
"address": "Ленинградский просп., 77К3"
|
||||
},
|
||||
{
|
||||
"flatType": "rooms",
|
||||
"roomsCount": 1,
|
||||
"priceRur": 45000.0,
|
||||
"floorNumber": 1,
|
||||
"floorsCount": 9,
|
||||
"photosCount": 7,
|
||||
"url": "https://cian.ru/rent/flat/159035369",
|
||||
"address": "Ленинградский просп., 77К3"
|
||||
}
|
||||
],
|
||||
"count": 3,
|
||||
"url": "https://cian.ru/cat.php?deal_type=rent&offer_type=flat&engine_version=2&house%5B0%5D=26698"
|
||||
},
|
||||
{
|
||||
"lat": 55.809226,
|
||||
"lng": 37.504978,
|
||||
"offers": [
|
||||
{
|
||||
"flatType": "rooms",
|
||||
"roomsCount": 2,
|
||||
"priceRur": 60000.0,
|
||||
"floorNumber": 5,
|
||||
"floorsCount": 8,
|
||||
"photosCount": 37,
|
||||
"url": "https://cian.ru/rent/flat/157212858",
|
||||
"address": "Ленинградское ш., 3С1"
|
||||
},
|
||||
{
|
||||
"flatType": "rooms",
|
||||
"roomsCount": 2,
|
||||
"priceRur": 35000.0,
|
||||
"floorNumber": 6,
|
||||
"floorsCount": 8,
|
||||
"photosCount": 5,
|
||||
"url": "https://cian.ru/rent/flat/158689565",
|
||||
"address": "Ленинградское ш., 3С1"
|
||||
}
|
||||
],
|
||||
"count": 2,
|
||||
"url": "https://cian.ru/cat.php?deal_type=rent&offer_type=flat&engine_version=2&house%5B0%5D=583624"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
PARTNERS_CALCULATE_BY_COORDS = """
|
||||
{
|
||||
"ButtonText": null,
|
||||
"Message": null,
|
||||
"PaymentMethods": null,
|
||||
"Prices": null,
|
||||
"ShowFrom": false,
|
||||
"Success": true,
|
||||
"TypeId": 2,
|
||||
"CurrencyCode": "RUB",
|
||||
"FeedTime": 9,
|
||||
"Price": 244,
|
||||
"PriceString": "244.00 ₽"
|
||||
}
|
||||
"""
|
||||
|
||||
PROMO_GALLERY_CITY = """
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"url": "bundle/73af3f02-b8e3-4f60-8ef0-1c3c5cff43ca",
|
||||
"name": "По Виа Рипетта до мавзолея Августа и Алтаря мира",
|
||||
"author": {
|
||||
"key_id": "00000000-0000-0000-0000-000000000000",
|
||||
"name": "The Village"
|
||||
},
|
||||
"image_url": "http://localhost:8000/images/73af3f02-b8e3-4f60-8ef0-1c3c5cff43ca.jpg",
|
||||
"access": "public",
|
||||
"lux_category": {
|
||||
"name": "LUX",
|
||||
"color": "666666"
|
||||
},
|
||||
"tier": "price.tier"
|
||||
},
|
||||
{
|
||||
"url": "bundle/73af3f02-b8e3-4f60-8ef0-1c3c5cff43ca",
|
||||
"name": "Полеты в метро",
|
||||
"author": {
|
||||
"key_id": "00000000-0000-0000-0000-000000000000",
|
||||
"name": "Bmj"
|
||||
},
|
||||
"access": "public",
|
||||
"lux_category": {
|
||||
"color": "000000"
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [],
|
||||
"meta": {
|
||||
"more": "search?city=888"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
PROMO_GALLERY_CITY_SINGLE_EMPTY = """
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"url": "bundle/73af3f02-b8e3-4f60-8ef0-1c3c5cff43ca",
|
||||
"name": "По Виа Рипетта до мавзолея Августа и Алтаря мира",
|
||||
"author": {
|
||||
"key_id": "00000000-0000-0000-0000-000000000000",
|
||||
"name": "The Village"
|
||||
},
|
||||
"image_url": "http://localhost:8000/images/73af3f02-b8e3-4f60-8ef0-1c3c5cff43ca.jpg",
|
||||
"access": "public",
|
||||
"lux_category": {
|
||||
"name": "LUX",
|
||||
"color": "666666"
|
||||
},
|
||||
"tier": "price.tier"
|
||||
}
|
||||
],
|
||||
"errors": [],
|
||||
"meta": {
|
||||
"more": "search?city=888"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
PROMO_GALLERY_CITY_SINGLE = """
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"url": "bundle/73af3f02-b8e3-4f60-8ef0-1c3c5cff43ca",
|
||||
"name": "По Виа Рипетта до мавзолея Августа и Алтаря мира",
|
||||
"author": {
|
||||
"key_id": "00000000-0000-0000-0000-000000000000",
|
||||
"name": "The Village"
|
||||
},
|
||||
"image_url": "http://localhost:8000/images/73af3f02-b8e3-4f60-8ef0-1c3c5cff43ca.jpg",
|
||||
"access": "public",
|
||||
"lux_category": {
|
||||
"name": "LUX",
|
||||
"color": "666666"
|
||||
},
|
||||
"tier": "price.tier",
|
||||
"place": {
|
||||
"name": "Bookmark name",
|
||||
"description": "Bookmark description"
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [],
|
||||
"meta": {
|
||||
"more": "search?city=888"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
FREENOW_AUTH_TOKEN = """
|
||||
{
|
||||
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXV",
|
||||
"token_type": "bearer",
|
||||
"expires_in": 600,
|
||||
"scope": "service-types"
|
||||
}
|
||||
"""
|
||||
|
||||
FREENOW_SERVICE_TYPES = """
|
||||
{
|
||||
"serviceTypes": [
|
||||
{
|
||||
"id": "TAXI",
|
||||
"type": "TAXI",
|
||||
"displayName": "Taxi",
|
||||
"eta": {
|
||||
"value": 0,
|
||||
"displayValue": "0 Minutes"
|
||||
},
|
||||
"fare": {
|
||||
"type": "FIXED",
|
||||
"value": 5000,
|
||||
"currencyCode": "GBP",
|
||||
"displayValue": "5000GBP"
|
||||
},
|
||||
"availablePaymentMethodTypes": [
|
||||
"BUSINESS_ACCOUNT",
|
||||
"CREDIT_CARD",
|
||||
"PAYPAL",
|
||||
"CASH"
|
||||
],
|
||||
"seats": {
|
||||
"max": 4,
|
||||
"values": [],
|
||||
"displayValue": "4"
|
||||
},
|
||||
"availableBookingOptions": [
|
||||
{
|
||||
"name": "COMMENT",
|
||||
"displayName": "COMMENT",
|
||||
"type": "TEXT"
|
||||
},
|
||||
{
|
||||
"name": "MERCEDES",
|
||||
"displayName": "MERCEDES",
|
||||
"type": "BOOLEAN"
|
||||
},
|
||||
{
|
||||
"name": "FAVORITE_DRIVER",
|
||||
"displayName": "FAVORITE_DRIVER",
|
||||
"type": "BOOLEAN"
|
||||
},
|
||||
{
|
||||
"name": "FIVE_STARS",
|
||||
"displayName": "FIVE_STARS",
|
||||
"type": "BOOLEAN"
|
||||
},
|
||||
{
|
||||
"name": "SMALL_ANIMAL",
|
||||
"displayName": "SMALL_ANIMAL",
|
||||
"type": "BOOLEAN"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
GUIDES_ON_MAP_GALLERY = """
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"point": {
|
||||
"lat": 12.345678,
|
||||
"lon": 91.234567
|
||||
},
|
||||
"bundle_counts": {
|
||||
"sights": 1,
|
||||
"outdoor": 0
|
||||
},
|
||||
"extra": {
|
||||
"server_id": "120-3957012735012rffasfaf",
|
||||
"name": "hello",
|
||||
"image_url": "world",
|
||||
"tag": "tag",
|
||||
"bookmark_count": 100,
|
||||
"has_track": true,
|
||||
"tracks_length": 1234.11,
|
||||
"tour_duration": {
|
||||
"hours": 5,
|
||||
"minutes": 17
|
||||
},
|
||||
"ascent": -300
|
||||
}
|
||||
},
|
||||
{
|
||||
"point": {
|
||||
"lat": 91.234567,
|
||||
"lon": 12.345678
|
||||
},
|
||||
"bundle_counts": {
|
||||
"sights": 1,
|
||||
"outdoor": 8
|
||||
},
|
||||
"extra": null
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"suggested_zoom_level": 5
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
CITYMOBIL_SUPPORTED_TARIFFS = """
|
||||
{
|
||||
"tariff_groups": [
|
||||
{
|
||||
"tariff_group_id": 2,
|
||||
"tariff_options": [],
|
||||
"tariff_detail": {
|
||||
"is_waypoints_enabled": false,
|
||||
"is_by_instruction_enabled": true,
|
||||
"is_options_enabled": true,
|
||||
"order_time_type": [
|
||||
"asap",
|
||||
"delay"
|
||||
],
|
||||
"description": "Таксипортация для ежедневных поездок",
|
||||
"luxury_level": 1,
|
||||
"car_models": "Kia Rio, Hyundai Solaris, VW Polo",
|
||||
"car_image": "https://external-storage.city-mobil.ru/generated/storage_files/Polo.png",
|
||||
"fastest_car_image": "https://external-storage.city-mobil.ru/generated/storage_files/econom_vw.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tariff_group_id": 4,
|
||||
"tariff_options": [],
|
||||
"tariff_detail": {
|
||||
"is_waypoints_enabled": false,
|
||||
"is_by_instruction_enabled": true,
|
||||
"is_options_enabled": true,
|
||||
"order_time_type": [
|
||||
"asap",
|
||||
"delay"
|
||||
],
|
||||
"description": "Комфортная таксипортация",
|
||||
"luxury_level": 3,
|
||||
"car_models": "Skoda Octavia, Hyundai Elantra, Kia Cerato",
|
||||
"car_image": "https://external-storage.city-mobil.ru/generated/storage_files/Octavia.png",
|
||||
"fastest_car_image": "https://external-storage.city-mobil.ru/generated/storage_files/comfort_shkoda.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tariff_group_id": 13,
|
||||
"tariff_options": [],
|
||||
"tariff_detail": {
|
||||
"is_waypoints_enabled": false,
|
||||
"is_by_instruction_enabled": true,
|
||||
"is_options_enabled": true,
|
||||
"order_time_type": [
|
||||
"asap",
|
||||
"delay"
|
||||
],
|
||||
"description": "Таксипортация с повышенным комфортом",
|
||||
"luxury_level": 4,
|
||||
"car_models": "Kia Optima, Toyota Camry, Hyundai Sonata",
|
||||
"car_image": "https://external-storage.city-mobil.ru/generated/storage_files/Optima.png",
|
||||
"fastest_car_image": "https://external-storage.city-mobil.ru/generated/storage_files/comfort_plus_kia.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tariff_group_id": 5,
|
||||
"tariff_options": [],
|
||||
"tariff_detail": {
|
||||
"is_waypoints_enabled": false,
|
||||
"is_by_instruction_enabled": true,
|
||||
"is_options_enabled": true,
|
||||
"order_time_type": [
|
||||
"asap",
|
||||
"delay"
|
||||
],
|
||||
"description": "Для важных встреч",
|
||||
"luxury_level": 5,
|
||||
"car_models": "BMW 5, Mercedes E-klasse, Audi A6",
|
||||
"car_image": "https://external-storage.city-mobil.ru/generated/storage_files/E-Klasse.png",
|
||||
"fastest_car_image": "https://external-storage.city-mobil.ru/generated/storage_files/buisness_merce.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tariff_group_id": 3,
|
||||
"tariff_options": [],
|
||||
"tariff_detail": {
|
||||
"is_waypoints_enabled": false,
|
||||
"is_by_instruction_enabled": true,
|
||||
"is_options_enabled": true,
|
||||
"order_time_type": [
|
||||
"asap",
|
||||
"delay"
|
||||
],
|
||||
"description": "",
|
||||
"luxury_level": 0,
|
||||
"car_models": "Ford Focus, Kia Rio, Nissan Almera",
|
||||
"car_image": "",
|
||||
"fastest_car_image": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"tariff_group_id": 7,
|
||||
"tariff_options": [],
|
||||
"tariff_detail": {
|
||||
"is_waypoints_enabled": false,
|
||||
"is_by_instruction_enabled": true,
|
||||
"is_options_enabled": true,
|
||||
"order_time_type": [
|
||||
"asap",
|
||||
"delay"
|
||||
],
|
||||
"description": "Таксипортация для большой компании",
|
||||
"luxury_level": -1,
|
||||
"car_models": "Ford Galaxy, Citroen C4 Picasso, Chevrolet Orlando",
|
||||
"car_image": "https://external-storage.city-mobil.ru/generated/storage_files/C4.png",
|
||||
"fastest_car_image": "https://external-storage.city-mobil.ru/generated/storage_files/miniven_citrienC4.png"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tariff_group_id": 27,
|
||||
"tariff_options": [
|
||||
{
|
||||
"id": 99,
|
||||
"title": "От двери до двери",
|
||||
"type": "delivery_tariff_door_to_door",
|
||||
"is_toggle_selection": true,
|
||||
"description": "Водитель сам заберет посылку у вас и доставит ее адресату до квартиры"
|
||||
}
|
||||
],
|
||||
"tariff_detail": {
|
||||
"is_waypoints_enabled": false,
|
||||
"is_by_instruction_enabled": false,
|
||||
"is_options_enabled": false,
|
||||
"order_time_type": [
|
||||
"asap"
|
||||
],
|
||||
"description": "",
|
||||
"luxury_level": -1,
|
||||
"car_models": "Когда можно не ехать, но нужно передать документы, ключи или чемодан",
|
||||
"car_image": "https://external-storage.city-mobil.ru/generated/storage_files/Delivery_day.png",
|
||||
"fastest_car_image": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
CITYMOBIL_CALCULATE_PRICE = """
|
||||
{
|
||||
"distance_text": "8 км",
|
||||
"duration_text": "15 мин",
|
||||
"id_calculation": "494d1fc56850ae4411e86fe2d902abe9",
|
||||
"prices": [
|
||||
{
|
||||
"coefficient": 1,
|
||||
"fixed_price": true,
|
||||
"has_discount": false,
|
||||
"id_tariff": 4098,
|
||||
"id_tariff_group": 2,
|
||||
"label": "763₽. В пути ~15 мин",
|
||||
"new_user_discount": false,
|
||||
"price": 763,
|
||||
"total_price": 763,
|
||||
"tariff_info": {
|
||||
"name": "Эконом",
|
||||
"price": "763₽. В пути ~15 мин",
|
||||
"car_models": "Kia Rio, Hyundai Solaris, VW Polo, Renault Logan, Skoda Rapid, Nissan Almera, Chevrolet Aveo, Ford Focus.",
|
||||
"car_capacity": "Пассажиров: 4, мест багажа: 2",
|
||||
"link": "https://t.city-mobil.ru/view/tariffs/2/ru?id_tariff=4098",
|
||||
"details": [
|
||||
{
|
||||
"text": "Стоимость поездки",
|
||||
"value": "763₽"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"coefficient": 1,
|
||||
"fixed_price": true,
|
||||
"has_discount": false,
|
||||
"id_tariff": 2,
|
||||
"id_tariff_group": 4,
|
||||
"label": "816₽. В пути ~15 мин",
|
||||
"new_user_discount": false,
|
||||
"price": 816,
|
||||
"total_price": 816,
|
||||
"tariff_info": {
|
||||
"name": "Комфорт",
|
||||
"price": "816₽. В пути ~15 мин",
|
||||
"car_models": "Skoda Octavia, Hyundai Elantra, Kia Cerato, Toyota Camry, Nissan Teana, Ford Mondeo, Kia Ceed, Hyundai Sonata",
|
||||
"car_capacity": "Пассажиров: 4, мест багажа: 2. Кондиционер обязателен.",
|
||||
"link": "https://t.city-mobil.ru/view/tariffs/2/ru?id_tariff=2",
|
||||
"details": [
|
||||
{
|
||||
"text": "Стоимость поездки",
|
||||
"value": "816₽"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"coefficient": 1,
|
||||
"fixed_price": true,
|
||||
"has_discount": false,
|
||||
"id_tariff": 144,
|
||||
"id_tariff_group": 5,
|
||||
"label": "1000₽. В пути ~15 мин",
|
||||
"new_user_discount": false,
|
||||
"price": 1000,
|
||||
"total_price": 1000,
|
||||
"tariff_info": {
|
||||
"name": "Бизнес",
|
||||
"price": "1000₽. В пути ~15 мин",
|
||||
"car_models": "BMW 5, Mercedes E-klasse, Audi A6, Lexus GS, Hyundai Equus",
|
||||
"car_capacity": "Пассажиров: 4, мест багажа: 2",
|
||||
"link": "https://t.city-mobil.ru/view/tariffs/2/ru?id_tariff=144",
|
||||
"details": [
|
||||
{
|
||||
"text": "Стоимость поездки",
|
||||
"value": "1000₽"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"distance": 7250,
|
||||
"duration": 846,
|
||||
"points": ""
|
||||
},
|
||||
"service_status": 1,
|
||||
"eta": 600
|
||||
}
|
||||
"""
|
||||
213
tools/python/test_server/server/testserver.py
Normal file
213
tools/python/test_server/server/testserver.py
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
"""
|
||||
This is a simple web-server that does very few things. It is necessary for
|
||||
the downloader tests.
|
||||
|
||||
Here is the logic behind the initialization:
|
||||
Because several instances of the test can run simultaneously on the Build
|
||||
machine, we have to take this into account and not start another server if
|
||||
one is already running. However, there is a chance that a server will not
|
||||
terminate correctly, and will still hold the port, so we will not be able
|
||||
to initialize another server.
|
||||
|
||||
So before initializing the server, we check if any processes are using the port
|
||||
that we want to use. If we find such a process, we assume that it might be
|
||||
working, and wait for about 10 seconds for it to start serving. If it does not,
|
||||
we kill it.
|
||||
|
||||
Next, we check the name of our process and see if there are other processes
|
||||
with the same name. If there are, we assume that they might start serving any
|
||||
moment. So we iterate over the ones that have PID lower than ours, and wait
|
||||
for them to start serving. If a process doesn't serve, we kill it.
|
||||
|
||||
If we have killed (or someone has) all the processes with PIDs lower than ours,
|
||||
we try to start serving. If we succeed, we kill all other processes with the
|
||||
same name as ours. If we don't someone else will kill us.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
from http.server import BaseHTTPRequestHandler
|
||||
from http.server import HTTPServer
|
||||
from ResponseProvider import Payload
|
||||
from ResponseProvider import ResponseProvider
|
||||
from ResponseProvider import ResponseProviderMixin
|
||||
from SiblingKiller import SiblingKiller
|
||||
from threading import Timer
|
||||
from config import LIFESPAN, PING_TIMEOUT, PORT
|
||||
import os
|
||||
import socket
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
|
||||
import logging
|
||||
import logging.config
|
||||
|
||||
try:
|
||||
from tornado_handler import MainHandler
|
||||
USE_TORNADO = True
|
||||
except:
|
||||
USE_TORNADO = False
|
||||
|
||||
|
||||
logging.basicConfig(format='%(asctime)s %(message)s', level=logging.DEBUG)
|
||||
|
||||
|
||||
class InternalServer(HTTPServer):
|
||||
|
||||
def kill_me(self):
|
||||
self.shutdown()
|
||||
logging.info(f"The server's life has come to an end, pid: {os.getpid()}")
|
||||
|
||||
|
||||
def reset_selfdestruct_timer(self):
|
||||
if self.self_destruct_timer:
|
||||
self.self_destruct_timer.cancel()
|
||||
|
||||
self.self_destruct_timer = Timer(LIFESPAN, self.kill_me)
|
||||
self.self_destruct_timer.start()
|
||||
|
||||
|
||||
def __init__(self, server_address, RequestHandlerClass,
|
||||
bind_and_activate=True):
|
||||
|
||||
HTTPServer.__init__(self, server_address, RequestHandlerClass,
|
||||
bind_and_activate=bind_and_activate)
|
||||
|
||||
self.self_destruct_timer = None
|
||||
self.clients = 1
|
||||
self.reset_selfdestruct_timer()
|
||||
|
||||
|
||||
def suicide(self):
|
||||
self.clients -= 1
|
||||
if self.clients == 0:
|
||||
if self.self_destruct_timer is not None:
|
||||
self.self_destruct_timer.cancel()
|
||||
|
||||
quick_and_painless_timer = Timer(0.1, self.kill_me)
|
||||
quick_and_painless_timer.start()
|
||||
|
||||
|
||||
class TestServer:
|
||||
|
||||
def __init__(self):
|
||||
|
||||
self.may_serve = False
|
||||
|
||||
pid = os.getpid()
|
||||
logging.info(f"Init server. Pid: {pid}")
|
||||
|
||||
self.server = None
|
||||
|
||||
killer = SiblingKiller(PORT, PING_TIMEOUT)
|
||||
killer.kill_siblings()
|
||||
if killer.allow_serving():
|
||||
try:
|
||||
self.init_server()
|
||||
logging.info(f"Started server with pid: {pid}")
|
||||
self.may_serve = True
|
||||
|
||||
except socket.error:
|
||||
logging.info("Failed to start the server: Port is in use")
|
||||
except Exception as e:
|
||||
logging.debug(e)
|
||||
logging.info("Failed to start serving for unknown reason")
|
||||
traceback.print_exc()
|
||||
else:
|
||||
logging.info(f"Not allowed to start serving for process: {pid}")
|
||||
|
||||
def init_server(self):
|
||||
|
||||
if USE_TORNADO:
|
||||
MainHandler.init_server(PORT, LIFESPAN)
|
||||
else:
|
||||
print("""
|
||||
*************
|
||||
WARNING: Using the python's built-in BaseHTTPServer!
|
||||
It is all right if you run the tests on your local machine, but if you are running tests on a server,
|
||||
please consider installing Tornado. It is a much more powerful web-server. Otherwise you will find
|
||||
that some of your downloader tests either fail or hang.
|
||||
|
||||
do
|
||||
|
||||
sudo pip install tornado
|
||||
|
||||
or go to http://www.tornadoweb.org/en/stable/ for more detail.
|
||||
*************
|
||||
""")
|
||||
|
||||
self.server = InternalServer(('localhost', PORT), PostHandler)
|
||||
|
||||
|
||||
|
||||
def start_serving(self):
|
||||
if not self.may_serve:
|
||||
return
|
||||
|
||||
if USE_TORNADO:
|
||||
MainHandler.start_serving()
|
||||
|
||||
else:
|
||||
thread = threading.Thread(target=self.server.serve_forever)
|
||||
thread.deamon = True
|
||||
thread.start()
|
||||
|
||||
|
||||
class PostHandler(BaseHTTPRequestHandler, ResponseProviderMixin):
|
||||
|
||||
def dispatch_response(self, payload):
|
||||
|
||||
self.send_response(payload.response_code())
|
||||
for h in payload.headers():
|
||||
self.send_header(h, payload.headers()[h])
|
||||
self.send_header("Content-Length", payload.length())
|
||||
self.end_headers()
|
||||
self.wfile.write(payload.message())
|
||||
|
||||
|
||||
def init_vars(self):
|
||||
self.response_provider = ResponseProvider(self)
|
||||
|
||||
|
||||
def do_POST(self):
|
||||
self.init_vars()
|
||||
self.server.reset_selfdestruct_timer()
|
||||
headers = self.prepare_headers()
|
||||
payload = self.response_provider.response_for_url_and_headers(self.path, headers)
|
||||
if payload.response_code() >= 300:
|
||||
length = int(self.headers.get('content-length'))
|
||||
self.dispatch_response(Payload(self.rfile.read(length)))
|
||||
else:
|
||||
self.dispatch_response(payload)
|
||||
|
||||
|
||||
def do_GET(self):
|
||||
headers = self.prepare_headers()
|
||||
self.init_vars()
|
||||
self.dispatch_response(self.response_provider.response_for_url_and_headers(self.path, headers))
|
||||
|
||||
|
||||
def prepare_headers(self):
|
||||
ret = dict()
|
||||
for h in self.headers:
|
||||
ret[h.lower()] = self.headers.get(h)
|
||||
return ret
|
||||
|
||||
|
||||
def got_pinged(self):
|
||||
self.server.clients += 1
|
||||
|
||||
|
||||
def kill(self):
|
||||
logging.debug("Kill called in testserver")
|
||||
self.server.suicide()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
server = TestServer()
|
||||
server.start_serving()
|
||||
99
tools/python/test_server/server/tornado_handler.py
Normal file
99
tools/python/test_server/server/tornado_handler.py
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
from __future__ import print_function
|
||||
|
||||
from ResponseProvider import Payload
|
||||
from ResponseProvider import ResponseProvider
|
||||
from ResponseProvider import ResponseProviderMixin
|
||||
from threading import Timer
|
||||
import os
|
||||
import threading
|
||||
import tornado.ioloop
|
||||
import tornado.web
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
class MainHandler(tornado.web.RequestHandler, ResponseProviderMixin):
|
||||
|
||||
ping_count = 1
|
||||
self_destruct_timer = None
|
||||
|
||||
def got_pinged(self):
|
||||
MainHandler.ping_count += 1
|
||||
|
||||
|
||||
def kill(self):
|
||||
MainHandler.ping_count -= 1
|
||||
if MainHandler.ping_count <= 0: #so that if we decrease the value from several threads we still kill it.
|
||||
MainHandler.suicide()
|
||||
if MainHandler.self_destruct_timer:
|
||||
MainHandler.self_destruct_timer.cancel()
|
||||
|
||||
def dispatch_response(self, payload):
|
||||
self.set_status(payload.response_code())
|
||||
for h in payload.headers():
|
||||
self.add_header(h, payload.headers()[h])
|
||||
self.add_header("Content-Length", payload.length())
|
||||
self.write(payload.message())
|
||||
|
||||
|
||||
def prepare_headers(self):
|
||||
ret = dict()
|
||||
for h in self.request.headers:
|
||||
ret[h.lower()] = self.request.headers.get(h)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def init_vars(self):
|
||||
self.response_provider = ResponseProvider(self)
|
||||
self.headers = self.prepare_headers()
|
||||
|
||||
|
||||
def prepare(self):
|
||||
MainHandler.reset_self_destruct_timer()
|
||||
self.init_vars()
|
||||
|
||||
|
||||
def get(self, param):
|
||||
self.dispatch_response(self.response_provider.response_for_url_and_headers(self.request.uri, self.headers))
|
||||
|
||||
|
||||
def post(self, param):
|
||||
payload = self.response_provider.response_for_url_and_headers(self.request.uri, self.headers)
|
||||
if payload.response_code() >= 300:
|
||||
self.dispatch_response(Payload(self.request.body))
|
||||
else:
|
||||
self.dispatch_response(payload)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def suicide():
|
||||
tornado.ioloop.IOLoop.current().stop()
|
||||
logging.info("The server's life has come to an end, pid: {}".format(os.getpid()))
|
||||
|
||||
|
||||
@staticmethod
|
||||
def reset_self_destruct_timer():
|
||||
if MainHandler.self_destruct_timer:
|
||||
logging.debug("Canceling the kill timer")
|
||||
MainHandler.self_destruct_timer.cancel()
|
||||
MainHandler.self_destruct_timer = Timer(MainHandler.lifespan, MainHandler.suicide)
|
||||
logging.debug("Starting the kill timer")
|
||||
MainHandler.self_destruct_timer.start()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def start_serving():
|
||||
thread = threading.Thread(target=tornado.ioloop.IOLoop.current().start)
|
||||
thread.deamon = True
|
||||
thread.start()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def init_server(port, lifespan):
|
||||
MainHandler.lifespan = lifespan
|
||||
MainHandler.reset_self_destruct_timer()
|
||||
application = tornado.web.Application([
|
||||
(r"/(.*)", MainHandler),
|
||||
])
|
||||
application.listen(port)
|
||||
Loading…
Add table
Add a link
Reference in a new issue