Moving poc under testutils directory.
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
build
|
||||
*.egg-info
|
||||
dist
|
||||
@@ -0,0 +1,584 @@
|
||||
[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=
|
||||
|
||||
# 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. Specifying 0 will auto-detect the
|
||||
# number of processors available to use.
|
||||
jobs=1
|
||||
|
||||
# Control the amount of potential inferred values when inferring a single
|
||||
# object. This can help the performance when dealing with large functions or
|
||||
# complex, nested conditions.
|
||||
limit-inference-results=100
|
||||
|
||||
# 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,
|
||||
raw-checker-failed,
|
||||
bad-inline-option,
|
||||
locally-disabled,
|
||||
file-ignored,
|
||||
suppressed-message,
|
||||
useless-suppression,
|
||||
deprecated-pragma,
|
||||
use-symbolic-message-instead,
|
||||
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,
|
||||
too-many-return-statements,
|
||||
invalid-name,
|
||||
no-else-return,
|
||||
too-many-statements,
|
||||
too-many-branches,
|
||||
no-else-raise,
|
||||
no-member,
|
||||
unused-argument,
|
||||
no-value-for-parameter,
|
||||
missing-docstring,
|
||||
fixme,
|
||||
broad-except,
|
||||
redefined-builtin
|
||||
|
||||
|
||||
# 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, e.g.
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
output-format=text
|
||||
|
||||
# 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=sys.exit
|
||||
|
||||
|
||||
[STRING]
|
||||
|
||||
# This flag controls whether the implicit-str-concat-in-sequence should
|
||||
# generate a warning on implicit string concatenation in sequences defined over
|
||||
# several lines.
|
||||
check-str-concat-over-line-jumps=no
|
||||
|
||||
|
||||
[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
|
||||
|
||||
|
||||
[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=100
|
||||
|
||||
# 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]
|
||||
|
||||
# Format style used to check logging format string. `old` means using %
|
||||
# formatting, while `new` is for `{}` formatting.
|
||||
logging-format-style=old
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format.
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,
|
||||
XXX,
|
||||
TODO
|
||||
|
||||
|
||||
[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
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid defining 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. expected to
|
||||
# not be 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,builtins,io
|
||||
|
||||
|
||||
[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
|
||||
|
||||
# Tells whether to warn about missing members when the owner of the attribute
|
||||
# is inferred to be None.
|
||||
ignore-none=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
|
||||
|
||||
|
||||
[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.
|
||||
# These decorators are taken in consideration only for invalid-name.
|
||||
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=
|
||||
|
||||
|
||||
[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=cls
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# Maximum number of arguments for function / method.
|
||||
max-args=5
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=7
|
||||
|
||||
# Maximum number of boolean expressions in an 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
|
||||
# "BaseException, Exception".
|
||||
overgeneral-exceptions=BaseException,
|
||||
Exception
|
||||
@@ -0,0 +1 @@
|
||||
3.7.6
|
||||
@@ -0,0 +1,14 @@
|
||||
FROM python:3.7-alpine
|
||||
|
||||
RUN apk upgrade --update-cache \
|
||||
&& apk add bash curl coreutils file \
|
||||
&& pip install pipenv
|
||||
|
||||
COPY Pipfile Pipfile.lock /
|
||||
|
||||
RUN pipenv install --system --deploy \
|
||||
&& rm -f /Pipfile /Pipfile.lock
|
||||
|
||||
COPY bin/* /usr/bin/
|
||||
|
||||
ENTRYPOINT ["/usr/bin/kapow"]
|
||||
@@ -0,0 +1,16 @@
|
||||
.PHONY: sync test
|
||||
|
||||
all: test
|
||||
|
||||
sync:
|
||||
pipenv sync --dev
|
||||
|
||||
test: sync
|
||||
KAPOW_DATAAPI_URL=http://localhost:8081 pipenv run make -C ../../spec/test
|
||||
|
||||
fix:
|
||||
KAPOW_DATAAPI_URL=http://localhost:8081 pipenv run make -C ../../spec/test fix
|
||||
|
||||
clean:
|
||||
pipenv --rm
|
||||
make -C ../../spec/test clean
|
||||
@@ -0,0 +1,13 @@
|
||||
[[source]]
|
||||
name = "pypi"
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[dev-packages]
|
||||
pylint = "*"
|
||||
|
||||
[packages]
|
||||
kapow = {path = ".",editable = true}
|
||||
|
||||
# [requires]
|
||||
# python_version = "3.7"
|
||||
Generated
+219
@@ -0,0 +1,219 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "d9b6573eb6ec7cac4644707ab68c73765d49be59ed32db7147d8aef8b18016ab"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"aiohttp": {
|
||||
"hashes": [
|
||||
"sha256:00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55",
|
||||
"sha256:0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed",
|
||||
"sha256:09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10",
|
||||
"sha256:199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5",
|
||||
"sha256:296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1",
|
||||
"sha256:368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939",
|
||||
"sha256:40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390",
|
||||
"sha256:629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa",
|
||||
"sha256:6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc",
|
||||
"sha256:87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5",
|
||||
"sha256:9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d",
|
||||
"sha256:9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf",
|
||||
"sha256:9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6",
|
||||
"sha256:a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72",
|
||||
"sha256:a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12",
|
||||
"sha256:a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366",
|
||||
"sha256:acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4",
|
||||
"sha256:b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300",
|
||||
"sha256:c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d",
|
||||
"sha256:cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303",
|
||||
"sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6",
|
||||
"sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889"
|
||||
],
|
||||
"version": "==3.5.4"
|
||||
},
|
||||
"async-timeout": {
|
||||
"hashes": [
|
||||
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
|
||||
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
|
||||
],
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
|
||||
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
|
||||
],
|
||||
"version": "==19.3.0"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
|
||||
"sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
|
||||
],
|
||||
"version": "==2019.11.28"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
||||
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
||||
],
|
||||
"version": "==7.0"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
|
||||
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
|
||||
],
|
||||
"version": "==2.8"
|
||||
},
|
||||
"kapow": {
|
||||
"editable": true,
|
||||
"path": "."
|
||||
},
|
||||
"multidict": {
|
||||
"hashes": [
|
||||
"sha256:13f3ebdb5693944f52faa7b2065b751cb7e578b8dd0a5bb8e4ab05ad0188b85e",
|
||||
"sha256:26502cefa86d79b86752e96639352c7247846515c864d7c2eb85d036752b643c",
|
||||
"sha256:4fba5204d32d5c52439f88437d33ad14b5f228e25072a192453f658bddfe45a7",
|
||||
"sha256:527124ef435f39a37b279653ad0238ff606b58328ca7989a6df372fd75d7fe26",
|
||||
"sha256:5414f388ffd78c57e77bd253cf829373721f450613de53dc85a08e34d806e8eb",
|
||||
"sha256:5eee66f882ab35674944dfa0d28b57fa51e160b4dce0ce19e47f495fdae70703",
|
||||
"sha256:63810343ea07f5cd86ba66ab66706243a6f5af075eea50c01e39b4ad6bc3c57a",
|
||||
"sha256:6bd10adf9f0d6a98ccc792ab6f83d18674775986ba9bacd376b643fe35633357",
|
||||
"sha256:83c6ddf0add57c6b8a7de0bc7e2d656be3eefeff7c922af9a9aae7e49f225625",
|
||||
"sha256:93166e0f5379cf6cd29746989f8a594fa7204dcae2e9335ddba39c870a287e1c",
|
||||
"sha256:9a7b115ee0b9b92d10ebc246811d8f55d0c57e82dbb6a26b23c9a9a6ad40ce0c",
|
||||
"sha256:a38baa3046cce174a07a59952c9f876ae8875ef3559709639c17fdf21f7b30dd",
|
||||
"sha256:a6d219f49821f4b2c85c6d426346a5d84dab6daa6f85ca3da6c00ed05b54022d",
|
||||
"sha256:a8ed33e8f9b67e3b592c56567135bb42e7e0e97417a4b6a771e60898dfd5182b",
|
||||
"sha256:d7d428488c67b09b26928950a395e41cc72bb9c3d5abfe9f0521940ee4f796d4",
|
||||
"sha256:dcfed56aa085b89d644af17442cdc2debaa73388feba4b8026446d168ca8dad7",
|
||||
"sha256:f29b885e4903bd57a7789f09fe9d60b6475a6c1a4c0eca874d8558f00f9d4b51"
|
||||
],
|
||||
"version": "==4.7.4"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
|
||||
"sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
|
||||
],
|
||||
"version": "==2.22.0"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc",
|
||||
"sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"
|
||||
],
|
||||
"version": "==1.25.8"
|
||||
},
|
||||
"yarl": {
|
||||
"hashes": [
|
||||
"sha256:0c2ab325d33f1b824734b3ef51d4d54a54e0e7a23d13b86974507602334c2cce",
|
||||
"sha256:0ca2f395591bbd85ddd50a82eb1fde9c1066fafe888c5c7cc1d810cf03fd3cc6",
|
||||
"sha256:2098a4b4b9d75ee352807a95cdf5f10180db903bc5b7270715c6bbe2551f64ce",
|
||||
"sha256:25e66e5e2007c7a39541ca13b559cd8ebc2ad8fe00ea94a2aad28a9b1e44e5ae",
|
||||
"sha256:26d7c90cb04dee1665282a5d1a998defc1a9e012fdca0f33396f81508f49696d",
|
||||
"sha256:308b98b0c8cd1dfef1a0311dc5e38ae8f9b58349226aa0533f15a16717ad702f",
|
||||
"sha256:3ce3d4f7c6b69c4e4f0704b32eca8123b9c58ae91af740481aa57d7857b5e41b",
|
||||
"sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b",
|
||||
"sha256:5b10eb0e7f044cf0b035112446b26a3a2946bca9d7d7edb5e54a2ad2f6652abb",
|
||||
"sha256:6faa19d3824c21bcbfdfce5171e193c8b4ddafdf0ac3f129ccf0cdfcb083e462",
|
||||
"sha256:944494be42fa630134bf907714d40207e646fd5a94423c90d5b514f7b0713fea",
|
||||
"sha256:a161de7e50224e8e3de6e184707476b5a989037dcb24292b391a3d66ff158e70",
|
||||
"sha256:a4844ebb2be14768f7994f2017f70aca39d658a96c786211be5ddbe1c68794c1",
|
||||
"sha256:c2b509ac3d4b988ae8769901c66345425e361d518aecbe4acbfc2567e416626a",
|
||||
"sha256:c9959d49a77b0e07559e579f38b2f3711c2b8716b8410b320bf9713013215a1b",
|
||||
"sha256:d8cdee92bc930d8b09d8bd2043cedd544d9c8bd7436a77678dd602467a993080",
|
||||
"sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2"
|
||||
],
|
||||
"version": "==1.4.2"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"astroid": {
|
||||
"hashes": [
|
||||
"sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a",
|
||||
"sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42"
|
||||
],
|
||||
"version": "==2.3.3"
|
||||
},
|
||||
"isort": {
|
||||
"hashes": [
|
||||
"sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1",
|
||||
"sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"
|
||||
],
|
||||
"version": "==4.3.21"
|
||||
},
|
||||
"lazy-object-proxy": {
|
||||
"hashes": [
|
||||
"sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d",
|
||||
"sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449",
|
||||
"sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08",
|
||||
"sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a",
|
||||
"sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50",
|
||||
"sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd",
|
||||
"sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239",
|
||||
"sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb",
|
||||
"sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea",
|
||||
"sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e",
|
||||
"sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156",
|
||||
"sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142",
|
||||
"sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442",
|
||||
"sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62",
|
||||
"sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db",
|
||||
"sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531",
|
||||
"sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383",
|
||||
"sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a",
|
||||
"sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357",
|
||||
"sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4",
|
||||
"sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"
|
||||
],
|
||||
"version": "==1.4.3"
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
|
||||
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
|
||||
],
|
||||
"version": "==0.6.1"
|
||||
},
|
||||
"pylint": {
|
||||
"hashes": [
|
||||
"sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd",
|
||||
"sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.4.4"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
|
||||
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
|
||||
],
|
||||
"version": "==1.14.0"
|
||||
},
|
||||
"wrapt": {
|
||||
"hashes": [
|
||||
"sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"
|
||||
],
|
||||
"version": "==1.11.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
Kapow! PoC
|
||||
==========
|
||||
|
||||
This directory contains a somewhat working Kapow! implementation written
|
||||
in Python.
|
||||
|
||||
The purpose of this implementation is to be an experimentation playfield
|
||||
for the developers, allowing us to discuss and test new features
|
||||
quickly.
|
||||
|
||||
Try the software within this directory with caution, anything can break
|
||||
at any moment.
|
||||
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
_,-._
|
||||
; ___ : ,------------------------------.
|
||||
,--' (. .) '--.__ | |
|
||||
_; ||| \ | Arrr!! Be ye warned! |
|
||||
'._,-----''';=.____," | |
|
||||
/// < o> |##| | |
|
||||
(o \`--' //`-----------------------------'
|
||||
///\ >>>> _\ <<<< //
|
||||
--._>>>>>>>><<<<<<<< /
|
||||
___() >>>[||||]<<<<
|
||||
`--'>>>>>>>><<<<<<<
|
||||
>>>>>>><<<<<<
|
||||
>>>>><<<<<
|
||||
>>ctr<<
|
||||
|
||||
|
||||
Running the Spec Test Suite
|
||||
---------------------------
|
||||
|
||||
Sitting in this directory run ``make`` to run the Kapow! Python PoC
|
||||
against the Spec Test Suite.
|
||||
Executable
+641
@@ -0,0 +1,641 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
#
|
||||
# Copyright 2019 Banco Bilbao Vizcaya Argentaria, S.A.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
from collections import namedtuple
|
||||
from urllib.parse import urlparse
|
||||
from uuid import uuid4
|
||||
import asyncio
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shlex
|
||||
import ssl
|
||||
import sys
|
||||
|
||||
from aiohttp import web, StreamReader
|
||||
from aiohttp.web_urldispatcher import UrlDispatcher
|
||||
import click
|
||||
import requests
|
||||
|
||||
|
||||
log = logging.getLogger('kapow')
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
########################################################################
|
||||
# Resource Management #
|
||||
########################################################################
|
||||
|
||||
|
||||
CONNECTIONS = {}
|
||||
|
||||
|
||||
class Connection:
|
||||
"""
|
||||
Manages the lifecycle of a Kapow! connection.
|
||||
|
||||
Behaves like a memory for the "fields" available in HTTP
|
||||
connections.
|
||||
|
||||
"""
|
||||
def __init__(self, request):
|
||||
self._stream = None
|
||||
self._body = io.BytesIO()
|
||||
self._status = 200
|
||||
self._headers = dict()
|
||||
self._cookies = dict()
|
||||
|
||||
self.request = request
|
||||
|
||||
async def get(self, key):
|
||||
"""Get the content of the field `key`."""
|
||||
res = urlparse(key)
|
||||
|
||||
def nth(n):
|
||||
"""Return the nth element in a path."""
|
||||
return res.path.split('/')[n]
|
||||
|
||||
if res.path == 'request/method':
|
||||
return self.request.method.encode('utf-8')
|
||||
elif res.path == 'request/body':
|
||||
return self.request.content
|
||||
elif res.path == 'request/path':
|
||||
return self.request.path.encode('utf-8')
|
||||
elif res.path == 'request/host':
|
||||
return self.request.host.encode('utf-8')
|
||||
elif res.path.startswith('request/matches/'):
|
||||
return self.request.match_info[nth(2)].encode('utf-8')
|
||||
elif res.path.startswith('request/params/'):
|
||||
return self.request.rel_url.query[nth(2)].encode('utf-8')
|
||||
elif res.path.startswith('request/headers/'):
|
||||
return self.request.headers[nth(2)].encode('utf-8')
|
||||
elif res.path.startswith('request/cookies/'):
|
||||
return self.request.cookies[nth(2)].encode('utf-8')
|
||||
elif res.path == 'request/form':
|
||||
data = await self.request.post()
|
||||
files = [fieldname.encode('utf-8')
|
||||
for fieldname, field in data.items()]
|
||||
return b'\n'.join(files)
|
||||
elif res.path.startswith('request/form/'):
|
||||
return (await self.request.post())[nth(2)].encode('utf-8')
|
||||
elif res.path == 'request/files':
|
||||
data = await self.request.post()
|
||||
files = [fieldname.encode('utf-8')
|
||||
for fieldname, field in data.items()
|
||||
if hasattr(field, 'filename')]
|
||||
return b'\n'.join(files)
|
||||
elif res.path.startswith('request/files/'):
|
||||
name = nth(2)
|
||||
content = nth(3) # filename / content
|
||||
field = (await self.request.post())[name]
|
||||
if content == 'filename':
|
||||
try:
|
||||
return field.filename.encode('utf-8')
|
||||
except Exception:
|
||||
return b''
|
||||
elif content == 'content':
|
||||
try:
|
||||
return field.file.read()
|
||||
except Exception:
|
||||
return b''
|
||||
else:
|
||||
raise ValueError(f'Unknown content type {content!r}')
|
||||
else:
|
||||
raise ValueError('Unknown path')
|
||||
|
||||
async def set(self, key, content):
|
||||
"""Set the field `key` with the value in `content`."""
|
||||
res = urlparse(key)
|
||||
|
||||
def nth(n):
|
||||
return res.path.split('/')[n]
|
||||
|
||||
if res.path == 'response/status':
|
||||
self._status = int((await content.read()).decode('utf-8'))
|
||||
elif res.path == 'response/body':
|
||||
self._body.write(await content.read())
|
||||
elif res.path.startswith('response/headers/'):
|
||||
clean = (await content.read()).rstrip(b'\n').decode('utf-8')
|
||||
self._headers[nth(2)] = clean
|
||||
elif res.path.startswith('response/cookies/'):
|
||||
clean = (await content.read()).rstrip(b'\n').decode('utf-8')
|
||||
self._cookies[nth(2)] = clean
|
||||
elif res.path == 'response/stream':
|
||||
if self._stream is None:
|
||||
self._stream = web.StreamResponse(status=self._status,
|
||||
reason="OK",
|
||||
headers=self._headers)
|
||||
for name, value in self._cookies.items():
|
||||
self._stream.set_cookie(name, value)
|
||||
await self._stream.prepare(self.request)
|
||||
|
||||
chunk = await content.readany()
|
||||
while chunk:
|
||||
await self._stream.write(chunk)
|
||||
chunk = await content.readany()
|
||||
else:
|
||||
raise ValueError(f'Unknown path {res.path!r}')
|
||||
|
||||
async def append(self, key, content):
|
||||
"""Append to field `key` the value in `content`."""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def build_response(self):
|
||||
"""Return the appropriate aiohttp.web.*Response."""
|
||||
if self._stream is None:
|
||||
response = web.Response(body=self._body.getvalue(),
|
||||
status=self._status,
|
||||
headers=self._headers)
|
||||
for name, value in self._cookies.items():
|
||||
response.set_cookie(name, value)
|
||||
return response
|
||||
else:
|
||||
await self._stream.write_eof()
|
||||
return self._stream
|
||||
|
||||
|
||||
async def get_field(request):
|
||||
"""Get the value of some HTTP field in the given connection."""
|
||||
id = request.match_info["id"]
|
||||
field = request.match_info["field"]
|
||||
|
||||
try:
|
||||
connection = CONNECTIONS[id]
|
||||
except KeyError:
|
||||
response = web.json_response(data=error_body("Handler ID Not Found"), status=404, reason="Not Found")
|
||||
else:
|
||||
try:
|
||||
content = await connection.get(field)
|
||||
except ValueError:
|
||||
return web.json_response(data=error_body("Invalid Resource Path"), status=400, reason="Bad Request")
|
||||
except KeyError:
|
||||
return web.json_response(data=error_body("Resource Item Not Found"), status=404, reason="Not Found")
|
||||
|
||||
if isinstance(content, StreamReader):
|
||||
response = web.StreamResponse(status=200, reason="OK")
|
||||
await response.prepare(request)
|
||||
|
||||
chunk = await content.readany()
|
||||
while chunk:
|
||||
await response.write(chunk)
|
||||
chunk = await content.readany()
|
||||
|
||||
await response.write_eof()
|
||||
else:
|
||||
response = web.Response(body=content)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
async def set_field(request):
|
||||
"""Set the value of some HTTP field in the given connection."""
|
||||
id = request.match_info["id"]
|
||||
field = request.match_info["field"]
|
||||
|
||||
try:
|
||||
connection = CONNECTIONS[id]
|
||||
except ValueError:
|
||||
return web.json_response(data=error_body("Invalid Resource Path"), status=400, reason="Bad Request")
|
||||
except KeyError:
|
||||
response = web.json_response(data=error_body("Handler ID Not Found"), status=404, reason="Not Found")
|
||||
else:
|
||||
try:
|
||||
await connection.set(field, request.content)
|
||||
except ConnectionResetError:
|
||||
# Raised when trying to write to an already-closed stream.
|
||||
request.transport.close()
|
||||
else:
|
||||
response = web.Response(body=b'')
|
||||
|
||||
return response
|
||||
|
||||
|
||||
async def append_field(request):
|
||||
pass
|
||||
|
||||
|
||||
########################################################################
|
||||
# Endpoint Execution #
|
||||
########################################################################
|
||||
|
||||
|
||||
def handle_route(entrypoint, command):
|
||||
"""
|
||||
Return an aiohttp route handler that will execute entrypoint and
|
||||
command in order to manage a Kapow! route.
|
||||
|
||||
"""
|
||||
async def _handle(request):
|
||||
# Register a new connection to Kapow!
|
||||
id = "CONN_" + str(uuid4()).replace('-', '_')
|
||||
connection = CONNECTIONS[id] = Connection(request)
|
||||
|
||||
# Run entrypoint + command passing the connection id
|
||||
executable, *params = shlex.split(entrypoint)
|
||||
args = ' '.join([executable]
|
||||
+ [shlex.quote(token) for token in params]
|
||||
+ [shlex.quote(command)])
|
||||
try:
|
||||
shell_task = await asyncio.create_subprocess_shell(
|
||||
args,
|
||||
env={**os.environ,
|
||||
"KAPOW_DATAAPI_URL": "http://localhost:8081",
|
||||
"KAPOW_CONTROLAPI_URL": "http://localhost:8081",
|
||||
"KAPOW_HANDLER_ID": id
|
||||
},
|
||||
stdin=asyncio.subprocess.DEVNULL)
|
||||
|
||||
await shell_task.wait()
|
||||
except:
|
||||
raise
|
||||
else:
|
||||
# Respond when the command finish
|
||||
return await connection.build_response()
|
||||
finally:
|
||||
del CONNECTIONS[id]
|
||||
|
||||
return _handle
|
||||
|
||||
|
||||
########################################################################
|
||||
# Route Management #
|
||||
########################################################################
|
||||
|
||||
|
||||
def error_body(reason):
|
||||
return {"reason": reason, "foo": "bar"}
|
||||
|
||||
def get_routes(app):
|
||||
async def _get_routes(request):
|
||||
"""Return the list of registered routes."""
|
||||
data = [{"index": idx,
|
||||
"method": r.method,
|
||||
"id": r.id,
|
||||
"url_pattern": r.path,
|
||||
"entrypoint": r.entrypoint,
|
||||
"command": r.command}
|
||||
for idx, r in enumerate(app["user_routes"])]
|
||||
return web.json_response(data)
|
||||
return _get_routes
|
||||
|
||||
|
||||
def get_route(app):
|
||||
async def _get_route(request):
|
||||
"""Return requested registered route."""
|
||||
id = request.match_info["id"]
|
||||
for idx, r in enumerate(app["user_routes"]):
|
||||
if r.id == id:
|
||||
return web.json_response({"index": idx,
|
||||
"method": r.method,
|
||||
"id": r.id,
|
||||
"url_pattern": r.path,
|
||||
"entrypoint": r.entrypoint,
|
||||
"command": r.command})
|
||||
else:
|
||||
return web.json_response(data=error_body("Route Not Found"), status=404, reason="Not Found")
|
||||
return _get_route
|
||||
|
||||
|
||||
def insert_route(app):
|
||||
async def _insert_route(request):
|
||||
"""Insert a new Kapow! route."""
|
||||
try:
|
||||
content = await request.json()
|
||||
except ValueError:
|
||||
return web.json_response(data=error_body("Malformed JSON"), status=400, reason="Bad Request")
|
||||
|
||||
try:
|
||||
index = int(content["index"])
|
||||
assert index >= 0
|
||||
method = content.get("method", "GET")
|
||||
entrypoint = content.get("entrypoint", "/bin/sh -c")
|
||||
command = content.get("command", "")
|
||||
route = KapowRoute(method=method,
|
||||
path=content["url_pattern"],
|
||||
id="ROUTE_" + str(uuid4()).replace('-', '_'),
|
||||
entrypoint=entrypoint,
|
||||
command=command,
|
||||
handler=handle_route(entrypoint, command))
|
||||
app.change_routes((app["user_routes"][:index]
|
||||
+ [route]
|
||||
+ app["user_routes"][index:]))
|
||||
except (InvalidRouteError, KeyError, AssertionError, ValueError) as exc:
|
||||
return web.json_response(data=error_body("Invalid Route"), status=422, reason="Unprocessable Entity")
|
||||
else:
|
||||
app["user_routes"].insert(index, route)
|
||||
return web.json_response({"id": route.id,
|
||||
"method": route.method,
|
||||
"url_pattern": route.path,
|
||||
"entrypoint": route.entrypoint,
|
||||
"command": route.command,
|
||||
"index": index}, status=201)
|
||||
return _insert_route
|
||||
|
||||
|
||||
def append_route(app):
|
||||
async def _append_route(request):
|
||||
"""Append a new Kapow! route."""
|
||||
try:
|
||||
content = await request.json()
|
||||
except ValueError as exc:
|
||||
return web.json_response(data=error_body("Malformed JSON"), status=400, reason="Bad Request")
|
||||
|
||||
try:
|
||||
method = content.get("method", "GET")
|
||||
entrypoint = content.get("entrypoint", "/bin/sh -c")
|
||||
command = content.get("command", "")
|
||||
route = KapowRoute(method=method,
|
||||
path=content["url_pattern"],
|
||||
id="ROUTE_" + str(uuid4()).replace('-', '_'),
|
||||
entrypoint=entrypoint,
|
||||
command=command,
|
||||
handler=handle_route(entrypoint, command))
|
||||
app.change_routes(app["user_routes"] + [route])
|
||||
except (InvalidRouteError, KeyError) as exc:
|
||||
return web.json_response(data=error_body("Invalid Route"), status=422, reason="Unprocessable Entity")
|
||||
else:
|
||||
app["user_routes"].append(route)
|
||||
return web.json_response({"id": route.id,
|
||||
"method": route.method,
|
||||
"url_pattern": route.path,
|
||||
"entrypoint": route.entrypoint,
|
||||
"command": route.command,
|
||||
"index": len(app["user_routes"])-1},
|
||||
status=201)
|
||||
return _append_route
|
||||
|
||||
|
||||
def delete_route(app):
|
||||
async def _delete_route(request):
|
||||
"""Delete the given Kapow! route."""
|
||||
id = request.match_info["id"]
|
||||
routes = [r for r in app["user_routes"] if r.id != id]
|
||||
if len(routes) == len(app["user_routes"]):
|
||||
return web.json_response(data=error_body("Route Not Found"), status=404, reason="Not Found")
|
||||
else:
|
||||
app.change_routes(routes)
|
||||
app["user_routes"] = routes
|
||||
return web.Response(status=204, reason="No Content")
|
||||
return _delete_route
|
||||
|
||||
|
||||
########################################################################
|
||||
# aiohttp webapp #
|
||||
########################################################################
|
||||
|
||||
|
||||
async def run_init_script(app, scripts, interactive):
|
||||
"""
|
||||
Run the init script if given, then wait for the shell to finish.
|
||||
|
||||
"""
|
||||
if not scripts:
|
||||
# No script given
|
||||
if not interactive:
|
||||
return
|
||||
else:
|
||||
cmd = "/bin/bash"
|
||||
else:
|
||||
def build_filenames():
|
||||
for filename in scripts:
|
||||
yield shlex.quote(filename)
|
||||
yield "<(echo)"
|
||||
filenames = " ".join(build_filenames())
|
||||
if interactive:
|
||||
cmd = f"/bin/bash --init-file <(cat {filenames})"
|
||||
else:
|
||||
cmd = f"/bin/bash <(cat {filenames})"
|
||||
|
||||
shell_task = await asyncio.create_subprocess_shell(
|
||||
cmd,
|
||||
executable="/bin/bash",
|
||||
env={**os.environ,
|
||||
"KAPOW_DATAAPI_URL": "http://localhost:8081",
|
||||
"KAPOW_CONTROLAPI_URL": "http://localhost:8081"
|
||||
})
|
||||
|
||||
await shell_task.wait()
|
||||
if interactive:
|
||||
await app.cleanup()
|
||||
os._exit(shell_task.returncode)
|
||||
|
||||
|
||||
class InvalidRouteError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DynamicApplication(web.Application):
|
||||
"""
|
||||
A wrapper around `aiohttp.web.Application` allowing changing routes
|
||||
dynamically.
|
||||
|
||||
This is not safe as mentioned here:
|
||||
https://github.com/aio-libs/aiohttp/issues/3238.
|
||||
|
||||
On the other hand this is a PoC anyway...
|
||||
|
||||
"""
|
||||
def change_routes(self, routes):
|
||||
router = UrlDispatcher()
|
||||
try:
|
||||
for route in routes:
|
||||
router.add_route(route.method,
|
||||
route.path,
|
||||
route.handler,
|
||||
name=route.id)
|
||||
except Exception as exc:
|
||||
raise InvalidRouteError("Invalid route") from exc
|
||||
else:
|
||||
self._router = router
|
||||
if self._frozen:
|
||||
self._router.freeze()
|
||||
|
||||
|
||||
KapowRoute = namedtuple('KapowRoute',
|
||||
('method',
|
||||
'path',
|
||||
'id',
|
||||
'entrypoint',
|
||||
'command',
|
||||
'handler'))
|
||||
|
||||
|
||||
async def start_background_tasks(app):
|
||||
global loop
|
||||
app["debug_tasks"] = loop.create_task(run_init_script(app, app["scripts"], app["interactive"]))
|
||||
|
||||
|
||||
async def start_kapow_server(bind, scripts, certfile=None, interactive=False, keyfile=None):
|
||||
user_app = DynamicApplication(client_max_size=1024**3)
|
||||
user_app["user_routes"] = list() # [KapowRoute]
|
||||
user_runner = web.AppRunner(user_app)
|
||||
await user_runner.setup()
|
||||
|
||||
ssl_context = None
|
||||
if certfile and keyfile:
|
||||
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||
ssl_context.load_cert_chain(certfile, keyfile)
|
||||
|
||||
ip, port = bind.split(':')
|
||||
user_site = web.TCPSite(user_runner, ip, int(port), ssl_context=ssl_context)
|
||||
await user_site.start()
|
||||
|
||||
control_app = web.Application(client_max_size=1024**3)
|
||||
control_app.add_routes([
|
||||
# Control API
|
||||
web.get('/routes', get_routes(user_app)),
|
||||
web.get('/routes/{id}', get_route(user_app)),
|
||||
web.post('/routes', append_route(user_app)),
|
||||
web.put('/routes', insert_route(user_app)),
|
||||
web.delete('/routes/{id}', delete_route(user_app)),
|
||||
|
||||
# Data API
|
||||
web.get('/handlers/{id}/{field:.*}', get_field),
|
||||
web.put('/handlers/{id}/{field:.*}', set_field),
|
||||
])
|
||||
control_app["scripts"] = scripts
|
||||
control_app["interactive"] = interactive
|
||||
control_app.on_startup.append(start_background_tasks)
|
||||
|
||||
control_runner = web.AppRunner(control_app)
|
||||
await control_runner.setup()
|
||||
control_site = web.TCPSite(control_runner, '127.0.0.1', 8081)
|
||||
await control_site.start()
|
||||
|
||||
|
||||
|
||||
########################################################################
|
||||
# Command Line #
|
||||
########################################################################
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.pass_context
|
||||
def kapow(ctx):
|
||||
"""Start aiohttp app."""
|
||||
pass
|
||||
|
||||
|
||||
@kapow.command(help="Start a Kapow! server")
|
||||
@click.option("--certfile", default=None)
|
||||
@click.option("--keyfile", default=None)
|
||||
@click.option("--bind", default="0.0.0.0:8080")
|
||||
@click.option("-i", "--interactive", is_flag=True)
|
||||
@click.argument("scripts", nargs=-1)
|
||||
def server(certfile, keyfile, bind, interactive, scripts):
|
||||
if bool(certfile) ^ bool(keyfile):
|
||||
print("For SSL both 'certfile' and 'keyfile' should be provided.")
|
||||
sys.exit(1)
|
||||
loop.run_until_complete(start_kapow_server(bind, scripts, certfile, interactive, keyfile))
|
||||
loop.run_forever()
|
||||
|
||||
@kapow.group(help="Manage current server HTTP routes")
|
||||
def route():
|
||||
pass
|
||||
|
||||
|
||||
@route.command("add")
|
||||
@click.option("-c", "--command", nargs=1)
|
||||
@click.option("-e", "--entrypoint", default="/bin/sh -c")
|
||||
@click.option("-X", "--method", default="GET")
|
||||
@click.option("--url", envvar='KAPOW_CONTROLAPI_URL')
|
||||
@click.argument("url_pattern", nargs=1)
|
||||
@click.argument("command_file", required=False)
|
||||
def route_add(url_pattern, entrypoint, command, method, url, command_file):
|
||||
if command:
|
||||
# Command is given inline
|
||||
source = command
|
||||
elif command_file is None:
|
||||
# No command
|
||||
source = ""
|
||||
elif command_file == '-':
|
||||
# Read commands from stdin
|
||||
source = sys.stdin.read()
|
||||
else:
|
||||
# Read commands from a file
|
||||
with open(command_file, 'r', encoding='utf-8') as handler:
|
||||
source = handler.read()
|
||||
|
||||
response = requests.post(f"{url}/routes",
|
||||
json={"method": method,
|
||||
"url_pattern": url_pattern,
|
||||
"entrypoint": entrypoint,
|
||||
"command": source})
|
||||
response.raise_for_status()
|
||||
print(json.dumps(response.json(), indent=2))
|
||||
|
||||
|
||||
@route.command("remove")
|
||||
@click.option("--url", envvar='KAPOW_CONTROLAPI_URL')
|
||||
@click.argument("route-id")
|
||||
def route_remove(route_id, url):
|
||||
response = requests.delete(f"{url}/routes/{route_id}")
|
||||
response.raise_for_status()
|
||||
|
||||
|
||||
@route.command("list")
|
||||
@click.option("--url", envvar='KAPOW_CONTROLAPI_URL')
|
||||
@click.argument("route-id", nargs=1, required=False, default=None)
|
||||
def route_list(route_id, url):
|
||||
if route_id is None:
|
||||
response = requests.get(f"{url}/routes")
|
||||
else:
|
||||
response = requests.get(f"{url}/routes/{route_id}")
|
||||
response.raise_for_status()
|
||||
print(json.dumps(response.json(), indent=2))
|
||||
|
||||
|
||||
@kapow.command("set", help="Set data from the current context")
|
||||
@click.option("--url", envvar='KAPOW_DATAAPI_URL')
|
||||
@click.option("--handler-id", envvar='KAPOW_HANDLER_ID')
|
||||
@click.argument("path", nargs=1)
|
||||
@click.argument("value", required=False)
|
||||
def kapow_set(url, handler_id, path, value):
|
||||
if value is None:
|
||||
data = sys.stdin.buffer
|
||||
else:
|
||||
data = value.encode('utf-8')
|
||||
|
||||
try:
|
||||
response = requests.put(f"{url}/handlers/{handler_id}{path}",
|
||||
data=data)
|
||||
except requests.exceptions.ConnectionError:
|
||||
return False
|
||||
else:
|
||||
response.raise_for_status()
|
||||
|
||||
|
||||
@kapow.command("get", help="Get data from the current context")
|
||||
@click.option("--url", envvar='KAPOW_DATAAPI_URL')
|
||||
@click.option("--handler-id", envvar='KAPOW_HANDLER_ID')
|
||||
@click.argument("path", nargs=1)
|
||||
def kapow_get(url, handler_id, path):
|
||||
try:
|
||||
response = requests.get(f"{url}/handlers/{handler_id}{path}",
|
||||
stream=True)
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.ConnectionError:
|
||||
return False
|
||||
else:
|
||||
for chunk in response.iter_content(chunk_size=None):
|
||||
sys.stdout.buffer.write(chunk)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
kapow()
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
|
||||
BASE="$1"
|
||||
PATHNAME="$2"
|
||||
REAL="$(realpath --relative-base="$BASE" "$BASE/$PATHNAME")"
|
||||
|
||||
if [ ! -f "$BASE/$PATHNAME" ]; then
|
||||
kapow set /response/status 404
|
||||
exit
|
||||
else
|
||||
case $REAL in
|
||||
"/"*)
|
||||
kapow set /response/status 403
|
||||
exit
|
||||
;;
|
||||
*)
|
||||
kapow set /response/status 200
|
||||
kapow set /response/headers/Content-Type "$(python -m mimetypes "$BASE/$REAL" | awk '/type:/ {print $2; exit 0}; !/type:/ {print "application/octet-stream"}')"
|
||||
kapow set /response/body < "$BASE/$REAL"
|
||||
esac
|
||||
fi
|
||||
@@ -0,0 +1,21 @@
|
||||
[metadata]
|
||||
name = kapow
|
||||
version = 0.0.1
|
||||
description = HTTP microframework for POSIX Shells
|
||||
keywords = shell, posix, http, microservice, api
|
||||
license = Apache 2.0
|
||||
|
||||
[options]
|
||||
zip_safe = True
|
||||
include_package_data = True
|
||||
scripts =
|
||||
bin/kapow
|
||||
bin/static
|
||||
install_requires =
|
||||
aiohttp==3.5.4
|
||||
requests==2.22.0
|
||||
click==7.0
|
||||
|
||||
[pycodestyle]
|
||||
max-line-length = 100
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
from setuptools import setup
|
||||
|
||||
|
||||
print("""
|
||||
. . .,---.,---., .|, .,---.|
|
||||
| | ||---||---'|\ |||\ || _.|
|
||||
| | || || \ | \ ||| \ || |
|
||||
`-'-'` '` `` `'`` `'`---'o
|
||||
================================
|
||||
|
||||
You are installing a proof-of-concept software that IS NOT PRODUCTION READY.
|
||||
|
||||
""")
|
||||
|
||||
setup()
|
||||
Reference in New Issue
Block a user