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.
@@ -0,0 +1,5 @@
1
+ # Initial version authors
2
+
3
+ * Max Ott <max.ott@csiro.au>
4
+
5
+ # Partial list of contributors
@@ -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