Skip to content

Commit b270d53

Browse files
committed
Updated Marketplace models & log_level settings
1 parent 6a7fb18 commit b270d53

11 files changed

+225
-177
lines changed

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ unfixable = [ "B" ]
7575

7676
[tool.ruff.format]
7777
line-ending = "lf"
78+
#quote-style="single"
7879
docstring-code-format = false
7980
docstring-code-line-length = "dynamic"
8081

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__ = "1.0.3"
5+
__version__ = "1.0.4"

tenint/connector.py

+72-85
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import json
1010
import logging
1111
import os
12-
import tomllib
1312
from enum import Enum
1413
from pathlib import Path
1514
from typing import Annotated, Callable
@@ -29,10 +28,6 @@
2928
logger = logging.getLogger("tenint.connector")
3029

3130

32-
class ConfigurationError(Exception):
33-
pass
34-
35-
3631
class LogLevel(str, Enum):
3732
notset = "notset"
3833
debug = "debug"
@@ -105,53 +100,63 @@ def log_env_vars(self) -> None:
105100
value = "{{ HIDDEN }}"
106101
logger.debug(f'EnvVar {key}="{value}"')
107102

108-
def fetch_config(
109-
self,
110-
data: str | None = None,
111-
fn: Path | None = None,
112-
) -> BaseModel:
103+
def fetch_config(self, data: str) -> (BaseModel, int):
113104
"""
114105
Fetch and validate the configuration from either the data string or the filepath
115106
and return the settings object to the caller.
116107
117108
Args:
118-
data:
119-
The string object of the
109+
data: JSON formatted string of the settings
110+
111+
Returns:
112+
The pydantic settings model and the status code.
113+
"""
114+
try:
115+
return self.settings(**json.loads(data)), 0
116+
except ValidationError as e:
117+
self.console.print(f"JSON String validation failed: {e}")
118+
except Exception as _:
119+
self.console.print_exception()
120+
return None, 2
121+
122+
def callback(
123+
self, url: str | None, resp: dict, job_id: str | None, status_code: int
124+
) -> None:
120125
"""
121-
settings = None
122-
123-
# If a string object is passed, we will handle that first.
124-
if data:
125-
settings = self.settings(**json.loads(data))
126-
127-
# If a file has been passed in instead an the suffix appears to be a JSON
128-
# suffix, then we will assume a JSON file and handle accordingly.
129-
elif fn and fn.is_file() and fn.suffix.lower() in [".json", ".jsn"]:
130-
with fn.open("r", encoding="utf-8") as fobj:
131-
settings = self.settings(**json.load(fobj))
132-
133-
# If the file passed has a TOML suffix, we will process as a toml file through
134-
# tomllib.
135-
elif fn and fn.is_file() and fn.suffix.lower() in [".toml", ".tml"]:
136-
with fn.open("rb") as fobj:
137-
settings = self.settings(**tomllib.load(fobj))
138-
139-
# If we processed anything, then return the settings object, otherwise raise a
140-
# ConfigurationError
141-
if settings:
142-
logger.debug(f"Job config={settings.model_dump(mode='json')}")
143-
return settings
144-
raise ConfigurationError("No valid configurations passed.")
126+
Initiate the callback response to the job scheduler
127+
128+
Args:
129+
url: Callback url
130+
resp: Dictionary response of the job
131+
job_id: The Job UUID
132+
status_code: Job status
133+
"""
134+
# Set the Callback payload to the job response if the response is a dictionary
135+
try:
136+
payload = CallbackResponse(exit_code=status_code, **resp).model_dump(
137+
mode="json"
138+
)
139+
except (ValidationError, TypeError) as _:
140+
logger.error(f"Job response format is invalid: {resp=}")
141+
payload = CallbackResponse(exit_code=status_code).model_dump(mode="json")
142+
143+
# If a callback and a job id have been set, then we will initiate a callback
144+
# to the job manager with the response payload of the job to inform the manager
145+
# that we have finished.
146+
if job_id and url:
147+
requests.post(url, headers={"X-Job-ID": job_id}, json=payload, verify=False)
148+
logger.info(f"Called back to {url=} with {job_id=} and {payload=}")
149+
else:
150+
logger.warning("Did not initiate a callback!")
151+
logger.info(f"callback={payload}")
145152

146153
def cmd_config(
147154
self, pretty: Annotated[bool, Option(help="Pretty format the response")] = False
148155
):
149156
"""
150157
Return the connector configuration
151158
"""
152-
indent = 2
153-
if not pretty:
154-
indent = None
159+
indent = 2 if pretty else None
155160

156161
class Config(Configuration):
157162
settings_model: type[Settings] = self.settings
@@ -173,23 +178,14 @@ def cmd_validate(self):
173178
def cmd_run(
174179
self,
175180
json_data: Annotated[
176-
str | None,
181+
str,
177182
Option(
178183
"--json",
179184
"-j",
180185
envvar="CONFIG_JSON",
181186
help="The JSON config object as a string",
182187
),
183-
] = None,
184-
filename: Annotated[
185-
Path | None,
186-
Option(
187-
"--filename",
188-
"-f",
189-
envvar="CONFIG_FILENAME",
190-
help="Filename of either a json or toml file containing the job config",
191-
),
192-
] = None,
188+
],
193189
job_id: Annotated[
194190
str | None,
195191
Option(
@@ -209,15 +205,15 @@ def cmd_run(
209205
),
210206
] = None,
211207
log_level: Annotated[
212-
LogLevel,
208+
LogLevel | None,
213209
Option(
214210
"--log-level",
215211
"-l",
216212
envvar="LOG_LEVEL",
217213
help="Sets the logging verbosity for the job",
218214
case_sensitive=False,
219215
),
220-
] = LogLevel.info,
216+
] = None,
221217
since: Annotated[
222218
int | None,
223219
Option(
@@ -231,13 +227,24 @@ def cmd_run(
231227
"""
232228
Invoke the connector
233229
"""
230+
resp = None
231+
config, status_code = self.fetch_config(json_data)
232+
233+
# Set the log level, using local before config and then ultimately setting the
234+
# log level to debug if nothing has been set.
235+
if log_level:
236+
lvl = log_level.upper()
237+
elif config:
238+
lvl = config.log_level
239+
else:
240+
lvl = "DEBUG"
241+
242+
# Configure the logging handlers
234243
logging.basicConfig(
235-
level=log_level.upper(),
236-
# format="%(asctime) %(name)s(%(filename)s:%(lineno)d): %(message)s",
244+
level=lvl,
237245
format="%(name)s: %(message)s",
238246
datefmt="%Y-%m-%d %H:%M:%S",
239247
handlers=[
240-
# logging.FileHandler("job.log"),
241248
RichHandler(
242249
console=self.console,
243250
show_path=False,
@@ -248,46 +255,26 @@ def cmd_run(
248255
logging.StreamHandler(),
249256
],
250257
)
258+
259+
# Log the environment variables and configuration data
251260
self.log_env_vars()
252261
logger.info(f"Logging to {self.logfile}")
253-
status_code = 0
254-
resp = None
262+
logger.debug(f"Job {json_data=}")
263+
logger.info(f"Job config={config.model_dump_json() if config else None}")
255264

256265
# Attempt to run the connector job and handle any errors that may be thrown
257266
# in a graceful way.
258267
try:
259-
config = self.fetch_config(json_data, filename)
260-
resp = self.main(config=config, since=since)
261-
except (ValidationError, ConfigurationError) as e:
262-
logging.error(f"Invalid configuration presented: {e}")
263-
status_code = 2
268+
if status_code == 0:
269+
resp = self.main(config=config, since=since)
264270
except Exception as _:
265271
logging.exception("Job run failed with error", stack_info=2)
266272
status_code = 1
267273

268-
# Set the Callback payload to the job response if the response is a dictionary
269-
try:
270-
payload = CallbackResponse(exit_code=status_code, **resp).model_dump(
271-
mode="json"
272-
)
273-
except (ValidationError, TypeError) as _:
274-
logger.error(f"Job response format is invalid: {resp=}")
275-
payload = CallbackResponse(exit_code=status_code).model_dump(mode="json")
276-
277-
# If a callback and a job id have been set, then we will initiate a callback
278-
# to the job manager with the response payload of the job to inform the manager
279-
# that we have finished.
280-
if job_id and callback_url:
281-
requests.post(
282-
callback_url,
283-
headers={"X-Job-ID": job_id},
284-
json=payload,
285-
verify=False,
286-
)
287-
logger.info(f"Called back to {callback_url=} with {job_id=} and {payload=}")
288-
else:
289-
logger.warning("Did not initiate a callback!")
290-
logger.info(f"callback={payload}")
274+
# Initiate the callback to the job management system.
275+
self.callback(
276+
url=callback_url, job_id=job_id, resp=resp, status_code=status_code
277+
)
291278

292279
# Exit the connector with the status code from the runner.
293280
raise Exit(code=status_code)

tenint/models/configuration.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any
1+
from typing import Any, Literal
22

33
from pydantic import BaseModel, ConfigDict, Field, computed_field
44

@@ -7,6 +7,7 @@
77

88
class Settings(BaseModel):
99
model_config = ConfigDict(extra="forbid")
10+
log_level: Literal["INFO", "DEBUG"] = "INFO"
1011

1112

1213
class Configuration(BaseModel):

tenint/models/marketplace.py

+19-2
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,38 @@
22

33
import tomllib
44
from pathlib import Path
5+
from typing import Literal
56

67
from pydantic import AnyHttpUrl, BaseModel, EmailStr
78

89
from .pyproject import PyProject
910

1011

12+
class MarketplaceResources(BaseModel):
13+
disk: int
14+
memory: int
15+
cpu_cores: int
16+
17+
18+
class MarketplaceTimeout(BaseModel):
19+
min: int
20+
default: int
21+
max: int
22+
23+
1124
class MarketplaceConnector(BaseModel):
1225
name: str
1326
slug: str
1427
description: str
1528
icon_url: AnyHttpUrl
1629
image_url: str
17-
timeout: int
1830
marketplace_tag: str
1931
connector_owner: str
2032
support_contact: EmailStr
2133
tags: list[str]
34+
schedule_types: list[Literal["hourly", "daily"]]
35+
resources: MarketplaceResources
36+
timeout: MarketplaceTimeout
2237

2338
@classmethod
2439
def load_from_pyproject(
@@ -55,7 +70,9 @@ def load_from_pyproject(
5570
description=obj.project.description,
5671
icon_url=icon_url,
5772
image_url=image_url,
58-
timeout=obj.tool.tenint.connector.timeout,
73+
timeout=obj.tool.tenint.connector.timeout.model_dump(),
74+
resources=obj.tool.tenint.connector.resources.model_dump(),
75+
schedule_types=obj.tool.tenint.connector.schedule_types,
5976
marketplace_tag=obj.project.version,
6077
connector_owner=obj.project.authors[0].name,
6178
support_contact=obj.project.authors[0].email,

tenint/models/pyproject.py

+18-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from typing import Annotated
1+
from typing import Annotated, Literal
22

3-
from pydantic import BaseModel, Field
3+
from pydantic import BaseModel, Field, model_validator
4+
from pydantic.functional_validators import model_validator
45
from pydantic.networks import AnyHttpUrl, EmailStr
56

67

@@ -29,10 +30,24 @@ class Project(BaseModel):
2930
]
3031

3132

33+
class TenintConnectorTimeout(BaseModel):
34+
min: int = 3600
35+
default: int = 3600
36+
max: int = 86400
37+
38+
39+
class TenintConnectorResources(BaseModel):
40+
disk: int = 1024
41+
memory: int = 1024
42+
cpu_cores: int = 1
43+
44+
3245
class TenintConnector(BaseModel):
3346
title: str
3447
tags: list[str]
35-
timeout: int = 3600
48+
schedule_types: list[Literal["hourly", "daily"]] = ["daily"]
49+
timeout: Annotated[TenintConnectorTimeout, Field(validate_default=True)] = {}
50+
resources: Annotated[TenintConnectorResources, Field(validate_default=True)] = {}
3651

3752

3853
class Tenint(BaseModel):

tenint/templates/pyproject.toml

+12-1
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,15 @@ title = "Example Connector"
4242
tags = ['tvm', 'example']
4343

4444
# The default maximum run time in seconds
45-
timeout = 3600
45+
[tool.tenint.connector.timeout]
46+
default = 3600
47+
48+
49+
# This section defines the minimum resources necessary to run the
50+
# connector. Below are the default values. Disk and Memory values
51+
# are in Megabytes, where CPU Cores is is simply the number of cores
52+
# required to run the connector.
53+
[tool.tenint.connector.resources]
54+
disk = 1024
55+
memory = 1024
56+
cpu_cores = 1

tests/models/test_configuration.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,13 @@ class TestConfiguration(Configuration):
2727
def test_config(config):
2828
assert config.settings == {
2929
"properties": {
30-
"is_true": {"default": True, "title": "Is True", "type": "boolean"}
30+
"is_true": {"default": True, "title": "Is True", "type": "boolean"},
31+
"log_level": {
32+
"default": "INFO",
33+
"title": "Log Level",
34+
"type": "string",
35+
"enum": ["INFO", "DEBUG"],
36+
},
3137
},
3238
"title": "TestSettings",
3339
"type": "object",

0 commit comments

Comments
 (0)