commit
f6f51d35a5
@ -0,0 +1,9 @@ |
||||
_work_/ |
||||
|
||||
.venv* |
||||
*.pyc |
||||
.pytest_cache/ |
||||
*.egg-info/ |
||||
|
||||
*.swp |
||||
*.swo |
@ -0,0 +1,549 @@ |
||||
[MASTER] |
||||
|
||||
# A comma-separated list of package or module names from where C extensions may |
||||
# be loaded. Extensions are loading into the active Python interpreter and may |
||||
# run arbitrary code |
||||
extension-pkg-whitelist=PyQt5 |
||||
|
||||
# Add files or directories to the blacklist. They should be base names, not |
||||
# paths. |
||||
ignore=CVS |
||||
|
||||
# Add files or directories matching the regex patterns to the blacklist. The |
||||
# regex matches against base names, not paths. |
||||
ignore-patterns= |
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as |
||||
# pygtk.require(). |
||||
#init-hook= |
||||
|
||||
# Use multiple processes to speed up Pylint. |
||||
jobs=1 |
||||
|
||||
# List of plugins (as comma separated values of python modules names) to load, |
||||
# usually to register additional checkers. |
||||
load-plugins= |
||||
|
||||
# Pickle collected data for later comparisons. |
||||
persistent=yes |
||||
|
||||
# Specify a configuration file. |
||||
#rcfile= |
||||
|
||||
# When enabled, pylint would attempt to guess common misconfiguration and emit |
||||
# user-friendly hints instead of false-positive error messages |
||||
suggestion-mode=yes |
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the |
||||
# active Python interpreter and may run arbitrary code. |
||||
unsafe-load-any-extension=no |
||||
|
||||
|
||||
[MESSAGES CONTROL] |
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show |
||||
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED |
||||
confidence= |
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You |
||||
# can either give multiple identifiers separated by comma (,) or put this |
||||
# option multiple times (only on the command line, not in the configuration |
||||
# file where it should appear only once).You can also use "--disable=all" to |
||||
# disable everything first and then reenable specific checks. For example, if |
||||
# you want to run only the similarities checker, you can use "--disable=all |
||||
# --enable=similarities". If you want to run only the classes checker, but have |
||||
# no Warning level messages displayed, use"--disable=all --enable=classes |
||||
# --disable=W" |
||||
disable=print-statement, |
||||
parameter-unpacking, |
||||
unpacking-in-except, |
||||
old-raise-syntax, |
||||
backtick, |
||||
long-suffix, |
||||
old-ne-operator, |
||||
old-octal-literal, |
||||
import-star-module-level, |
||||
non-ascii-bytes-literal, |
||||
invalid-unicode-literal, |
||||
raw-checker-failed, |
||||
bad-inline-option, |
||||
locally-disabled, |
||||
locally-enabled, |
||||
file-ignored, |
||||
suppressed-message, |
||||
useless-suppression, |
||||
deprecated-pragma, |
||||
apply-builtin, |
||||
basestring-builtin, |
||||
buffer-builtin, |
||||
cmp-builtin, |
||||
coerce-builtin, |
||||
execfile-builtin, |
||||
file-builtin, |
||||
long-builtin, |
||||
raw_input-builtin, |
||||
reduce-builtin, |
||||
standarderror-builtin, |
||||
unicode-builtin, |
||||
xrange-builtin, |
||||
coerce-method, |
||||
delslice-method, |
||||
getslice-method, |
||||
setslice-method, |
||||
no-absolute-import, |
||||
old-division, |
||||
dict-iter-method, |
||||
dict-view-method, |
||||
next-method-called, |
||||
metaclass-assignment, |
||||
indexing-exception, |
||||
raising-string, |
||||
reload-builtin, |
||||
oct-method, |
||||
hex-method, |
||||
nonzero-method, |
||||
cmp-method, |
||||
input-builtin, |
||||
round-builtin, |
||||
intern-builtin, |
||||
unichr-builtin, |
||||
map-builtin-not-iterating, |
||||
zip-builtin-not-iterating, |
||||
range-builtin-not-iterating, |
||||
filter-builtin-not-iterating, |
||||
using-cmp-argument, |
||||
eq-without-hash, |
||||
div-method, |
||||
idiv-method, |
||||
rdiv-method, |
||||
exception-message-attribute, |
||||
invalid-str-codec, |
||||
sys-max-int, |
||||
bad-python3-import, |
||||
deprecated-string-function, |
||||
deprecated-str-translate-call, |
||||
deprecated-itertools-function, |
||||
deprecated-types-field, |
||||
next-method-defined, |
||||
dict-items-not-iterating, |
||||
dict-keys-not-iterating, |
||||
dict-values-not-iterating, |
||||
deprecated-operator-function, |
||||
deprecated-urllib-function, |
||||
xreadlines-attribute, |
||||
deprecated-sys-function, |
||||
exception-escape, |
||||
comprehension-escape, |
||||
bad-whitespace, |
||||
invalid-name, |
||||
wrong-import-position |
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can |
||||
# either give multiple identifier separated by comma (,) or put this option |
||||
# multiple time (only on the command line, not in the configuration file where |
||||
# it should appear only once). See also the "--disable" option for examples. |
||||
enable=c-extension-no-member |
||||
|
||||
|
||||
[REPORTS] |
||||
|
||||
# Python expression which should return a note less than 10 (10 is the highest |
||||
# note). You have access to the variables errors warning, statement which |
||||
# respectively contain the number of errors / warnings messages and the total |
||||
# number of statements analyzed. This is used by the global evaluation report |
||||
# (RP0004). |
||||
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) |
||||
|
||||
# Template used to display messages. This is a python new-style format string |
||||
# used to format the message information. See doc for all details |
||||
#msg-template= |
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, json |
||||
# and msvs (visual studio).You can also give a reporter class, eg |
||||
# mypackage.mymodule.MyReporterClass. |
||||
output-format=colorized |
||||
|
||||
# Tells whether to display a full report or only the messages |
||||
reports=no |
||||
|
||||
# Activate the evaluation score. |
||||
score=yes |
||||
|
||||
|
||||
[REFACTORING] |
||||
|
||||
# Maximum number of nested blocks for function / method body |
||||
max-nested-blocks=5 |
||||
|
||||
# Complete name of functions that never returns. When checking for |
||||
# inconsistent-return-statements if a never returning function is called then |
||||
# it will be considered as an explicit return statement and no message will be |
||||
# printed. |
||||
never-returning-functions=optparse.Values,sys.exit |
||||
|
||||
|
||||
[VARIABLES] |
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that |
||||
# you should avoid to define new builtins when possible. |
||||
additional-builtins= |
||||
|
||||
# Tells whether unused global variables should be treated as a violation. |
||||
allow-global-unused-variables=yes |
||||
|
||||
# List of strings which can identify a callback function by name. A callback |
||||
# name must start or end with one of those strings. |
||||
callbacks=cb_, |
||||
_cb |
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expectedly |
||||
# not used). |
||||
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ |
||||
|
||||
# Argument names that match this expression will be ignored. Default to name |
||||
# with leading underscore |
||||
ignored-argument-names=_.*|^ignored_|^unused_ |
||||
|
||||
# Tells whether we should check for unused import in __init__ files. |
||||
init-import=no |
||||
|
||||
# List of qualified module names which can have objects that can redefine |
||||
# builtins. |
||||
redefining-builtins-modules=six.moves,past.builtins,future.builtins,io,builtins |
||||
|
||||
|
||||
[TYPECHECK] |
||||
|
||||
# List of decorators that produce context managers, such as |
||||
# contextlib.contextmanager. Add to this list to register other decorators that |
||||
# produce valid context managers. |
||||
contextmanager-decorators=contextlib.contextmanager |
||||
|
||||
# List of members which are set dynamically and missed by pylint inference |
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular |
||||
# expressions are accepted. |
||||
generated-members= |
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A |
||||
# mixin class is detected if its name ends with "mixin" (case insensitive). |
||||
ignore-mixin-members=yes |
||||
|
||||
# This flag controls whether pylint should warn about no-member and similar |
||||
# checks whenever an opaque object is returned when inferring. The inference |
||||
# can return multiple potential results while evaluating a Python object, but |
||||
# some branches might not be evaluated, which results in partial inference. In |
||||
# that case, it might be useful to still emit no-member and other checks for |
||||
# the rest of the inferred objects. |
||||
ignore-on-opaque-inference=yes |
||||
|
||||
# List of class names for which member attributes should not be checked (useful |
||||
# for classes with dynamically set attributes). This supports the use of |
||||
# qualified names. |
||||
ignored-classes=optparse.Values,thread._local,_thread._local |
||||
|
||||
# List of module names for which member attributes should not be checked |
||||
# (useful for modules/projects where namespaces are manipulated during runtime |
||||
# and thus existing member attributes cannot be deduced by static analysis. It |
||||
# supports qualified module names, as well as Unix pattern matching. |
||||
ignored-modules= |
||||
|
||||
# Show a hint with possible names when a member name was not found. The aspect |
||||
# of finding the hint is based on edit distance. |
||||
missing-member-hint=yes |
||||
|
||||
# The minimum edit distance a name should have in order to be considered a |
||||
# similar match for a missing member name. |
||||
missing-member-hint-distance=1 |
||||
|
||||
# The total number of similar names that should be taken in consideration when |
||||
# showing a hint for a missing member. |
||||
missing-member-max-choices=1 |
||||
|
||||
|
||||
[FORMAT] |
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. |
||||
expected-line-ending-format= |
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit. |
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$ |
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line. |
||||
indent-after-paren=4 |
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 |
||||
# tab). |
||||
indent-string=' ' |
||||
|
||||
# Maximum number of characters on a single line. |
||||
max-line-length=120 |
||||
|
||||
# Maximum number of lines in a module |
||||
max-module-lines=1000 |
||||
|
||||
# List of optional constructs for which whitespace checking is disabled. `dict- |
||||
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. |
||||
# `trailing-comma` allows a space between comma and closing bracket: (a, ). |
||||
# `empty-line` allows space-only lines. |
||||
no-space-check=trailing-comma, |
||||
dict-separator |
||||
|
||||
# Allow the body of a class to be on the same line as the declaration if body |
||||
# contains single statement. |
||||
single-line-class-stmt=no |
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no |
||||
# else. |
||||
single-line-if-stmt=no |
||||
|
||||
|
||||
[LOGGING] |
||||
|
||||
# Logging modules to check that the string format arguments are in logging |
||||
# function parameter format |
||||
logging-modules=logging |
||||
|
||||
|
||||
[BASIC] |
||||
|
||||
# Naming style matching correct argument names |
||||
argument-naming-style=snake_case |
||||
|
||||
# Regular expression matching correct argument names. Overrides argument- |
||||
# naming-style |
||||
#argument-rgx= |
||||
|
||||
# Naming style matching correct attribute names |
||||
attr-naming-style=snake_case |
||||
|
||||
# Regular expression matching correct attribute names. Overrides attr-naming- |
||||
# style |
||||
#attr-rgx= |
||||
|
||||
# Bad variable names which should always be refused, separated by a comma |
||||
bad-names=foo, |
||||
bar, |
||||
baz, |
||||
toto, |
||||
tutu, |
||||
tata |
||||
|
||||
# Naming style matching correct class attribute names |
||||
class-attribute-naming-style=any |
||||
|
||||
# Regular expression matching correct class attribute names. Overrides class- |
||||
# attribute-naming-style |
||||
#class-attribute-rgx= |
||||
|
||||
# Naming style matching correct class names |
||||
class-naming-style=PascalCase |
||||
|
||||
# Regular expression matching correct class names. Overrides class-naming-style |
||||
#class-rgx= |
||||
|
||||
# Naming style matching correct constant names |
||||
const-naming-style=UPPER_CASE |
||||
|
||||
# Regular expression matching correct constant names. Overrides const-naming- |
||||
# style |
||||
#const-rgx= |
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter |
||||
# ones are exempt. |
||||
docstring-min-length=-1 |
||||
|
||||
# Naming style matching correct function names |
||||
function-naming-style=snake_case |
||||
|
||||
# Regular expression matching correct function names. Overrides function- |
||||
# naming-style |
||||
#function-rgx= |
||||
|
||||
# Good variable names which should always be accepted, separated by a comma |
||||
good-names=i, |
||||
j, |
||||
k, |
||||
ex, |
||||
Run, |
||||
_ |
||||
|
||||
# Include a hint for the correct naming format with invalid-name |
||||
include-naming-hint=no |
||||
|
||||
# Naming style matching correct inline iteration names |
||||
inlinevar-naming-style=any |
||||
|
||||
# Regular expression matching correct inline iteration names. Overrides |
||||
# inlinevar-naming-style |
||||
#inlinevar-rgx= |
||||
|
||||
# Naming style matching correct method names |
||||
method-naming-style=snake_case |
||||
|
||||
# Regular expression matching correct method names. Overrides method-naming- |
||||
# style |
||||
#method-rgx= |
||||
|
||||
# Naming style matching correct module names |
||||
module-naming-style=snake_case |
||||
|
||||
# Regular expression matching correct module names. Overrides module-naming- |
||||
# style |
||||
#module-rgx= |
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when |
||||
# the name regexes allow several styles. |
||||
name-group= |
||||
|
||||
# Regular expression which should only match function or class names that do |
||||
# not require a docstring. |
||||
no-docstring-rgx=^_ |
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add |
||||
# to this list to register other decorators that produce valid properties. |
||||
property-classes=abc.abstractproperty |
||||
|
||||
# Naming style matching correct variable names |
||||
variable-naming-style=snake_case |
||||
|
||||
# Regular expression matching correct variable names. Overrides variable- |
||||
# naming-style |
||||
#variable-rgx= |
||||
|
||||
|
||||
[SIMILARITIES] |
||||
|
||||
# Ignore comments when computing similarities. |
||||
ignore-comments=yes |
||||
|
||||
# Ignore docstrings when computing similarities. |
||||
ignore-docstrings=yes |
||||
|
||||
# Ignore imports when computing similarities. |
||||
ignore-imports=no |
||||
|
||||
# Minimum lines number of a similarity. |
||||
min-similarity-lines=4 |
||||
|
||||
|
||||
[SPELLING] |
||||
|
||||
# Limits count of emitted suggestions for spelling mistakes |
||||
max-spelling-suggestions=4 |
||||
|
||||
# Spelling dictionary name. Available dictionaries: none. To make it working |
||||
# install python-enchant package. |
||||
spelling-dict= |
||||
|
||||
# List of comma separated words that should not be checked. |
||||
spelling-ignore-words= |
||||
|
||||
# A path to a file that contains private dictionary; one word per line. |
||||
spelling-private-dict-file= |
||||
|
||||
# Tells whether to store unknown words to indicated private dictionary in |
||||
# --spelling-private-dict-file option instead of raising a message. |
||||
spelling-store-unknown-words=no |
||||
|
||||
|
||||
[MISCELLANEOUS] |
||||
|
||||
# List of note tags to take in consideration, separated by a comma. |
||||
notes=FIXME, |
||||
XXX, |
||||
TODO |
||||
|
||||
|
||||
[CLASSES] |
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes. |
||||
defining-attr-methods=__init__, |
||||
__new__, |
||||
setUp |
||||
|
||||
# List of member names, which should be excluded from the protected access |
||||
# warning. |
||||
exclude-protected=_asdict, |
||||
_fields, |
||||
_replace, |
||||
_source, |
||||
_make |
||||
|
||||
# List of valid names for the first argument in a class method. |
||||
valid-classmethod-first-arg=cls |
||||
|
||||
# List of valid names for the first argument in a metaclass class method. |
||||
valid-metaclass-classmethod-first-arg=mcs |
||||
|
||||
|
||||
[DESIGN] |
||||
|
||||
# Maximum number of arguments for function / method |
||||
max-args=8 |
||||
|
||||
# Maximum number of attributes for a class (see R0902). |
||||
max-attributes=7 |
||||
|
||||
# Maximum number of boolean expressions in a if statement |
||||
max-bool-expr=5 |
||||
|
||||
# Maximum number of branch for function / method body |
||||
max-branches=12 |
||||
|
||||
# Maximum number of locals for function / method body |
||||
max-locals=15 |
||||
|
||||
# Maximum number of parents for a class (see R0901). |
||||
max-parents=7 |
||||
|
||||
# Maximum number of public methods for a class (see R0904). |
||||
max-public-methods=20 |
||||
|
||||
# Maximum number of return / yield for function / method body |
||||
max-returns=6 |
||||
|
||||
# Maximum number of statements in function / method body |
||||
max-statements=50 |
||||
|
||||
# Minimum number of public methods for a class (see R0903). |
||||
min-public-methods=2 |
||||
|
||||
|
||||
[IMPORTS] |
||||
|
||||
# Allow wildcard imports from modules that define __all__. |
||||
allow-wildcard-with-all=no |
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and |
||||
# 3 compatible code, which means that the block might have code that exists |
||||
# only in one or another interpreter, leading to false positives when analysed. |
||||
analyse-fallback-blocks=no |
||||
|
||||
# Deprecated modules which should not be used, separated by a comma |
||||
deprecated-modules=optparse,tkinter.tix |
||||
|
||||
# Create a graph of external dependencies in the given file (report RP0402 must |
||||
# not be disabled) |
||||
ext-import-graph= |
||||
|
||||
# Create a graph of every (i.e. internal and external) dependencies in the |
||||
# given file (report RP0402 must not be disabled) |
||||
import-graph= |
||||
|
||||
# Create a graph of internal dependencies in the given file (report RP0402 must |
||||
# not be disabled) |
||||
int-import-graph= |
||||
|
||||
# Force import order to recognize a module as part of the standard |
||||
# compatibility libraries. |
||||
known-standard-library= |
||||
|
||||
# Force import order to recognize a module as part of a third party library. |
||||
known-third-party=enchant |
||||
|
||||
|
||||
[EXCEPTIONS] |
||||
|
||||
# Exceptions that will emit a warning when being caught. Defaults to |
||||
# "Exception" |
||||
overgeneral-exceptions=Exception |
@ -0,0 +1,3 @@ |
||||
recursive-include vasl_templates/webapp/config *.* |
||||
recursive-include vasl_templates/webapp/static *.* |
||||
recursive-include vasl_templates/webapp/templates *.* |
@ -0,0 +1,2 @@ |
||||
[pytest] |
||||
addopts = --pylint |
@ -0,0 +1,33 @@ |
||||
""" Setup the package. |
||||
|
||||
Install this module in development mode to get the tests to work: |
||||
pip install --editable .[dev] |
||||
""" |
||||
|
||||
from setuptools import setup, find_packages |
||||
|
||||
setup( |
||||
name = "vasl_templates", |
||||
version = "0.1", |
||||
packages = find_packages(), |
||||
install_requires = [ |
||||
# Python 3.6.5 |
||||
"flask==1.0.2", |
||||
# NOTE: PyQt5 requirements: https://doc.qt.io/qt-5/linux.html |
||||
# Linux: mesa-libGL-devel ; @"C Development Tools and Libraries" |
||||
# nb: WebEngine seems to be broken in 5.10.1 :-/ |
||||
"PyQT5==5.10.0", |
||||
], |
||||
extras_require = { |
||||
"dev": [ |
||||
"pytest==3.6.0", |
||||
"selenium==3.12.0", |
||||
"pylint==1.9.2", |
||||
"pytest-pylint==0.9.0", |
||||
], |
||||
}, |
||||
include_package_data = True, |
||||
entry_points = { |
||||
"console_scripts": "vasl-templates = vasl_templates.main:main", |
||||
} |
||||
) |
@ -0,0 +1,87 @@ |
||||
#!/usr/bin/env python3 |
||||
""" Main entry point for the application. """ |
||||
|
||||
import sys |
||||
import threading |
||||
import traceback |
||||
import logging |
||||
import urllib.request |
||||
|
||||
from PyQt5.QtWidgets import QApplication |
||||
from PyQt5.QtCore import Qt |
||||
|
||||
from vasl_templates.main_window import MainWindow |
||||
from vasl_templates.webapp import app as webapp |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
class LoggerProxy: |
||||
"""Redirect messages to Python logging.""" |
||||
def __init__( self, logger, level ): |
||||
self.logger = logger |
||||
self.level = level |
||||
self.closed = False |
||||
def write( self, msg ): |
||||
"""Output a message.""" |
||||
if isinstance(msg, bytes): |
||||
msg = msg.decode( "utf-8" ) |
||||
msg = msg.rstrip() |
||||
if msg: |
||||
self.logger.log( self.level, msg ) |
||||
def flush( self ): |
||||
"""Flush the output stream.""" |
||||
pass |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def main(): |
||||
"""Main entry point for the application.""" |
||||
|
||||
# connect stdout/stderr to Python logging |
||||
sys.stdout = LoggerProxy( logging, logging.INFO ) |
||||
sys.stderr = LoggerProxy( logging, logging.WARNING ) |
||||
|
||||
# start the webapp server |
||||
port = webapp.config["FLASK_PORT_NO"] |
||||
def webapp_thread(): |
||||
"""Run the webapp server.""" |
||||
try: |
||||
webapp.run( host="localhost", port=port, use_reloader=False ) |
||||
except Exception as ex: |
||||
logging.critical( "WEBAPP SERVER EXCEPTION: %s", ex ) |
||||
logging.critical( traceback.format_exc() ) |
||||
raise |
||||
thread = threading.Thread( target=webapp_thread ) |
||||
thread.start() |
||||
|
||||
# check if we should disable OpenGL |
||||
# Using the QWebEngineView crashes on Windows 7 in a VM. It uses OpenGL, which is |
||||
# apparently not well supported on Windows, and is dependent on the graphics card driver: |
||||
# https://stackoverflow.com/a/50393872 |
||||
# https://stackoverflow.com/questions/33090346/is-there-any-way-to-use-qtwebengine-without-opengl |
||||
# Switching to software rendering (AA_UseSoftwareOpenGL) got things going :shrug: |
||||
# Also see: https://doc.qt.io/qt-5/windows-requirements.html |
||||
opengl_type = webapp.config.get( "OPENGL_TYPE" ) |
||||
if opengl_type: |
||||
logging.info( "Setting OpenGL: %s", opengl_type ) |
||||
opengl_type = getattr( Qt, opengl_type ) |
||||
QApplication.setAttribute( opengl_type ) |
||||
|
||||
# run the application |
||||
app = QApplication( sys.argv ) |
||||
url = "http://localhost:{}/main".format( port ) |
||||
main_window = MainWindow( url ) |
||||
main_window.show() |
||||
ret_code = app.exec_() |
||||
|
||||
# shutdown the webapp server |
||||
url = "http://localhost:{}/shutdown".format( port ) |
||||
urllib.request.urlopen( url ).read() |
||||
thread.join() |
||||
|
||||
return ret_code |
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||
|
||||
if __name__ == "__main__": |
||||
sys.exit( main() ) |
@ -0,0 +1,38 @@ |
||||
""" Main application window. """ |
||||
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel |
||||
from PyQt5.QtWebEngineWidgets import QWebEngineView |
||||
from PyQt5.QtGui import QDesktopServices |
||||
from PyQt5.QtCore import QUrl |
||||
|
||||
from vasl_templates.webapp.config.constants import APP_NAME |
||||
from vasl_templates.webapp import app as webapp |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
class MainWindow( QWidget ): |
||||
"""Main application window.""" |
||||
|
||||
def __init__( self, url ): |
||||
|
||||
# initialize |
||||
super().__init__() |
||||
self.setWindowTitle( APP_NAME ) |
||||
|
||||
# initialize the layout |
||||
# FUDGE! We offer the option to disable the QWebEngineView since getting it to run |
||||
# under Windows (especially older versions) is unreliable (since it uses OpenGL). |
||||
# By disabling it, the program will at least start (in particular, the webapp server), |
||||
# and non-technical users can then open an external browser and connect to the webapp |
||||
# that way. Sigh... |
||||
layout = QVBoxLayout( self ) |
||||
if not webapp.config.get( "DISABLE_WEBENGINEVIEW" ): |
||||
# load the webapp |
||||
browser = QWebEngineView() |
||||
layout.addWidget( browser ) |
||||
browser.setUrl( QUrl(url) ) |
||||
else: |
||||
label = QLabel() |
||||
label.setText( "Running the {} application.\n\nClose this window when you're done.".format( APP_NAME ) ) |
||||
layout.addWidget( label ) |
||||
QDesktopServices.openUrl( QUrl(url) ) |
@ -0,0 +1,46 @@ |
||||
""" Initialize the package. """ |
||||
|
||||
import os |
||||
import configparser |
||||
import json |
||||
import logging |
||||
|
||||
from flask import Flask |
||||
|
||||
from vasl_templates.webapp.config.constants import APP_NAME, BASE_DIR |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
# initialize Flask |
||||
app = Flask( __name__ ) |
||||
|
||||
# load the application configuration |
||||
config_dir = os.path.join( BASE_DIR, "config" ) |
||||
config_parser = configparser.ConfigParser() |
||||
config_parser.optionxform = str # preserve case for the keys :-/ |
||||
config_parser.read( os.path.join( config_dir, "app.cfg" ) ) |
||||
app.config.update( dict( config_parser.items( "System" ) ) ) |
||||
fname = os.path.join( config_dir, "debug.cfg" ) |
||||
if os.path.isfile( fname ) : |
||||
config_parser.read( fname ) |
||||
app.config.update( dict( config_parser.items( "Debug" ) ) ) |
||||
|
||||
# initialize logging |
||||
fname = os.path.join( config_dir, "logging.cfg" ) |
||||
if os.path.isfile( fname ): |
||||
import logging.config |
||||
logging.config.dictConfig( json.load( open(fname,"r") ) ) |
||||
|
||||
# load the application |
||||
import vasl_templates.webapp.main #pylint: disable=cyclic-import |
||||
import vasl_templates.webapp.generate #pylint: disable=cyclic-import |
||||
|
||||
# initialize the application |
||||
logger = logging.getLogger( "startup" ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@app.context_processor |
||||
def inject_template_params(): |
||||
"""Inject template parameters into Jinja2.""" |
||||
return { "APP_NAME": APP_NAME } |
@ -0,0 +1,2 @@ |
||||
debug.cfg |
||||
logging.cfg |
@ -0,0 +1,3 @@ |
||||
[System] |
||||
|
||||
FLASK_PORT_NO = 5010 |
@ -0,0 +1,8 @@ |
||||
""" Application constants. """ |
||||
|
||||
import os |
||||
|
||||
APP_NAME = "VASL Templates" |
||||
APP_VERSION = "v0.1" |
||||
|
||||
BASE_DIR = os.path.abspath( os.path.join( os.path.split(__file__)[0], ".." ) ) |
@ -0,0 +1,15 @@ |
||||
""" Webapp handlers. """ |
||||
|
||||
from flask import request |
||||
|
||||
from vasl_templates.webapp import app |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@app.route( "/generate", methods=["POST"] ) |
||||
def generate_html(): |
||||
"""Generate a response""" |
||||
val = request.form.get( "val" ) |
||||
if val: |
||||
val = val.strip() |
||||
return "You said: {}".format( '"{}"'.format(val) if val else "nothing!" ) |
@ -0,0 +1,20 @@ |
||||
""" Main webapp handlers. """ |
||||
|
||||
from flask import request, render_template |
||||
|
||||
from vasl_templates.webapp import app |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@app.route( "/main" ) |
||||
def main(): |
||||
"""Return the main page.""" |
||||
return render_template( "main.html" ) |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@app.route( "/shutdown" ) |
||||
def shutdown(): |
||||
"""Shutdown the webapp (for testing porpoises).""" |
||||
request.environ.get( "werkzeug.server.shutdown" )() |
||||
return "" |
@ -0,0 +1,27 @@ |
||||
#!/usr/bin/env python3 |
||||
""" Run the webapp server. """ |
||||
|
||||
import os |
||||
import glob |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
# monitor extra files for changes |
||||
extra_files = [] |
||||
for fspec in ["config","static","templates"] : |
||||
fspec = os.path.abspath( os.path.join( os.path.dirname(__file__), fspec ) ) |
||||
if os.path.isdir( fspec ): |
||||
files = [ os.path.join(fspec,f) for f in os.listdir(fspec) ] |
||||
files = [ f for f in files if os.path.isfile(f) and os.path.splitext(f)[1] not in [".swp"] ] |
||||
else: |
||||
files = glob.glob( fspec ) |
||||
extra_files.extend( files ) |
||||
|
||||
# run the server |
||||
from vasl_templates.webapp import app |
||||
app.run( |
||||
host = "localhost", |
||||
port = app.config["FLASK_PORT_NO"], |
||||
debug = True, |
||||
extra_files = extra_files |
||||
) |
@ -0,0 +1 @@ |
||||
* { margin: 0 ; padding: 0 } |
File diff suppressed because one or more lines are too long
@ -0,0 +1,19 @@ |
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
$(document).ready( function () { |
||||
$("form").submit( function( evt ) { |
||||
evt.preventDefault() ; |
||||
var data = { val: $("form input[name='val']").val() } ; |
||||
$.post( { |
||||
url: gGenerateURL, |
||||
data: data, |
||||
success: function( data ) { |
||||
$("#response").text( data ) ; |
||||
}, |
||||
error: function( xhr, status, errorMsg ) { |
||||
$("#response").text( "ERROR: "+errorMsg ) ; |
||||
}, |
||||
} ) ; |
||||
} ) ; |
||||
} ) ; |
@ -0,0 +1,29 @@ |
||||
<!doctype html> |
||||
<html lang="en"> |
||||
|
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<title> {{APP_NAME}} </title> |
||||
<link rel="shortcut icon" href="{{url_for('static', filename='images/favicon.ico')}}"> |
||||
<link rel="stylesheet" type="text/css" href="{{url_for('static',filename='css/main.css')}}" /> |
||||
</head> |
||||
|
||||
<body> |
||||
|
||||
<form action="/generate"> |
||||
Say something: |
||||
<input type="text" name="val" size="20"> |
||||
<input type="submit" value="GO"> |
||||
</form> |
||||
|
||||
<div id="response"> </div> |
||||
|
||||
</body> |
||||
|
||||
<script src="{{url_for('static',filename='jquery/jquery-3.3.1.min.js')}}"></script> |
||||
<script> |
||||
gGenerateURL = "{{url_for('generate_html')}}" ; |
||||
</script> |
||||
<script src="{{url_for('static',filename='main.js')}}"></script> |
||||
|
||||
</html> |
@ -0,0 +1,74 @@ |
||||
""" pytest support functions. """ |
||||
|
||||
import os |
||||
import threading |
||||
import logging |
||||
import tempfile |
||||
import urllib.request |
||||
import pytest |
||||
|
||||
from flask import url_for |
||||
|
||||
from vasl_templates.webapp import app |
||||
app.testing = True |
||||
|
||||
FLASK_WEBAPP_PORT = 5001 |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
@pytest.fixture |
||||
def webapp(): |
||||
"""Launch the webapp.""" |
||||
|
||||
# initialize |
||||
# WTF?! https://github.com/pallets/flask/issues/824 |
||||
def make_webapp_url( endpoint ): |
||||
"""Generate a webapp URL.""" |
||||
with app.test_request_context(): |
||||
url = url_for( endpoint, _external=True ) |
||||
return url.replace( "localhost/", "localhost:{}/".format(FLASK_WEBAPP_PORT) ) |
||||
app.url_for = make_webapp_url |
||||
|
||||
# start the webapp server (in a background thread) |
||||
logging.disable( logging.CRITICAL ) |
||||
thread = threading.Thread( |
||||
target = lambda: app.run( host="0.0.0.0", port=FLASK_WEBAPP_PORT, use_reloader=False ) |
||||
) |
||||
thread.start() |
||||
yield app |
||||
|
||||
# shutdown the webapp server |
||||
urllib.request.urlopen( app.url_for("shutdown") ).read() |
||||
thread.join() |
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||
|
||||
@pytest.fixture |
||||
def test_client(): |
||||
"""Return a test client that can be used to connect to the webapp.""" |
||||
logging.disable( logging.CRITICAL ) |
||||
return app.test_client() |
||||
|
||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
||||
|
||||
@pytest.fixture |
||||
def webdriver(): |
||||
"""Return a webdriver that can be used to control a browser. |
||||
|
||||
It would be nice to be able to drive the embedded browser in the wrapper |
||||
Qt app with e.g. this: |
||||
https://github.com/cisco-open-source/qtwebdriver |
||||
but it only works with the old QtWebKit, which was removed in Qt 5.6 :-/ |
||||
""" |
||||
|
||||
# initialize |
||||
from selenium import webdriver as wb |
||||
driver = wb.Firefox( |
||||
log_path = os.path.join( tempfile.gettempdir(), "geckodriver.log" ) |
||||
) |
||||
|
||||
# return the webdriver to the caller |
||||
yield driver |
||||
|
||||
# clean up |
||||
driver.close() |
@ -0,0 +1,37 @@ |
||||
""" Test response generation. """ |
||||
|
||||
from vasl_templates.webapp.tests.utils import find_child |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def test_generate( webapp, webdriver ): |
||||
"""Test response generation.""" |
||||
|
||||
# initialize |
||||
webdriver.get( webapp.url_for( "main" ) ) |
||||
|
||||
# try saying something |
||||
textbox = find_child( webdriver, "input[type='text']" ) |
||||
textbox.clear() |
||||
textbox.send_keys( "Hi, there!" ) |
||||
submit = find_child( webdriver, "input[type='submit']" ) |
||||
submit.click() |
||||
response = find_child( webdriver, "#response" ) |
||||
assert response.text == 'You said: "Hi, there!"' |
||||
|
||||
# try saying something else |
||||
textbox = find_child( webdriver, "input[type='text']" ) |
||||
textbox.clear() |
||||
textbox.send_keys( "Yo mama so big..." ) |
||||
submit = find_child( webdriver, "input[type='submit']" ) |
||||
submit.click() |
||||
response = find_child( webdriver, "#response" ) |
||||
assert response.text == 'You said: "Yo mama so big..."' |
||||
|
||||
# try saying nothing |
||||
textbox = find_child( webdriver, "input[type='text']" ) |
||||
textbox.clear() |
||||
submit = find_child( webdriver, "input[type='submit']" ) |
||||
submit.click() |
||||
response = find_child( webdriver, "#response" ) |
||||
assert response.text == "You said: nothing!" |
@ -0,0 +1,19 @@ |
||||
""" Helper utilities. """ |
||||
|
||||
from selenium.common.exceptions import NoSuchElementException |
||||
|
||||
# --------------------------------------------------------------------- |
||||
|
||||
def find_child( elem, sel ): |
||||
"""Find a single child element.""" |
||||
try: |
||||
return elem.find_element_by_css_selector( sel ) |
||||
except NoSuchElementException: |
||||
return None |
||||
|
||||
def find_children( elem, sel ): |
||||
"""Find child elements.""" |
||||
try: |
||||
return elem.find_elements_by_css_selector( sel ) |
||||
except NoSuchElementException: |
||||
return None |
Loading…
Reference in new issue