Moved submission module into benchmark module

This commit is contained in:
2018-08-10 10:35:05 +02:00
parent 0e1d2810fe
commit 1defd9395a
7 changed files with 1 additions and 1 deletions

View File

@@ -843,7 +843,7 @@ class BENCHMARK_OT_share(bpy.types.Operator):
bl_label = "Share Benchmark Result"
def execute(self, context):
import submission
from benchmark import submission
make_buttons_default()
print('Submitting benchmark')

View File

@@ -0,0 +1,29 @@
def submit_benchmark(benchmark_data: dict):
"""Submit benchmark data to MyData.
Authenticates the user via the web browser and Blender ID if necessary.
Authentication tokens are stored on disk and validated before reusing.
"""
import logging
import os
from .client import CommunicationError, BenchmarkClient
mydata_url = os.environ.get('MYDATA') or 'https://mydata.blender.org/'
if 'MYDATA' in os.environ:
# Assume we're debugging here.
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)-15s %(levelname)8s %(name)s %(message)s')
bc = BenchmarkClient(mydata_url)
# Make sure we have a token; can start the browser to get one.
token = bc.load_auth_token()
result = bc.submit_benchmark(benchmark_data)
print(result)
# If we get a location from the MyData server, show it in a browser.
if result.location:
import webbrowser
webbrowser.open_new_tab(result.location)

View File

@@ -0,0 +1,552 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2005-2010 ActiveState Software Inc.
# Copyright (c) 2013 Eddy Petrișor
"""Utilities for determining application-specific dirs.
See <http://github.com/ActiveState/appdirs> for details and usage.
"""
# Dev Notes:
# - MSDN on where to store app data files:
# http://support.microsoft.com/default.aspx?scid=kb;en-us;310294#XSLTH3194121123120121120120
# - Mac OS X: http://developer.apple.com/documentation/MacOSX/Conceptual/BPFileSystem/index.html
# - XDG spec for Un*x: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
__version_info__ = (1, 4, 0)
__version__ = '.'.join(map(str, __version_info__))
import sys
import os
PY3 = sys.version_info[0] == 3
if PY3:
unicode = str
if sys.platform.startswith('java'):
import platform
os_name = platform.java_ver()[3][0]
if os_name.startswith('Windows'): # "Windows XP", "Windows 7", etc.
system = 'win32'
elif os_name.startswith('Mac'): # "Mac OS X", etc.
system = 'darwin'
else: # "Linux", "SunOS", "FreeBSD", etc.
# Setting this to "linux2" is not ideal, but only Windows or Mac
# are actually checked for and the rest of the module expects
# *sys.platform* style strings.
system = 'linux2'
else:
system = sys.platform
def user_data_dir(appname=None, appauthor=None, version=None, roaming=False):
r"""Return full path to the user-specific data dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname. You may
pass False to disable it.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"roaming" (boolean, default False) can be set True to use the Windows
roaming appdata directory. That means that for users on a Windows
network setup for roaming profiles, this user data will be
sync'd on login. See
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
for a discussion of issues.
Typical user data directories are:
Mac OS X: ~/Library/Application Support/<AppName>
Unix: ~/.local/share/<AppName> # or in $XDG_DATA_HOME, if defined
Win XP (not roaming): C:\Documents and Settings\<username>\Application Data\<AppAuthor>\<AppName>
Win XP (roaming): C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>
Win 7 (not roaming): C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>
Win 7 (roaming): C:\Users\<username>\AppData\Roaming\<AppAuthor>\<AppName>
For Unix, we follow the XDG spec and support $XDG_DATA_HOME.
That means, by default "~/.local/share/<AppName>".
"""
if system == "win32":
if appauthor is None:
appauthor = appname
const = roaming and "CSIDL_APPDATA" or "CSIDL_LOCAL_APPDATA"
path = os.path.normpath(_get_win_folder(const))
if appname:
if appauthor is not False:
path = os.path.join(path, appauthor, appname)
else:
path = os.path.join(path, appname)
elif system == 'darwin':
path = os.path.expanduser('~/Library/Application Support/')
if appname:
path = os.path.join(path, appname)
else:
path = os.getenv('XDG_DATA_HOME', os.path.expanduser("~/.local/share"))
if appname:
path = os.path.join(path, appname)
if appname and version:
path = os.path.join(path, version)
return path
def site_data_dir(appname=None, appauthor=None, version=None, multipath=False):
"""Return full path to the user-shared data dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname. You may
pass False to disable it.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"multipath" is an optional parameter only applicable to *nix
which indicates that the entire list of data dirs should be
returned. By default, the first item from XDG_DATA_DIRS is
returned, or '/usr/local/share/<AppName>',
if XDG_DATA_DIRS is not set
Typical user data directories are:
Mac OS X: /Library/Application Support/<AppName>
Unix: /usr/local/share/<AppName> or /usr/share/<AppName>
Win XP: C:\Documents and Settings\All Users\Application Data\<AppAuthor>\<AppName>
Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.)
Win 7: C:\ProgramData\<AppAuthor>\<AppName> # Hidden, but writeable on Win 7.
For Unix, this is using the $XDG_DATA_DIRS[0] default.
WARNING: Do not use this on Windows. See the Vista-Fail note above for why.
"""
if system == "win32":
if appauthor is None:
appauthor = appname
path = os.path.normpath(_get_win_folder("CSIDL_COMMON_APPDATA"))
if appname:
if appauthor is not False:
path = os.path.join(path, appauthor, appname)
else:
path = os.path.join(path, appname)
elif system == 'darwin':
path = os.path.expanduser('/Library/Application Support')
if appname:
path = os.path.join(path, appname)
else:
# XDG default for $XDG_DATA_DIRS
# only first, if multipath is False
path = os.getenv('XDG_DATA_DIRS',
os.pathsep.join(['/usr/local/share', '/usr/share']))
pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)]
if appname:
if version:
appname = os.path.join(appname, version)
pathlist = [os.sep.join([x, appname]) for x in pathlist]
if multipath:
path = os.pathsep.join(pathlist)
else:
path = pathlist[0]
return path
if appname and version:
path = os.path.join(path, version)
return path
def user_config_dir(appname=None, appauthor=None, version=None, roaming=False):
r"""Return full path to the user-specific config dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname. You may
pass False to disable it.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"roaming" (boolean, default False) can be set True to use the Windows
roaming appdata directory. That means that for users on a Windows
network setup for roaming profiles, this user data will be
sync'd on login. See
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
for a discussion of issues.
Typical user data directories are:
Mac OS X: same as user_data_dir
Unix: ~/.config/<AppName> # or in $XDG_CONFIG_HOME, if defined
Win *: same as user_data_dir
For Unix, we follow the XDG spec and support $XDG_CONFIG_HOME.
That means, by deafult "~/.config/<AppName>".
"""
if system in ["win32", "darwin"]:
path = user_data_dir(appname, appauthor, None, roaming)
else:
path = os.getenv('XDG_CONFIG_HOME', os.path.expanduser("~/.config"))
if appname:
path = os.path.join(path, appname)
if appname and version:
path = os.path.join(path, version)
return path
def site_config_dir(appname=None, appauthor=None, version=None, multipath=False):
"""Return full path to the user-shared data dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname. You may
pass False to disable it.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"multipath" is an optional parameter only applicable to *nix
which indicates that the entire list of config dirs should be
returned. By default, the first item from XDG_CONFIG_DIRS is
returned, or '/etc/xdg/<AppName>', if XDG_CONFIG_DIRS is not set
Typical user data directories are:
Mac OS X: same as site_data_dir
Unix: /etc/xdg/<AppName> or $XDG_CONFIG_DIRS[i]/<AppName> for each value in
$XDG_CONFIG_DIRS
Win *: same as site_data_dir
Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.)
For Unix, this is using the $XDG_CONFIG_DIRS[0] default, if multipath=False
WARNING: Do not use this on Windows. See the Vista-Fail note above for why.
"""
if system in ["win32", "darwin"]:
path = site_data_dir(appname, appauthor)
if appname and version:
path = os.path.join(path, version)
else:
# XDG default for $XDG_CONFIG_DIRS
# only first, if multipath is False
path = os.getenv('XDG_CONFIG_DIRS', '/etc/xdg')
pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)]
if appname:
if version:
appname = os.path.join(appname, version)
pathlist = [os.sep.join([x, appname]) for x in pathlist]
if multipath:
path = os.pathsep.join(pathlist)
else:
path = pathlist[0]
return path
def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True):
r"""Return full path to the user-specific cache dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname. You may
pass False to disable it.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"opinion" (boolean) can be False to disable the appending of
"Cache" to the base app data dir for Windows. See
discussion below.
Typical user cache directories are:
Mac OS X: ~/Library/Caches/<AppName>
Unix: ~/.cache/<AppName> (XDG default)
Win XP: C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>\Cache
Vista: C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>\Cache
On Windows the only suggestion in the MSDN docs is that local settings go in
the `CSIDL_LOCAL_APPDATA` directory. This is identical to the non-roaming
app data dir (the default returned by `user_data_dir` above). Apps typically
put cache data somewhere *under* the given dir here. Some examples:
...\Mozilla\Firefox\Profiles\<ProfileName>\Cache
...\Acme\SuperApp\Cache\1.0
OPINION: This function appends "Cache" to the `CSIDL_LOCAL_APPDATA` value.
This can be disabled with the `opinion=False` option.
"""
if system == "win32":
if appauthor is None:
appauthor = appname
path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA"))
if appname:
if appauthor is not False:
path = os.path.join(path, appauthor, appname)
else:
path = os.path.join(path, appname)
if opinion:
path = os.path.join(path, "Cache")
elif system == 'darwin':
path = os.path.expanduser('~/Library/Caches')
if appname:
path = os.path.join(path, appname)
else:
path = os.getenv('XDG_CACHE_HOME', os.path.expanduser('~/.cache'))
if appname:
path = os.path.join(path, appname.lower().replace(' ', '-'))
if appname and version:
path = os.path.join(path, version)
return path
def user_log_dir(appname=None, appauthor=None, version=None, opinion=True):
r"""Return full path to the user-specific log dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname. You may
pass False to disable it.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"opinion" (boolean) can be False to disable the appending of
"Logs" to the base app data dir for Windows, and "log" to the
base cache dir for Unix. See discussion below.
Typical user cache directories are:
Mac OS X: ~/Library/Logs/<AppName>
Unix: ~/.cache/<AppName>/log # or under $XDG_CACHE_HOME if defined
Win XP: C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>\Logs
Vista: C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>\Logs
On Windows the only suggestion in the MSDN docs is that local settings
go in the `CSIDL_LOCAL_APPDATA` directory. (Note: I'm interested in
examples of what some windows apps use for a logs dir.)
OPINION: This function appends "Logs" to the `CSIDL_LOCAL_APPDATA`
value for Windows and appends "log" to the user cache dir for Unix.
This can be disabled with the `opinion=False` option.
"""
if system == "darwin":
path = os.path.join(
os.path.expanduser('~/Library/Logs'),
appname)
elif system == "win32":
path = user_data_dir(appname, appauthor, version)
version = False
if opinion:
path = os.path.join(path, "Logs")
else:
path = user_cache_dir(appname, appauthor, version)
version = False
if opinion:
path = os.path.join(path, "log")
if appname and version:
path = os.path.join(path, version)
return path
class AppDirs(object):
"""Convenience wrapper for getting application dirs."""
def __init__(self, appname, appauthor=None, version=None, roaming=False,
multipath=False):
self.appname = appname
self.appauthor = appauthor
self.version = version
self.roaming = roaming
self.multipath = multipath
@property
def user_data_dir(self):
return user_data_dir(self.appname, self.appauthor,
version=self.version, roaming=self.roaming)
@property
def site_data_dir(self):
return site_data_dir(self.appname, self.appauthor,
version=self.version, multipath=self.multipath)
@property
def user_config_dir(self):
return user_config_dir(self.appname, self.appauthor,
version=self.version, roaming=self.roaming)
@property
def site_config_dir(self):
return site_config_dir(self.appname, self.appauthor,
version=self.version, multipath=self.multipath)
@property
def user_cache_dir(self):
return user_cache_dir(self.appname, self.appauthor,
version=self.version)
@property
def user_log_dir(self):
return user_log_dir(self.appname, self.appauthor,
version=self.version)
#---- internal support stuff
def _get_win_folder_from_registry(csidl_name):
"""This is a fallback technique at best. I'm not sure if using the
registry for this guarantees us the correct answer for all CSIDL_*
names.
"""
import _winreg
shell_folder_name = {
"CSIDL_APPDATA": "AppData",
"CSIDL_COMMON_APPDATA": "Common AppData",
"CSIDL_LOCAL_APPDATA": "Local AppData",
}[csidl_name]
key = _winreg.OpenKey(
_winreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
)
dir, type = _winreg.QueryValueEx(key, shell_folder_name)
return dir
def _get_win_folder_with_pywin32(csidl_name):
from win32com.shell import shellcon, shell
dir = shell.SHGetFolderPath(0, getattr(shellcon, csidl_name), 0, 0)
# Try to make this a unicode path because SHGetFolderPath does
# not return unicode strings when there is unicode data in the
# path.
try:
dir = unicode(dir)
# Downgrade to short path name if have highbit chars. See
# <http://bugs.activestate.com/show_bug.cgi?id=85099>.
has_high_char = False
for c in dir:
if ord(c) > 255:
has_high_char = True
break
if has_high_char:
try:
import win32api
dir = win32api.GetShortPathName(dir)
except ImportError:
pass
except UnicodeError:
pass
return dir
def _get_win_folder_with_ctypes(csidl_name):
import ctypes
csidl_const = {
"CSIDL_APPDATA": 26,
"CSIDL_COMMON_APPDATA": 35,
"CSIDL_LOCAL_APPDATA": 28,
}[csidl_name]
buf = ctypes.create_unicode_buffer(1024)
ctypes.windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf)
# Downgrade to short path name if have highbit chars. See
# <http://bugs.activestate.com/show_bug.cgi?id=85099>.
has_high_char = False
for c in buf:
if ord(c) > 255:
has_high_char = True
break
if has_high_char:
buf2 = ctypes.create_unicode_buffer(1024)
if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024):
buf = buf2
return buf.value
def _get_win_folder_with_jna(csidl_name):
import array
from com.sun import jna
from com.sun.jna.platform import win32
buf_size = win32.WinDef.MAX_PATH * 2
buf = array.zeros('c', buf_size)
shell = win32.Shell32.INSTANCE
shell.SHGetFolderPath(None, getattr(win32.ShlObj, csidl_name), None, win32.ShlObj.SHGFP_TYPE_CURRENT, buf)
dir = jna.Native.toString(buf.tostring()).rstrip("\0")
# Downgrade to short path name if have highbit chars. See
# <http://bugs.activestate.com/show_bug.cgi?id=85099>.
has_high_char = False
for c in dir:
if ord(c) > 255:
has_high_char = True
break
if has_high_char:
buf = array.zeros('c', buf_size)
kernel = win32.Kernel32.INSTANCE
if kernal.GetShortPathName(dir, buf, buf_size):
dir = jna.Native.toString(buf.tostring()).rstrip("\0")
return dir
if system == "win32":
try:
import win32com.shell
_get_win_folder = _get_win_folder_with_pywin32
except ImportError:
try:
from ctypes import windll
_get_win_folder = _get_win_folder_with_ctypes
except ImportError:
try:
import com.sun.jna
_get_win_folder = _get_win_folder_with_jna
except ImportError:
_get_win_folder = _get_win_folder_from_registry
#---- self test code
if __name__ == "__main__":
appname = "MyApp"
appauthor = "MyCompany"
props = ("user_data_dir", "site_data_dir",
"user_config_dir", "site_config_dir",
"user_cache_dir", "user_log_dir")
print("-- app dirs (with optional 'version')")
dirs = AppDirs(appname, appauthor, version="1.0")
for prop in props:
print("%s: %s" % (prop, getattr(dirs, prop)))
print("\n-- app dirs (without optional 'version')")
dirs = AppDirs(appname, appauthor)
for prop in props:
print("%s: %s" % (prop, getattr(dirs, prop)))
print("\n-- app dirs (without optional 'appauthor')")
dirs = AppDirs(appname)
for prop in props:
print("%s: %s" % (prop, getattr(dirs, prop)))
print("\n-- app dirs (with disabled 'appauthor')")
dirs = AppDirs(appname, appauthor=False)
for prop in props:
print("%s: %s" % (prop, getattr(dirs, prop)))

View File

@@ -0,0 +1,67 @@
import http.server
import logging
import random
import urllib.parse
from . import html, sockutil
log = logging.getLogger(__name__)
class TokenHTTPHandler(http.server.BaseHTTPRequestHandler):
"""Handle GET requests with tokens on the URL."""
def do_GET(self):
# /?token=72157630789362986-5405f8542b549e95
qs = urllib.parse.urlsplit(self.path).query
url_vars = urllib.parse.parse_qs(qs)
self.server.auth_token = url_vars['token'][0]
assert (isinstance(self.server.auth_token, str))
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(html.auth_okay_html)
class TokenHTTPServer(http.server.HTTPServer):
"""HTTP server on a random port, which will receive the token."""
def __init__(self) -> None:
self.log = log.getChild('TokenHTTPServer')
self.local_addr = self.listen_port()
self.log.info('Creating HTTP server at %s', self.local_addr)
self.auth_token = None
http.server.HTTPServer.__init__(self, self.local_addr, TokenHTTPHandler)
def listen_port(self):
"""Returns the hostname and TCP/IP port number to listen on.
Finds a random free port between 1100 and 20000.
"""
# Find a random free port
local_addr = ('localhost', int(random.uniform(1100, 20000)))
self.log.debug('Finding free port starting at %s', local_addr)
return sockutil.find_free_port(local_addr)
def wait_for_token(self, timeout=None):
"""Starts the HTTP server, waits for the Token."""
if self.auth_token is None:
self.timeout = timeout
self.handle_request()
if self.auth_token:
self.log.info('Auth token received: %s' % self.auth_token)
return self.auth_token
@property
def auth_callback_url(self) -> str:
return f'http://localhost:{self.local_addr[1]}/'

View File

@@ -0,0 +1,191 @@
import functools
import json
import logging
import typing
import pathlib
import urllib.parse
import requests
log = logging.getLogger(__name__)
class CommunicationError(requests.exceptions.BaseHTTPError):
"""Raised when we get an invalid status code form the MyData server."""
def __init__(self, message: str, response: requests.Response):
self.message = message
self.status_code = response.status_code
self.body = response.text
if response.headers.get('Content-Type', '') == 'application/json':
self.json = response.json()
else:
self.json = None
def __str__(self):
return f'{self.message}; ' \
f'status_code={self.status_code}; json={self.json}; body={self.body}'
class SubmissionResult:
"""Metadata of the submitted benchmark.
:ivar benchmark_id: the ID string of the benchmark; always set.
:ivar location: an URL where the benchmark can be viewed & managed; may be the empty string
if not known.
"""
def __init__(self, benchmark_id: str, location: str):
assert benchmark_id
self.benchmark_id = benchmark_id
self.location = location
def __repr__(self):
return f'<SubmissionResult(benchmark_id={self.benchmark_id!r} location={self.location!r})>'
class BenchmarkClient:
default_timeout = 30 # seconds
def __init__(self, mydata_server: str) -> None:
from requests.adapters import HTTPAdapter
self.auth_token = None
self.auth_http_server = None
self.session = requests.Session()
self.session.mount('https://', HTTPAdapter(max_retries=5))
self.url_generate_token = urllib.parse.urljoin(mydata_server, 'token/generate')
self.url_verify_token = urllib.parse.urljoin(mydata_server, 'token/verify')
self.url_submit = urllib.parse.urljoin(mydata_server, 'benchmarks/submit')
self.log = log.getChild('BenchmarkClient')
@functools.lru_cache(maxsize=1)
def _token_storage_path(self) -> pathlib.Path:
"""Determine storage location for the auth token."""
from . import appdirs
data_dir = appdirs.user_config_dir('blender-benchmark-client', 'Blender Foundation')
token_path = pathlib.Path(data_dir) / 'token.json'
self.log.debug('Tokens are stored in %s', token_path)
return token_path
def _load_token(self) -> typing.Optional[str]:
"""Load the token, return None when non-existant."""
tpath = self._token_storage_path()
self.log.info('Loading token from %s', tpath)
if not tpath.exists():
self.log.info('Token file does not exist')
return None
try:
with tpath.open('rb') as infile:
tokeninfo = json.load(infile)
except (IOError, OSError):
self.log.exception('Error reading token file %s', tpath)
return None
except json.JSONDecodeError:
self.log.exception('Malformed token file %s', tpath)
return None
return tokeninfo['token']
def _save_token(self):
"""Save the token to disk.
The token is stored as very simple JSON document, so that it's easy to
extend later (for example with expiry information).
"""
tpath = self._token_storage_path()
self.log.info('Saving token to %s', tpath)
tpath.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
with tpath.open('w') as outfile:
json.dump({'token': self.auth_token}, outfile)
def _start_http_server(self):
"""Starts the HTTP server, if it wasn't started already."""
from . import auth
if self.auth_http_server is not None:
return
self.auth_http_server = auth.TokenHTTPServer()
def _stop_http_server(self):
"""Stops the HTTP server, if one was started."""
if self.auth_http_server is None:
return
self.auth_http_server = None
def _verify_token(self) -> bool:
"""Verify the token with MyData, return True iff still valid."""
log.debug('validating token at %s', self.url_verify_token)
resp = self.session.get(self.url_verify_token,
headers={'Authorization': f'Bearer {self.auth_token}'},
timeout=self.default_timeout)
token_ok = resp.status_code in {200, 204}
if not token_ok:
log.info('Client token is no longer valid, will obtain another one.')
return token_ok
def load_auth_token(self) -> typing.Optional[str]:
"""Load & verify the token, start browser to get new one if needed."""
self.auth_token = self._load_token()
if self.auth_token and self._verify_token():
return self.auth_token
self.auth_via_browser()
if not self.auth_token:
self.log.error('Unable to get token')
return None
return self.auth_token
def auth_via_browser(self):
"""Open the webbrowser to request an auth token."""
import webbrowser
self._start_http_server()
params = urllib.parse.urlencode({'auth_callback': self.auth_http_server.auth_callback_url})
url = f"{self.url_generate_token}?{params}"
if not webbrowser.open_new_tab(url):
raise SystemError(f'Unable to open a browser to visit {url}')
self.auth_token = self.auth_http_server.wait_for_token()
self._stop_http_server()
if self.auth_token:
self._save_token()
def submit_benchmark(self, benchmark_data: dict) -> SubmissionResult:
"""Submit the benchmark to MyData.
:return: Metadata of the benchmark.
"""
payload = {
'data': benchmark_data,
'schema_version': 0,
}
log.info('Submitting benchmark to %s:\n%s', self.url_submit,
json.dumps(payload, sort_keys=True, indent=4))
resp = self.session.post(self.url_submit,
json=payload,
headers={'Authorization': f'Bearer {self.auth_token}'},
timeout=self.default_timeout)
if resp.status_code != 201:
log.error('Bad status code %d received: %s', resp.status_code, resp.text)
raise CommunicationError(f'Bad status code received', resp)
result = resp.json()
return SubmissionResult(
result['benchmark_id'],
resp.headers.get('Location') or ''
)

View File

@@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
"""HTML code."""
auth_okay_html = """<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Blender Benchmark Client</title>
<style type='text/css'>
html, body {
font-family: Ubuntu, Verdana, sans-serif;
font-size: 12pt;
background-color: white;
color: black;
}
section {
width: 500px;
margin-left: auto;
margin-right: auto;
border-radius: 10px;
box-shadow: 10px 10px 30px rgba(0, 0, 0, 0.15);
}
article, h1 {
border: 3px solid #1750d2;
}
article {
border-radius: 0 0 10px 10px;
background-color: #fefefe;
}
h1 {
border-radius: 10px 10px 0 0;
margin-top: 50px;
margin-bottom: 0;
text-align: center;
text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.5);
font-size: 200%;
background-color: #1750d2;
color: white;
}
p {
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.15);
margin: 30px 30px 10px 30px;
}
p.note {
font-size: 70%;
color: #888;
}
p.note a:visited, p.note a:link {
color: #888;
}
</style>
</head>
<body>
<section>
<h1>Blender Benchmark Client</h1>
<article>
<p>Your Blender Benchmark Client is now associated with your Blender ID account.</p>
<p>You can now <strong>close this browser window</strong>, and return to the Blender Benchmark Client.</p>
</article>
</section>
</body>
</html>
""" # noqa: W293
auth_okay_html = auth_okay_html.encode('utf-8')

View File

@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
"""Utility functions for working with network sockets.
Created by Sybren A. Stüvel for Chess IX, Haarlem, The Netherlands.
Licensed under the Apache 2 license.
"""
import logging
import os
import socket
LOG = logging.getLogger(__name__)
def is_bindable(address):
"""Tries to bind a listening socket to the given address.
Returns True if this works, False otherwise. In any case the socket is
closed before returning.
"""
sock = None
try:
sock = socket.socket()
if os.name == 'posix':
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(address)
sock.close()
except IOError as ex:
LOG.debug('is_bindable(%s): %s', address, ex)
if sock:
sock.close()
return False
return True
def find_free_port(start_address):
"""Incrementally searches for a TCP port that can be bound to.
:param start_address: (hostname, portnr) tuple defining the host to
bind and the portnumber to start the search
:type start_address: tuple
:return: the address containing the first port number that was found
to be free.
:rtype: tuple of (hostname, port_nr)
"""
(hostname, port_nr) = start_address
LOG.debug('find_free_port(%s)', start_address)
while not is_bindable((hostname, port_nr)):
LOG.debug('find_free_port: %i is not bindable, trying next port', port_nr)
port_nr += 1
return hostname, port_nr