Skip to content

Commit 3cb8472

Browse files
committed
Added initial callback payload support
1 parent 7db3cdc commit 3cb8472

File tree

6 files changed

+96
-38
lines changed

6 files changed

+96
-38
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ __pycache__
77
.vars
88
.secrets
99
requirements.txt
10+
/example

tenint/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
from .models.configuration import Configuration, Settings # noqa F401
33
from .connector import Connector # noqa F401
44

5-
__version__ = '0.9.1'
5+
__version__ = '0.9.2'

tenint/connector.py

+40-10
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@
2020
from typer import Exit, Option, Typer
2121
from typer.main import get_command_name
2222

23+
from .models.callback import CallbackResponse
2324
from .models.configuration import Configuration, Settings
2425
from .models.credentials import Credential
2526

27+
logger = logging.getLogger('tenint.connector')
28+
2629

2730
class ConfigurationError(Exception):
2831
pass
@@ -61,7 +64,6 @@ def __init__(
6164
):
6265
self.app = Typer(add_completion=False)
6366
self.console = Console()
64-
6567
self.settings = settings
6668
self.defaults = (
6769
defaults if defaults else settings.model_construct().model_dump()
@@ -215,25 +217,53 @@ def cmd_run(
215217
"""
216218
logging.basicConfig(
217219
level=log_level.upper(),
218-
format='%(asctime)s %(levelname)s %(message)s',
220+
format='%(name)s: %(message)s',
219221
datefmt='%Y-%m-%d %H:%M:%S',
220-
handlers=[RichHandler(console=self.console, rich_tracebacks=True)],
222+
handlers=[
223+
RichHandler(
224+
console=self.console,
225+
rich_tracebacks=True,
226+
omit_repeated_times=False,
227+
)
228+
],
221229
)
222230
status_code = 0
231+
resp = None
223232

233+
# Attempt to run the connector job and handle any errors that may be thrown
234+
# in a graceful way.
224235
try:
225236
config = self.fetch_config(json_data, filename)
226-
self.main(config=config, since=since)
227-
except (ValidationError, ConfigurationError) as exc:
228-
self.console.print(exc)
237+
resp = self.main(config=config, since=since)
238+
except (ValidationError, ConfigurationError) as _:
239+
logging.error('Invalid configuration presented', exc_info=True)
229240
status_code = 2
230241
except Exception as _:
231-
self.console.print_exception()
242+
logging.error('Job run failed with an error', exc_info=True)
232243
status_code = 1
233244

245+
# Set the Callback payload to the job response if the response is a dictionary
246+
try:
247+
payload = CallbackResponse(exit_code=status_code, **resp).model_dump(
248+
mode='json'
249+
)
250+
except (ValidationError, TypeError) as _:
251+
logger.error(f'Job response format is invalid: {resp=}')
252+
payload = CallbackResponse(exit_code=status_code).model_dump(mode='json')
253+
254+
# If a callback and a job id have been set, then we will initiate a callback
255+
# to the job manager with the response payload of the job to inform the manager
256+
# that we have finished.
234257
if job_id and callback_url:
235-
requests.post(callback_url, headers={'X-Job-ID': job_id})
236-
self.console.log(f'Called back to {callback_url=} with {job_id=}')
258+
requests.post(
259+
callback_url,
260+
headers={'X-Job-ID': job_id},
261+
json=payload,
262+
)
263+
logger.info(f'Called back to {callback_url=} with {job_id=} and {payload=}')
237264
else:
238-
self.console.print('Did not initiate a callback!', style='bold orange1')
265+
logger.warning('Did not initiate a callback!')
266+
self.console.print(f'callback={payload}')
267+
268+
# Exit the connector with the status code from the runner.
239269
raise Exit(code=status_code)

tenint/models/callback.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from pydantic import BaseModel
2+
3+
4+
class CallbackDataTypeCounts(BaseModel):
5+
sent: int = 0
6+
modified: int = 0
7+
failed: int = 0
8+
9+
10+
class CallbackCounters(BaseModel):
11+
assets: CallbackDataTypeCounts = CallbackDataTypeCounts()
12+
findings: CallbackDataTypeCounts = CallbackDataTypeCounts()
13+
14+
15+
class CallbackResponse(BaseModel):
16+
exit_code: int
17+
counts: CallbackCounters = CallbackCounters()

tenint/templates/connector.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@ class AppSettings(Settings):
2222

2323

2424
@connector.job
25-
def main(config: AppSettings, since: int | None = None):
25+
def main(config: AppSettings, since: int | None = None) -> dict:
2626
log.debug('This is a debug test')
2727
log.info('this is an info test')
2828
log.warning('this is a warning')
2929
log.error('this is an error')
3030
print('hello world')
31+
return {'counts': {'assets': {'sent': 0}}}
3132

3233

3334
if __name__ == '__main__':

tests/test_connector.py

+35-26
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,19 @@ def test_connector_validation(AppSettings):
9999
assert 'Not yet implemented' in result.stdout
100100

101101

102-
def test_connector_run(AppSettings, main):
102+
def test_connector_run(AppSettings, main, caplog):
103103
connector = Connector(settings=AppSettings)
104104
connector.job(main)
105-
result = runner.invoke(connector.app, ['run', '-j', '{"is_bool": true}'])
106-
assert result.exit_code == 0
107-
assert 'hello world' in result.stdout
108-
assert 'Did not initiate a callback!' in result.stdout
105+
with caplog.at_level(logging.INFO):
106+
result = runner.invoke(connector.app, ['run', '-j', '{"is_bool": true}'])
107+
assert result.exit_code == 0
108+
assert 'hello world' in result.stdout
109+
assert 'This is a debug test' not in caplog.text
110+
assert 'This is an info test' in caplog.text
111+
assert 'This is an error test' in caplog.text
112+
assert 'Job response format is invalid' in caplog.text
113+
assert 'Did not initiate a callback!' in caplog.text
114+
assert 'callback={' in result.stdout
109115

110116

111117
def test_connector_run_config_fail(AppSettings, main):
@@ -115,39 +121,42 @@ def test_connector_run_config_fail(AppSettings, main):
115121
assert result.exit_code == 2
116122

117123

118-
def test_connector_run_fail(AppSettings):
124+
def test_connector_run_fail(AppSettings, caplog):
119125
connector = Connector(settings=AppSettings)
120126

121127
@connector.job
122128
def failmain(config: dict, since: None = None):
123129
raise Exception('I have failed')
124130

125-
result = runner.invoke(connector.app, ['run', '-j', '{"is_bool": true}'])
126-
assert result.exit_code == 1
127-
assert 'I have failed' in result.stdout
131+
with caplog.at_level(logging.INFO):
132+
result = runner.invoke(connector.app, ['run', '-j', '{"is_bool": true}'])
133+
assert result.exit_code == 1
134+
assert 'I have failed' in caplog.text
128135

129136

130137
@responses.activate
131-
def test_connector_callback(AppSettings, main):
138+
def test_connector_callback(AppSettings, main, caplog):
132139
responses.post(
133140
'http://callback-url.local/callback',
134141
match=[matchers.header_matcher({'X-Job-ID': 'abcdef'})],
135142
)
136143
connector = Connector(settings=AppSettings)
137144
connector.job(main)
138-
result = runner.invoke(
139-
connector.app,
140-
[
141-
'run',
142-
'-j',
143-
'{"is_bool": true}',
144-
'-J',
145-
'abcdef',
146-
'-c',
147-
'http://callback-url.local/callback',
148-
],
149-
)
150-
assert result.exit_code == 0
151-
assert 'Called back' in result.stdout
152-
assert "callback_url='http://callback-url.local/callback'" in result.stdout
153-
assert "job_id='abcdef'" in result.stdout
145+
146+
with caplog.at_level(logging.INFO):
147+
result = runner.invoke(
148+
connector.app,
149+
[
150+
'run',
151+
'-j',
152+
'{"is_bool": true}',
153+
'-J',
154+
'abcdef',
155+
'-c',
156+
'http://callback-url.local/callback',
157+
],
158+
)
159+
assert result.exit_code == 0
160+
assert 'Called back' in caplog.text
161+
assert "callback_url='http://callback-url.local/callback'" in caplog.text
162+
assert "job_id='abcdef'" in caplog.text

0 commit comments

Comments
 (0)