Commit 9a571e96 authored by Jakub Beránek's avatar Jakub Beránek
Browse files

ENH: new Python client

parent 7c17dbe5
Pipeline #3796 failed with stage
in 2 minutes and 34 seconds
......@@ -18,13 +18,15 @@ test:dashboard:
- CI=false npm run build
test:server:
image: python:3.5-slim
image: mongo:3-jessie
stage: test
before_script:
- cd server
- pip install flake8
- apt-get update && apt-get install -y --no-install-recommends python3 python3-pip
- pip3 install flake8 pytest pymongo
- pip3 install python/
script:
- flake8 .
- python3 -m pytest tests --verbose
publish:dashboard:
image: docker:latest
......
......@@ -145,30 +145,32 @@ class ProjectOverviewComponent extends PureComponent<Props & RouteComponentProps
return (
<>
<div>
We provide a simple Python script located at <code>server/scripts/collect.py</code> to
simplify measurement results uploads.<br />
We provide a simple Python library located at <code>python/sw-client</code> that can
simplify measurement uploads and user creation.<br />
You can use the following snippet as an example how to use it.
</div>
<SyntaxHighlighter language='python' style={dracula}>
{`from collect import create_context, send_measurement
{`from swclient.session import Session
ctx = create_context(
session = Session(
"${API_SERVER}", # server address
"${this.getUploadToken()}" # upload token
)
send_measurement(ctx,
session.upload_measurement(
"MyAwesomeBenchmark", # benchmark name
{ # environment of the measurement
"commit": "abcdef",
"branch": "master",
"threads": "16"
}, { # measured results
},
{ # measured results
"executionTime": {
"value": "13.37",
"type": "time"
}
})`}
}
)`}
</SyntaxHighlighter>
</>
);
......
......@@ -45,8 +45,8 @@ You will get back a string containing the upload token.
After you have an upload token, you can upload measurements for
the given project. If you don't feel like creating HTTP requests manually,
you can use the helper scripts that we prepared (they are located in the
``server/scripts`` folder). For using the uploading API directly look
you can use a helper library (located at ``python/swclient``).
For using the uploading API directly look
:api:`here <#tag/Measurement/paths/~1measurements/post>`.
Example curl request to upload a measurement to a project:
......
......@@ -55,12 +55,11 @@ Example request for creating a user using curl:
$ curl -H "Content-Type: application/json" -H "Authorization: <admin-token>" \
<server>/users -d '{"username": "user", "password": "12345"}'
You can also use the helper script ``server/scripts/createuser.py`` to create a
user:
You can also use the helper library to create a user:
.. code-block:: bash
$ python scripts/createuser.py <server-address> <admin-token> <username> <password>
$ python -m python/swclient <server-address> <admin-token> create-user <username>
Once you create user accounts for your users, they can then login using the
:doc:`dashboard <dashboard>`.
......
python-dateutil
requests
from setuptools import setup, find_packages
with open('requirements.txt') as reqs:
requirements = [line for line in reqs.read().split('\n') if line]
setup(
name='snailwatch client',
version='0.1',
description='Snailwatch client for uploading measurements',
author='Jakub Beránek, Stanislav Bohm',
author_email='jakub.beranek.st@vsb.cz',
url='https://snailwatch.readthedocs.io',
license='MIT',
packages=find_packages(),
install_requires=requirements
)
from .cmd import main
main()
import argparse
import getpass
import json
import dateutil.parser
from .session import Session
def parse_args():
parser = argparse.ArgumentParser("swclient")
parser.add_argument("server_url", help="Address of Snailwatch server")
parser.add_argument("token", help="Security token")
subparsers = parser.add_subparsers(title="action", dest="action")
create_user_parser = subparsers.add_parser('create-user')
create_user_parser.add_argument("username")
create_user_parser = subparsers.add_parser('upload')
create_user_parser.add_argument("benchmark")
create_user_parser.add_argument("env")
create_user_parser.add_argument("result")
create_user_parser.add_argument("--timestamp")
create_user_parser = subparsers.add_parser('upload-file')
create_user_parser.add_argument("filename")
return parser.parse_args()
def main():
args = parse_args()
session = Session(args.server_url, args.token)
if args.action == "create-user":
password = getpass.getpass()
session.create_user(args.username, password)
elif args.action == "upload":
timestamp = None
if args.timestamp:
timestamp = dateutil.parser.parse(args.timestamp)
session.upload_measurement(args.benchmark,
json.loads(args.env),
json.loads(args.result),
timestamp)
elif args.action == "upload-file":
with open(args.filename) as f:
data = json.load(f)
timestamp = None
if "timestamp" in data:
timestamp = dateutil.parser.parse(data.timestamp)
session.upload_measurement(data["benchmark"],
data["environment"],
data["result"],
timestamp)
else:
print("Enter a valid subcommand (create-user, upload or upload-file)")
class SnailwatchException(Exception):
pass
import requests
import datetime
from .common import SnailwatchException
class Session:
def __init__(self, server_url, token):
if "://" not in server_url:
server_url = "http://" + server_url
self.server_url = server_url
self.token = token
def _post(self, address, payload):
http_headers = {
'Content-Type': 'application/json',
'Authorization': self.token
}
response = requests.post(
'{}/{}'.format(self.server_url, address),
json=payload,
headers=http_headers)
if response.status_code != 201:
raise SnailwatchException('Remote request failed, '
'status: {}, message: {}',
response.status_code, response.content)
return response.json()
def upload_measurement(
self, benchmark, environment, result, timestamp=None):
if timestamp is None:
timestamp = datetime.datetime.utcnow()
timestamp = timestamp.replace(microsecond=0)
payload = {
'benchmark': benchmark,
'timestamp': timestamp.isoformat(),
'environment': environment,
'result': result
}
return self._post("measurements", payload)
def create_user(self, username, password):
payload = {
'username': username,
'password': password
}
return self._post("users", payload)
......@@ -13,6 +13,5 @@ def start():
with app.app_context():
init_database(app)
setup_routes(app)
app.run(threaded=True, host='0.0.0.0', port=get_server_port())
from datetime import datetime
import requests
def create_context(server, upload_token):
"""
Creates context for measurement results uploads.
:param server: Address of the Snailwatch server
:param upload_token: Upload token
"""
return {
'server': server,
'upload_token': upload_token
}
def send_measurement(context, benchmark, environment, result, timestamp=None):
"""
Sends a measurement to the server.
:param context: Context created by create_context function.
:param benchmark: Benchmark name
:param environment: Environment of the measurement
:param result: Result of the measurement
:param timestamp: Optional time of the measurement (if not given
the current time is recorded)
"""
if timestamp is None:
timestamp = datetime.utcnow()
timestamp = timestamp.replace(microsecond=0)
payload = {
'benchmark': benchmark,
'timestamp': timestamp.isoformat(),
'environment': environment,
'result': result
}
hdr = {
'Content-Type': 'application/json',
'Authorization': context['upload_token']
}
response = requests.post('{}/measurements'.format(context['server']),
json=payload,
headers=hdr)
if response.status_code != 201:
raise Exception('Error while sending measurement, '
'response status: {}, error: {}', response.status_code,
response.content)
import sys
import requests
def create_user(server, admin_token, username, password):
"""
Creates a user on the server.
:param server: Address of the Snailwatch server (e.g. localhost:5000)
:param admin_token: Admin token
:param username: Username
:param password: Password
"""
payload = {
'username': username,
'password': password
}
hdr = {
'Content-Type': 'application/json',
'Authorization': admin_token
}
response = requests.post('{}/users'.format(server), json=payload,
headers=hdr)
data = response.content
if response.status_code != 201:
raise Exception('Error while creating user, '
'response status: {}, error: {}', response.status_code,
data)
else:
return response.json()
if __name__ == '__main__':
if len(sys.argv) < 5:
print('Usage:')
print('python createuser.py <server-address> <admin-token> '
'<username> <password>')
exit(1)
server = sys.argv[1]
token = sys.argv[2]
username = sys.argv[3]
password = sys.argv[4]
user = create_user(server, token, username, password)
print('User {} successfully created, id: {}'.format(username, user['_id']))
import os
import sys
import pytest
import subprocess
import time
import signal
import requests
import shutil
from pymongo import MongoClient
TEST_DIR = os.path.dirname(__file__)
ROOT = os.path.dirname(TEST_DIR)
WORK_DIR = os.path.join(TEST_DIR, "work")
SNAILWATCH_SERVER = os.path.join(ROOT, "server", "start.py")
PYTHON_DIR = os.path.join(ROOT, "python")
sys.path.insert(0, os.path.join(ROOT, "python"))
class Env:
def __init__(self):
self.processes = []
self.cleanups = []
def start_process(self, name, args, env=None, catch_io=True):
fname = os.path.join(WORK_DIR, name)
if catch_io:
with open(fname + ".out", "w") as out:
p = subprocess.Popen(args,
preexec_fn=os.setsid,
stdout=out,
stderr=subprocess.STDOUT,
cwd=WORK_DIR,
env=env)
else:
p = subprocess.Popen(args,
cwd=WORK_DIR,
env=env)
self.processes.append((name, p))
return p
def kill_all(self):
for fn in self.cleanups:
fn()
for n, p in self.processes:
# Kill the whole group since the process may spawn a child
if not p.poll():
os.killpg(os.getpgid(p.pid), signal.SIGTERM)
class SnailWatchEnv(Env):
port = 5011
mongo_port = None
admin_token = "ABC1"
user_token = None
upload_token = None
db_name = "sw-test"
def __init__(self):
super().__init__()
self.mongo_client = MongoClient('127.0.0.1', self.mongo_port)
self.mongo_client.drop_database(self.db_name)
self.db = self.mongo_client[self.db_name]
def run_cmd_client(self, args, stdin=None):
env = os.environ.copy()
env["PYTHONPATH"] = PYTHON_DIR
subprocess.check_call(
("python3", "-m", "swclient") + args, env=env, stdin=stdin)
def make_env(self):
env = os.environ.copy()
env.update({
"ADMIN_TOKEN": self.admin_token,
"MONGO_DB": self.db_name,
"PORT": str(self.port)})
return env
def start(self, do_init=True):
env = self.make_env()
self.start_process("server", ("python3", SNAILWATCH_SERVER), env=env)
time.sleep(3)
if do_init:
self.create_user("tester", "testpass")
self.user_token = self.login("tester", "testpass")
self.project_id = self.create_project("project1")["_id"]
self.upload_token = self.get_upload_token(self.project_id)
def _request(self, address, payload, token):
http_headers = {
'Content-Type': 'application/json',
}
if token:
http_headers['Authorization'] = token
if payload is None:
response = requests.get(
'{}/{}'.format(self.server_url, address),
headers=http_headers)
else:
response = requests.post(
'{}/{}'.format(self.server_url, address),
json=payload,
headers=http_headers)
if response.status_code not in (200, 201):
raise Exception('Remote request failed, '
'status: {}, message: {}',
response.status_code, response.content)
return response.json()
def get_upload_token(self, project_id):
return self._request(
"get-upload-token/" + project_id, None, self.user_token)
def login(self, username, password):
payload = {
'username': username,
'password': password
}
return self._request("login", payload, None)
def create_user(self, username, password):
payload = {
'username': username,
'password': password
}
return self._request("users", payload, self.admin_token)
def create_project(self, name):
payload = {
'name': name,
}
return self._request("projects", payload, self.user_token)
@property
def server_url(self):
return "http://localhost:{}".format(self.port)
def prepare():
"""Prepare working directory
If directory exists then it is cleaned;
If it does not exists then it is created.
"""
if os.path.isdir(WORK_DIR):
for root, dirs, files in os.walk(WORK_DIR):
for d in dirs:
os.chmod(os.path.join(root, d), 0o700)
for f in files:
os.chmod(os.path.join(root, f), 0o700)
for item in os.listdir(WORK_DIR):
path = os.path.join(WORK_DIR, item)
if os.path.isfile(path):
os.unlink(path)
else:
shutil.rmtree(path)
else:
os.makedirs(WORK_DIR)
os.chdir(WORK_DIR)
@pytest.yield_fixture(autouse=True, scope="function")
def sw_env():
prepare()
env = SnailWatchEnv()
yield env
time.sleep(0.2)
try:
pass # TODO: env.final_check()
finally:
env.kill_all()
# Final sleep to let server port be freed, on some slow computers
# a new test is starter before the old server is properly cleaned
time.sleep(1)
from swclient.session import Session
from swclient.common import SnailwatchException
import pytest
import json
def test_session_create_user(sw_env):
sw_env.start(do_init=False)
s = Session(sw_env.server_url, sw_env.admin_token)
s.create_user("SnailMaster", "SnailPassword")
assert sw_env.db.users.find_one(
{"username": "SnailMaster"})["username"] == "SnailMaster"
def test_cmd_upload(sw_env):
sw_env.start()
sw_env.run_cmd_client((sw_env.server_url,
sw_env.upload_token,
"upload",
"test1",
json.dumps({"machine": "tester"}),
json.dumps({"result":
{"value": "321", "type": "size"}})))
result = list(sw_env.db.measurements.find({"benchmark": "test1"}))
assert len(result) == 1
def test_cmd_upload_file(sw_env, tmpdir):
data = tmpdir.join("data")
data.write("""
{
"benchmark": "test1",
"environment": {"machine": "tester"},
"result": {"rs": {"value": "321", "type": "size"}}
}
""")
sw_env.start()
sw_env.run_cmd_client((sw_env.server_url,
sw_env.upload_token,
"upload-file",
str(data.realpath())))
result = list(sw_env.db.measurements.find({"benchmark": "test1"}))
assert len(result) == 1
def test_session_upload(sw_env):
sw_env.start()
s = Session(sw_env.server_url, sw_env.upload_token)
s.upload_measurement("test1",
{"machine": "tester"},
{"result": {"value": "123", "type": "integer"}})
s.upload_measurement("test1",
{"machine": "tester"},
{"result": {"value": "321", "type": "size"}})
with pytest.raises(SnailwatchException):
s.upload_measurement("test1",
{"machine": "tester"},
{"result": {"value": "321", "type": "xxx"}})
result = list(sw_env.db.measurements.find({"benchmark": "test1"}))
assert len(result) == 2
* Running on http://0.0.0.0:5011/ (Press CTRL+C to quit)
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment