ivcap_service 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ivcap_service-0.3.0/AUTHORS.md +5 -0
- ivcap_service-0.3.0/LICENSE +29 -0
- ivcap_service-0.3.0/PKG-INFO +129 -0
- ivcap_service-0.3.0/README.md +106 -0
- ivcap_service-0.3.0/pyproject.toml +47 -0
- ivcap_service-0.3.0/src/ivcap_service/__init__.py +12 -0
- ivcap_service-0.3.0/src/ivcap_service/ivcap.py +145 -0
- ivcap_service-0.3.0/src/ivcap_service/logger.py +27 -0
- ivcap_service-0.3.0/src/ivcap_service/logging.json +46 -0
- ivcap_service-0.3.0/src/ivcap_service/py.typed +1 -0
- ivcap_service-0.3.0/src/ivcap_service/service.py +166 -0
- ivcap_service-0.3.0/src/ivcap_service/service_definition.py +116 -0
- ivcap_service-0.3.0/src/ivcap_service/testing.py +63 -0
- ivcap_service-0.3.0/src/ivcap_service/tool_definition.py +406 -0
- ivcap_service-0.3.0/src/ivcap_service/types.py +34 -0
- ivcap_service-0.3.0/src/ivcap_service/utils.py +83 -0
- ivcap_service-0.3.0/src/ivcap_service/version.py +19 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023, Commonwealth Scientific and Industrial Research Organisation (CSIRO) ABN 41 687 119 230
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
* Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ivcap_service
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: SDK library for building services for the IVCAP platform
|
|
5
|
+
Author: Max Ott
|
|
6
|
+
Author-email: max.ott@csiro.au
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Requires-Dist: cachetools (>=5.5.2,<6.0.0)
|
|
15
|
+
Requires-Dist: opentelemetry-distro (>=0.51b0,<0.52)
|
|
16
|
+
Requires-Dist: opentelemetry-exporter-otlp (>=1.30.0,<2.0.0)
|
|
17
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi (>=0.51b0,<0.52)
|
|
18
|
+
Requires-Dist: opentelemetry-instrumentation-httpx (>=0.51b0,<0.52)
|
|
19
|
+
Requires-Dist: opentelemetry-instrumentation-requests (>=0.51b0,<0.52)
|
|
20
|
+
Requires-Dist: uuid6 (==2024.7.10)
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# ivcap_ai_tool: A python library for building AI tools for the IVCAP platform
|
|
24
|
+
|
|
25
|
+
<a href="https://scan.coverity.com/projects/ivcap-works-ivcap-ai-tool-sdk-python">
|
|
26
|
+
<img alt="Coverity Scan Build Status"
|
|
27
|
+
src="https://img.shields.io/coverity/scan/31491.svg"/>
|
|
28
|
+
</a>
|
|
29
|
+
|
|
30
|
+
A python library containing various helper and middleware functions
|
|
31
|
+
to simplify developing AI tools to be deployed on IVCAP.
|
|
32
|
+
|
|
33
|
+
> **Note:** A template git repositiory using this library can be found on github
|
|
34
|
+
[ivcap-works/ivcap-python-ai-tool-template](https://github.com/ivcap-works/ivcap-python-ai-tool-template). You may clone that and start from there.
|
|
35
|
+
|
|
36
|
+
## Content
|
|
37
|
+
|
|
38
|
+
* [Register a Tool Function](#register)
|
|
39
|
+
* [Start the Service](#start)
|
|
40
|
+
* [JSON-RPC Middleware](#json-rpc)
|
|
41
|
+
* [Try-Later Middleware](#try-later)
|
|
42
|
+
|
|
43
|
+
### Register a Tool Function <a name="register"></a>
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
class Request(BaseModel):
|
|
47
|
+
jschema: str = Field("urn:sd:schema:some_tool.request.1", alias="$schema")
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
class Result(BaseModel):
|
|
51
|
+
jschema: str = Field("urn:sd:schema:some_tool.1", alias="$schema")
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
def some_tool(req: Request) -> Result:
|
|
55
|
+
"""
|
|
56
|
+
Here should go a quite extensive description of what the tool can be
|
|
57
|
+
used for so that an agent can work out if this tool is useful in
|
|
58
|
+
a specific context.
|
|
59
|
+
|
|
60
|
+
DO NOT ADD PARAMTER AND RETURN DECRIPTIONS -
|
|
61
|
+
DESCRIBE THEM IN THE `Request` MODEL
|
|
62
|
+
"""
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
return Result(...)
|
|
66
|
+
|
|
67
|
+
add_tool_api_route(app, "/", some_tool, opts=ToolOptions(tags=["Great Tool"]))
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Start the Service <a name="start"></a>
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
app = FastAPI(
|
|
74
|
+
..
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
start_tool_server(app, some_tool)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### JSON-RPC Middleware <a name="json-rpc"></a>
|
|
82
|
+
|
|
83
|
+
This middleware will convert any `POST /` with a payload
|
|
84
|
+
following the [JSON-RPC](https://www.jsonrpc.org/specification)
|
|
85
|
+
specification to an internal `POST /{method}` and will return
|
|
86
|
+
the result formatted according to the JSON-RPC spec.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from ivcap_fastapi import use_json_rpc_middleware
|
|
90
|
+
|
|
91
|
+
app = FastAPI(
|
|
92
|
+
..
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
use_json_rpc_middleware(app)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Try-Later Middleware <a name="try-later"></a>
|
|
99
|
+
|
|
100
|
+
This middleware is supporting the use case where the execution of a
|
|
101
|
+
requested service is taking longer than the caller is willing to wait.
|
|
102
|
+
A typical use case is where the service is itself outsourcing the execution
|
|
103
|
+
to some other long-running service but may immediately receive a reference
|
|
104
|
+
to the eventual result.
|
|
105
|
+
|
|
106
|
+
In this case, raising a `TryLaterException` will return with a 204
|
|
107
|
+
status code and additional information on how to later check back for the
|
|
108
|
+
result.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from ivcap_fastapi import TryLaterException, use_try_later_middleware
|
|
112
|
+
use_try_later_middleware(app)
|
|
113
|
+
|
|
114
|
+
@app.post("/big_job")
|
|
115
|
+
def big_job(req: Request) -> Response:
|
|
116
|
+
jobID, expected_exec_time = scheduling_big_job(req)
|
|
117
|
+
raise TryLaterException(f"/big_job/jobs/{jobID}", expected_exec_time)
|
|
118
|
+
|
|
119
|
+
@app.get("/big_job/jobs/{jobID}")
|
|
120
|
+
def get_job(jobID: str) -> Response:
|
|
121
|
+
resp = find_result_for(job_id)
|
|
122
|
+
return resp
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Specifically, raising `TryLaterException(location, delay)` will
|
|
126
|
+
return an HTTP response with a 204 status code with the additional
|
|
127
|
+
HTTP headers `Location` and `Retry-Later` set to `location` and
|
|
128
|
+
`delay` respectively.
|
|
129
|
+
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# ivcap_ai_tool: A python library for building AI tools for the IVCAP platform
|
|
2
|
+
|
|
3
|
+
<a href="https://scan.coverity.com/projects/ivcap-works-ivcap-ai-tool-sdk-python">
|
|
4
|
+
<img alt="Coverity Scan Build Status"
|
|
5
|
+
src="https://img.shields.io/coverity/scan/31491.svg"/>
|
|
6
|
+
</a>
|
|
7
|
+
|
|
8
|
+
A python library containing various helper and middleware functions
|
|
9
|
+
to simplify developing AI tools to be deployed on IVCAP.
|
|
10
|
+
|
|
11
|
+
> **Note:** A template git repositiory using this library can be found on github
|
|
12
|
+
[ivcap-works/ivcap-python-ai-tool-template](https://github.com/ivcap-works/ivcap-python-ai-tool-template). You may clone that and start from there.
|
|
13
|
+
|
|
14
|
+
## Content
|
|
15
|
+
|
|
16
|
+
* [Register a Tool Function](#register)
|
|
17
|
+
* [Start the Service](#start)
|
|
18
|
+
* [JSON-RPC Middleware](#json-rpc)
|
|
19
|
+
* [Try-Later Middleware](#try-later)
|
|
20
|
+
|
|
21
|
+
### Register a Tool Function <a name="register"></a>
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
class Request(BaseModel):
|
|
25
|
+
jschema: str = Field("urn:sd:schema:some_tool.request.1", alias="$schema")
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
class Result(BaseModel):
|
|
29
|
+
jschema: str = Field("urn:sd:schema:some_tool.1", alias="$schema")
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
def some_tool(req: Request) -> Result:
|
|
33
|
+
"""
|
|
34
|
+
Here should go a quite extensive description of what the tool can be
|
|
35
|
+
used for so that an agent can work out if this tool is useful in
|
|
36
|
+
a specific context.
|
|
37
|
+
|
|
38
|
+
DO NOT ADD PARAMTER AND RETURN DECRIPTIONS -
|
|
39
|
+
DESCRIBE THEM IN THE `Request` MODEL
|
|
40
|
+
"""
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
return Result(...)
|
|
44
|
+
|
|
45
|
+
add_tool_api_route(app, "/", some_tool, opts=ToolOptions(tags=["Great Tool"]))
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Start the Service <a name="start"></a>
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
app = FastAPI(
|
|
52
|
+
..
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
start_tool_server(app, some_tool)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### JSON-RPC Middleware <a name="json-rpc"></a>
|
|
60
|
+
|
|
61
|
+
This middleware will convert any `POST /` with a payload
|
|
62
|
+
following the [JSON-RPC](https://www.jsonrpc.org/specification)
|
|
63
|
+
specification to an internal `POST /{method}` and will return
|
|
64
|
+
the result formatted according to the JSON-RPC spec.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from ivcap_fastapi import use_json_rpc_middleware
|
|
68
|
+
|
|
69
|
+
app = FastAPI(
|
|
70
|
+
..
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
use_json_rpc_middleware(app)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Try-Later Middleware <a name="try-later"></a>
|
|
77
|
+
|
|
78
|
+
This middleware is supporting the use case where the execution of a
|
|
79
|
+
requested service is taking longer than the caller is willing to wait.
|
|
80
|
+
A typical use case is where the service is itself outsourcing the execution
|
|
81
|
+
to some other long-running service but may immediately receive a reference
|
|
82
|
+
to the eventual result.
|
|
83
|
+
|
|
84
|
+
In this case, raising a `TryLaterException` will return with a 204
|
|
85
|
+
status code and additional information on how to later check back for the
|
|
86
|
+
result.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from ivcap_fastapi import TryLaterException, use_try_later_middleware
|
|
90
|
+
use_try_later_middleware(app)
|
|
91
|
+
|
|
92
|
+
@app.post("/big_job")
|
|
93
|
+
def big_job(req: Request) -> Response:
|
|
94
|
+
jobID, expected_exec_time = scheduling_big_job(req)
|
|
95
|
+
raise TryLaterException(f"/big_job/jobs/{jobID}", expected_exec_time)
|
|
96
|
+
|
|
97
|
+
@app.get("/big_job/jobs/{jobID}")
|
|
98
|
+
def get_job(jobID: str) -> Response:
|
|
99
|
+
resp = find_result_for(job_id)
|
|
100
|
+
return resp
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Specifically, raising `TryLaterException(location, delay)` will
|
|
104
|
+
return an HTTP response with a 204 status code with the additional
|
|
105
|
+
HTTP headers `Location` and `Retry-Later` set to `location` and
|
|
106
|
+
`delay` respectively.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "ivcap_service"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "SDK library for building services for the IVCAP platform"
|
|
5
|
+
|
|
6
|
+
authors = ["Max Ott <max.ott@csiro.au>"]
|
|
7
|
+
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
|
|
10
|
+
include = ["src/ivcap_service/py.typed"]
|
|
11
|
+
|
|
12
|
+
[tool.poetry.dependencies]
|
|
13
|
+
python = ">=3.8"
|
|
14
|
+
uuid6 = "2024.7.10"
|
|
15
|
+
cachetools = "^5.5.2"
|
|
16
|
+
opentelemetry-distro = "^0.51b0"
|
|
17
|
+
opentelemetry-exporter-otlp = "^1.30.0"
|
|
18
|
+
opentelemetry-instrumentation-fastapi = "^0.51b0"
|
|
19
|
+
opentelemetry-instrumentation-requests = "^0.51b0"
|
|
20
|
+
opentelemetry-instrumentation-httpx = "^0.51b0"
|
|
21
|
+
|
|
22
|
+
[tool.poetry.dev-dependencies]
|
|
23
|
+
|
|
24
|
+
[tool.poetry.group.dev.dependencies]
|
|
25
|
+
fastapi = { version = "^0.111.1", extras = ["standard"] }
|
|
26
|
+
pytest = "^7.1.3"
|
|
27
|
+
pytest-cov = "^4.1.0"
|
|
28
|
+
Sphinx = "^5.2.3"
|
|
29
|
+
# myst-nb = "^1.2.0"
|
|
30
|
+
autoapi = "^2.0.1"
|
|
31
|
+
sphinx-autoapi = "^2.0.0"
|
|
32
|
+
sphinx-rtd-theme = "^1.0.0"
|
|
33
|
+
licenseheaders = "^0.8.8"
|
|
34
|
+
|
|
35
|
+
[tool.semantic_release]
|
|
36
|
+
version_variable = "pyproject.toml:version" # version location
|
|
37
|
+
branch = "main" # branch to make releases of
|
|
38
|
+
build_command = "poetry build" # build dists
|
|
39
|
+
dist_path = "dist/" # where to put dists
|
|
40
|
+
upload_to_release = true # auto-create GitHub release
|
|
41
|
+
upload_to_pypi = false # don't auto-upload to PyPI
|
|
42
|
+
remove_dist = false # don't remove dists
|
|
43
|
+
patch_without_tag = true # patch release by default
|
|
44
|
+
|
|
45
|
+
[build-system]
|
|
46
|
+
requires = ["poetry-core>=1.0.0"]
|
|
47
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2023 Commonwealth Scientific and Industrial Research Organisation (CSIRO). All rights reserved.
|
|
3
|
+
# Use of this source code is governed by a BSD-style license that can be
|
|
4
|
+
# found in the LICENSE file. See the AUTHORS file for names of contributors.
|
|
5
|
+
#
|
|
6
|
+
""" A library for building services for the IVCAP platform"""
|
|
7
|
+
|
|
8
|
+
from .version import __version__
|
|
9
|
+
from .logger import getLogger, logging_init
|
|
10
|
+
from .service import start_batch_service
|
|
11
|
+
from .service_definition import create_service_definition
|
|
12
|
+
from .tool_definition import create_tool_definition, print_tool_definition
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2023 Commonwealth Scientific and Industrial Research Organisation (CSIRO). All rights reserved.
|
|
3
|
+
# Use of this source code is governed by a BSD-style license that can be
|
|
4
|
+
# found in the LICENSE file. See the AUTHORS file for names of contributors.
|
|
5
|
+
#
|
|
6
|
+
from dataclasses import Field, dataclass
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import traceback
|
|
10
|
+
from typing import Any, Callable, Generic, List, Optional, TypeVar, Union, BinaryIO
|
|
11
|
+
|
|
12
|
+
from time import sleep
|
|
13
|
+
from urllib.parse import urlparse, urlunparse
|
|
14
|
+
import httpx
|
|
15
|
+
from pydantic import BaseModel, HttpUrl
|
|
16
|
+
from ivcap_fastapi import getLogger
|
|
17
|
+
|
|
18
|
+
from .types import BinaryResult, ExecutionError, IvcapResult
|
|
19
|
+
|
|
20
|
+
logger = getLogger("ivcap")
|
|
21
|
+
|
|
22
|
+
# Number of attempt to deliver job result before giving up
|
|
23
|
+
MAX_DELIVER_RESULT_ATTEMPTS = 4
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def verify_result(result: any, job_id: str, logger) -> any:
|
|
27
|
+
if isinstance(result, ExecutionError):
|
|
28
|
+
return result
|
|
29
|
+
if isinstance(result, BaseModel):
|
|
30
|
+
try:
|
|
31
|
+
return IvcapResult(
|
|
32
|
+
content=result.model_dump_json(by_alias=True),
|
|
33
|
+
content_type="application/json",
|
|
34
|
+
raw=result,
|
|
35
|
+
)
|
|
36
|
+
except Exception as ex:
|
|
37
|
+
msg = f"{job_id}: cannot json serialise pydantic isntance - {str(ex)}"
|
|
38
|
+
logger.warning(msg)
|
|
39
|
+
return ExecutionError(
|
|
40
|
+
error=msg,
|
|
41
|
+
type=type(ex).__name__,
|
|
42
|
+
traceback=traceback.format_exc()
|
|
43
|
+
)
|
|
44
|
+
if isinstance(result, BinaryResult):
|
|
45
|
+
return IvcapResult(content=result.content, content_type=result.content_type)
|
|
46
|
+
if isinstance(result, str):
|
|
47
|
+
return IvcapResult(content=result, content_type="text/plain", raw=result)
|
|
48
|
+
if isinstance(result, bytes):
|
|
49
|
+
# If it's a byte array, return it as is
|
|
50
|
+
return IvcapResult(
|
|
51
|
+
content=result,
|
|
52
|
+
content_type="application/octet-stream",
|
|
53
|
+
raw=result,
|
|
54
|
+
)
|
|
55
|
+
if isinstance(result, BinaryIO):
|
|
56
|
+
# If it's a file handler, return it as is
|
|
57
|
+
return IvcapResult(
|
|
58
|
+
content=result,
|
|
59
|
+
content_type="application/octet-stream",
|
|
60
|
+
raw=result
|
|
61
|
+
)
|
|
62
|
+
# normal model which should be serialisable
|
|
63
|
+
try:
|
|
64
|
+
result = IvcapResult(
|
|
65
|
+
content=json.dumps(result),
|
|
66
|
+
content_type="application/json"
|
|
67
|
+
)
|
|
68
|
+
except Exception as ex:
|
|
69
|
+
msg = f"{job_id}: cannot json serialise result - {str(ex)}"
|
|
70
|
+
logger.warning(msg)
|
|
71
|
+
result = ExecutionError(
|
|
72
|
+
error=msg,
|
|
73
|
+
type=type(ex).__name__,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def push_result(result: Union[IvcapResult, ExecutionError], job_id: str, authorization: Optional[str]=None):
|
|
77
|
+
"""Actively push result to sidecar, fail quietly."""
|
|
78
|
+
ivcap_url = get_ivcap_url()
|
|
79
|
+
if ivcap_url is None:
|
|
80
|
+
logger.warning(f"{job_id}: no ivcap url found - cannot push result")
|
|
81
|
+
return
|
|
82
|
+
url = urlunparse(ivcap_url._replace(path=f"/results/{job_id}"))
|
|
83
|
+
|
|
84
|
+
content_type="text/plain"
|
|
85
|
+
content="SOMETHING WENT WRONG _ PLEASE REPORT THIS ERROR"
|
|
86
|
+
is_error = False
|
|
87
|
+
if not (isinstance(result, ExecutionError) or isinstance(result, IvcapResult)):
|
|
88
|
+
msg = f"{job_id}: expected 'IvcapResult' or 'ExecutionError' but got {type(result)}"
|
|
89
|
+
logger.warning(msg)
|
|
90
|
+
result = ExecutionError(
|
|
91
|
+
error=msg,
|
|
92
|
+
type='InternalError',
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if isinstance(result, IvcapResult):
|
|
96
|
+
content = result.content
|
|
97
|
+
content_type = result.content_type
|
|
98
|
+
else:
|
|
99
|
+
is_error = True
|
|
100
|
+
if not isinstance(result, ExecutionError):
|
|
101
|
+
# this should never happen
|
|
102
|
+
logger.error(f"{job_id}: expected 'ExecutionError' but got {type(result)}")
|
|
103
|
+
result = ExecutionError(
|
|
104
|
+
error="please report unexpected internal error - expected 'ExecutionError' but got {type(result)}",
|
|
105
|
+
type="internal_error",
|
|
106
|
+
)
|
|
107
|
+
content = result.model_dump_json(by_alias=True)
|
|
108
|
+
content_type = "application/json"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
wait_time = 1
|
|
112
|
+
attempt = 0
|
|
113
|
+
headers = {
|
|
114
|
+
"Content-Type": content_type,
|
|
115
|
+
"Is-Error": str(is_error),
|
|
116
|
+
}
|
|
117
|
+
if not (authorization == None or authorization == ""):
|
|
118
|
+
headers["Authorization"] = authorization
|
|
119
|
+
|
|
120
|
+
while attempt < MAX_DELIVER_RESULT_ATTEMPTS:
|
|
121
|
+
try:
|
|
122
|
+
response = httpx.post(
|
|
123
|
+
url=url,
|
|
124
|
+
headers=headers,
|
|
125
|
+
data=content,
|
|
126
|
+
)
|
|
127
|
+
response.raise_for_status()
|
|
128
|
+
return
|
|
129
|
+
except Exception as e:
|
|
130
|
+
attempt += 1
|
|
131
|
+
logger.info(f"{job_id}: attempt #{attempt} failed to push result - will try again in {wait_time} sec - {type(e)}: {e}")
|
|
132
|
+
sleep(wait_time)
|
|
133
|
+
wait_time *= 2
|
|
134
|
+
|
|
135
|
+
logger.warning(f"{job_id}: giving up pushing result after {attempt} attempts")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def get_ivcap_url() -> HttpUrl:
|
|
139
|
+
"""
|
|
140
|
+
Returns the sidecar URL from the request headers.
|
|
141
|
+
"""
|
|
142
|
+
base = os.getenv("IVCAP_BASE_URL")
|
|
143
|
+
if base == "" or base is None:
|
|
144
|
+
return None
|
|
145
|
+
return urlparse(base)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2023 Commonwealth Scientific and Industrial Research Organisation (CSIRO). All rights reserved.
|
|
3
|
+
# Use of this source code is governed by a BSD-style license that can be
|
|
4
|
+
# found in the LICENSE file. See the AUTHORS file for names of contributors.
|
|
5
|
+
#
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
from logging.config import dictConfig
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
LOGGING_CONFIG={}
|
|
12
|
+
|
|
13
|
+
def getLogger(name: str) -> logging.Logger:
|
|
14
|
+
return logging.getLogger(name)
|
|
15
|
+
|
|
16
|
+
def service_log_config():
|
|
17
|
+
return LOGGING_CONFIG
|
|
18
|
+
|
|
19
|
+
def logging_init(cfg_path: str=None):
|
|
20
|
+
if not cfg_path:
|
|
21
|
+
script_dir = os.path.dirname(__file__)
|
|
22
|
+
cfg_path = os.path.join(script_dir, "logging.json")
|
|
23
|
+
|
|
24
|
+
with open(cfg_path, 'r') as file:
|
|
25
|
+
global LOGGING_CONFIG
|
|
26
|
+
LOGGING_CONFIG = json.load(file)
|
|
27
|
+
dictConfig(LOGGING_CONFIG)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"formatters": {
|
|
4
|
+
"default": {
|
|
5
|
+
"format": "%(asctime)s %(levelname)s (%(name)s): %(message)s",
|
|
6
|
+
"datefmt": "%Y-%m-%dT%H:%M:%S%z"
|
|
7
|
+
},
|
|
8
|
+
"access": {
|
|
9
|
+
"()": "uvicorn.logging.AccessFormatter",
|
|
10
|
+
"fmt": "%(asctime)s %(levelname)s (access): \"%(request_line)s\" %(status_code)s",
|
|
11
|
+
"datefmt": "%Y-%m-%dT%H:%M:%S%z"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"handlers": {
|
|
15
|
+
"default": {
|
|
16
|
+
"class": "logging.StreamHandler",
|
|
17
|
+
"level": "DEBUG",
|
|
18
|
+
"formatter": "default",
|
|
19
|
+
"stream": "ext://sys.stderr"
|
|
20
|
+
},
|
|
21
|
+
"access": {
|
|
22
|
+
"formatter": "access",
|
|
23
|
+
"class": "logging.StreamHandler",
|
|
24
|
+
"stream": "ext://sys.stdout"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"root": {
|
|
28
|
+
"level": "INFO",
|
|
29
|
+
"handlers": [
|
|
30
|
+
"default"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"loggers": {
|
|
34
|
+
"app": {
|
|
35
|
+
"level": "DEBUG"
|
|
36
|
+
},
|
|
37
|
+
"uvicorn.access": {
|
|
38
|
+
"handlers": [
|
|
39
|
+
"access"
|
|
40
|
+
],
|
|
41
|
+
"level": "INFO",
|
|
42
|
+
"propagate": false
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"disable_existing_loggers": false
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker file for PEP 561
|