Skip to content

Commit

Permalink
Fix #539 http: add client_id and authentication (#543)
Browse files Browse the repository at this point in the history
- Added quest.Attr.IS_SINGLETON to enable http.Session 
- documented quest.Attr.
- Fixed pksubprocess_test (hopefully) after discovering #544 race condition,
which only shows up when running parallel tests. #554 is not fixed by this
commit.
- test.sh also now prints the pid/time, which is helpful for debugging.
  • Loading branch information
robnagler authored Jan 21, 2025
1 parent 1d9f364 commit c646065
Show file tree
Hide file tree
Showing 8 changed files with 263 additions and 162 deletions.
289 changes: 167 additions & 122 deletions pykern/http.py

Large diffs are not rendered by default.

18 changes: 14 additions & 4 deletions pykern/http_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

class Setup:

AUTH_TOKEN = "http_unit_auth_secret"

def __init__(self, api_classes, attr_classes=(), coros=()):
import os, time
from pykern.pkcollections import PKDict
Expand All @@ -29,21 +31,26 @@ def _http_config():
# any uri is fine
api_uri="/http_unit",
# just needs to be >= 16 word (required by http) chars; apps should generate this randomly
auth_secret="http_unit_auth_secret",
tcp_ip=pkconst.LOCALHOST_IP,
tcp_port=pkunit.unbound_localhost_tcp_port(),
)

def _server():
from pykern import pkdebug, http

def _api_classes():
class AuthAPI(http.AuthAPI):
PYKERN_HTTP_TOKEN = self.AUTH_TOKEN

return list(api_classes) + [AuthAPI]

if rv := os.fork():
return rv
try:
pkdebug.pkdlog("start server")
http.server_start(
attr_classes=attr_classes,
api_classes=api_classes,
api_classes=_api_classes(),
http_config=self.http_config.copy(),
coros=coros,
)
Expand All @@ -65,9 +72,12 @@ def destroy(self):

os.kill(self.server_pid, signal.SIGKILL)

def __enter__(self):
async def __aenter__(self):
from pykern import http

await self.client.connect(http.AuthArgs(token=self.AUTH_TOKEN))
return self.client

def __exit__(self, *args, **kwargs):
async def __aexit__(self, *args, **kwargs):
self.client.destroy()
return False
4 changes: 2 additions & 2 deletions pykern/pkcollections.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""PKDict abstraction and utils
`PKDict` is similar to :class:`argparse.Namespace`, but is a dict that allows
Expand All @@ -7,6 +6,7 @@
:copyright: Copyright (c) 2015-2022 RadiaSoft LLC. All Rights Reserved.
:license: http://www.apache.org/licenses/LICENSE-2.0.html
"""

# Limit pykern imports so avoid dependency issues for pkconfig
import copy
import collections.abc
Expand Down Expand Up @@ -63,7 +63,7 @@ def __setattr__(self, name, value):
raise PKDictNameError(
"{}: invalid key for PKDict matches existing attribute".format(name)
)
super(PKDict, self).__setitem__(name, value)
self[name] = value

def copy(self):
"""Override `dict.copy` to ensure the class of the return object is correct.
Expand Down
8 changes: 4 additions & 4 deletions pykern/pksubprocess.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
# -*- coding: utf-8 -*-
"""Wrapper for subprocess.
:copyright: Copyright (c) 2016 RadiaSoft LLC. All Rights Reserved.
:license: http://www.apache.org/licenses/LICENSE-2.0.html
"""
from __future__ import absolute_import, division, print_function
from pykern.pkdebug import pkdc, pkdexc, pkdp

from pykern.pkdebug import pkdc, pkdexc, pkdp, pkdlog
import os
import signal
import six
Expand Down Expand Up @@ -43,6 +42,7 @@ def signal_handler(sig, frame):
if p:
p.send_signal(sig)
except Exception:
# Nothing we can do, still want to cascade
pass
finally:
ps = prev_signal[sig]
Expand Down Expand Up @@ -111,7 +111,7 @@ def wait_pid():
finally:
for sig in _SIGNALS:
signal.signal(sig, prev_signal[sig])
if not p is None:
if p is not None:
if msg:
msg("{}: terminating: {}", pid, cmd)
try:
Expand Down
40 changes: 33 additions & 7 deletions pykern/quest.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,22 +51,48 @@ def __init__(self, fmt, *args, **kwargs):


class Attr(PKDict):
# Class names bound to attribute keys
_KEY_MAP = PKDict()
#: shared Attrs do not have link to qcall
IS_SINGLETON = False

def __init__(self, qcall, **kwargs):
"""Initialize object from a qcall
"""Initialize object
Subclasses must define ATTR_KEY so it can be added to qcall.
If `IS_SINGLETON` is true then qcall must be None. This will
only be called outside of init_quest. Otherwise, qcall is bound to instance.
Args:
qcall (API): what qcall is being initialized
kwargs (dict): insert into dictionary
"""
super().__init__(qcall=qcall, **kwargs)
qcall.attr_set(self.ATTR_KEY, self)
if self.IS_SINGLETON:
assert qcall is None
super().__init__(**kwargs)
else:
super().__init__(qcall=qcall, **kwargs)

@classmethod
def init_quest(cls, qcall):
cls(qcall)
def init_quest(cls, qcall, **kwargs):
"""Initialize an instance of cls and put on qcall
If `IS_SINGLETON`, qcall is not put on self. `kwargs` must contain ATTR_KEY,
which is an instance of class.
Args:
qcall (API): quest
kwargs (**): values to passed to `start`
"""
if not cls.IS_SINGLETON:
self = cls(qcall, **kwargs)
elif (self := kwargs.get(cls.ATTR_KEY)) is None:
raise AssertionError(f"init_quest.kwargs does not contain {cls.ATTR_KEY}")
elif not isinstance(self, cls):
raise AssertionError(
f"init_quest.kwargs.{cls.ATTR_KEY}={self} not instance of {cls}"
)
qcall.attr_set(self.ATTR_KEY, self)


class Spec:
Expand Down
2 changes: 1 addition & 1 deletion test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
set -euo pipefail

_main() {
PYKERN_PKCLI_TEST_MAX_PROCS=4 pykern ci run
PYKERN_PKDEBUG_WANT_PID_TIME=1 PYKERN_PKCLI_TEST_MAX_PROCS=4 pykern ci run
}

_main "$@"
10 changes: 6 additions & 4 deletions tests/http_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@
async def test_basic():
from pykern import http_unit

with http_unit.Setup(api_classes=(_class(),)) as c:
async with http_unit.Setup(api_classes=(_class(),)) as c:
from pykern.pkcollections import PKDict
from pykern import pkunit

e = PKDict(a=1)
pkunit.pkeq(e, await c.call_api("echo", e))
e = PKDict(ping="pong")
pkunit.pkeq(e.pkupdate(counter=1), await c.call_api("echo", e))
pkunit.pkeq(e.pkupdate(counter=2), await c.call_api("echo", e))


def _class():
Expand All @@ -25,6 +26,7 @@ def _class():
class _API(quest.API):

async def api_echo(self, api_args):
return api_args
self.session.pksetdefault(counter=0).counter += 1
return api_args.pkupdate(counter=self.session.counter)

return _API
54 changes: 36 additions & 18 deletions tests/pksubprocess_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
)
def test_check_call_with_signals():
from pykern import pksubprocess
from pykern import pkunit, pkcompat
from pykern import pkunit, pkcompat, pkdebug
import os
import signal
import subprocess
Expand Down Expand Up @@ -59,44 +59,62 @@ def signal_handler(sig, frame):
signals = []
signal.signal(signal.SIGTERM, signal_handler)
with open("kill.sh", "w") as f:
f.write("kill -TERM {}\nsleep 10".format(os.getpid()))
# Need to wait before sending signal, because subprocess needs to allow
# this process (parent) to run
f.write(
f"""
sleep .2
kill -TERM {os.getpid()}
sleep 5
echo FAIL
"""
)
cmd = ["sh", "kill.sh"]
with pytest.raises(RuntimeError):
exc = None
try:
pksubprocess.check_call_with_signals(cmd, output=o, msg=msg)
except RuntimeError as e:
exc = e
except BaseException as e:
pkunit.pkfail("unexpected exception={}", e)
o.seek(0)
actual = o.read()
assert "" == actual, 'Expecting empty output "{}"'.format(actual)
assert signal.SIGTERM in signals, '"SIGTERM" not in signals "{}"'.format(
signals
pkunit.pkeq("", actual)
pkunit.pkok(
signal.SIGTERM in signals, '"SIGTERM" not in signals "{}"', signals
)
assert (
"error exit" in messages[1]
), '"error exit" not in messages[1] "{}"'.format(messages[1])
pkunit.pkre("error exit", messages[1])
if exc is None:
pkunit.pkfail("exception was not raised")

with open("kill.out", "w+") as o:
messages = []
signals = []
signal.signal(signal.SIGTERM, signal_handler)
with open("kill.sh", "w") as f:
f.write(
"""
setsid bash -c "sleep 1; echo hello; setsid sleep 1313 & disown" &
f"""
setsid bash -c "sleep 1; echo FAIL; setsid sleep 1313 & disown" &
disown
sleep .3
kill -TERM {}
sleep .1
""".format(
os.getpid()
)
sleep .2
kill -TERM {os.getpid()}
sleep 5
echo FAIL
"""
)
cmd = ["bash", "kill.sh"]
with pytest.raises(RuntimeError):
exc = None
try:
pksubprocess.check_call_with_signals(
cmd,
output=o,
msg=msg,
recursive_kill=True,
)
except RuntimeError as e:
exc = e
except BaseException as e:
pkunit.pkfail("unexpected exception={}", e)
time.sleep(2)
o.seek(0)
actual = o.read()
Expand Down

0 comments on commit c646065

Please sign in to comment.